提交 b87fe01f 编写于 作者: H hudingrong

2024-04-024-rule-stock

上级 b4c652e7
24-04-05.17:42:28.254 [RMI TCP Connection(16)-2.0.0.1] WARN HealthEndpointSupport - Health contributor org.springframework.boot.actuate.jdbc.DataSourceHealthIndicator (db) took 53954ms to respond
24-04-05.17:42:39.038 [redisson-netty-2-2] WARN RedisReactiveHealthIndicator - Redis health check failed
org.springframework.data.redis.RedisSystemException: NOAUTH Authentication required.. channel: [id: 0x2c1446a4, L:/192.168.30.92:64003 - R:120.78.91.227/120.78.91.227:6379] data: CommandData [promise=java.util.concurrent.CompletableFuture@331b138c[Not completed, 1 dependents], command=(INFO), params=[server], codec=org.redisson.client.codec.StringCodec]; nested exception is org.redisson.client.RedisAuthRequiredException: NOAUTH Authentication required.. channel: [id: 0x2c1446a4, L:/192.168.30.92:64003 - R:120.78.91.227/120.78.91.227:6379] data: CommandData [promise=java.util.concurrent.CompletableFuture@331b138c[Not completed, 1 dependents], command=(INFO), params=[server], codec=org.redisson.client.codec.StringCodec]
at org.redisson.spring.data.connection.RedissonBaseReactive.lambda$read$5(RedissonBaseReactive.java:91)
at reactor.core.publisher.Mono.lambda$onErrorMap$31(Mono.java:3811)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:94)
at reactor.core.publisher.MonoNext$NextSubscriber.onError(MonoNext.java:93)
at reactor.core.publisher.FluxCreate$BaseSink.error(FluxCreate.java:474)
at reactor.core.publisher.FluxCreate$BufferAsyncSink.drain(FluxCreate.java:802)
at reactor.core.publisher.FluxCreate$BufferAsyncSink.error(FluxCreate.java:747)
at reactor.core.publisher.FluxCreate$SerializedFluxSink.drainLoop(FluxCreate.java:237)
at reactor.core.publisher.FluxCreate$SerializedFluxSink.drain(FluxCreate.java:213)
at reactor.core.publisher.FluxCreate$SerializedFluxSink.error(FluxCreate.java:189)
at org.redisson.reactive.CommandReactiveService.lambda$reactive$1(CommandReactiveService.java:60)
at java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:774)
at java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:750)
at java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:488)
at java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:1990)
at org.redisson.command.RedisExecutor.handleError(RedisExecutor.java:583)
at org.redisson.command.RedisExecutor.handleResult(RedisExecutor.java:565)
at org.redisson.command.RedisExecutor.checkAttemptPromise(RedisExecutor.java:553)
at org.redisson.command.RedisExecutor.lambda$execute$5(RedisExecutor.java:195)
at java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:774)
at java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:750)
at java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:488)
at java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:1990)
at org.redisson.client.protocol.CommandData.tryFailure(CommandData.java:87)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:368)
at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:205)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:144)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:120)
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:529)
at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:748)
Caused by: org.redisson.client.RedisAuthRequiredException: NOAUTH Authentication required.. channel: [id: 0x2c1446a4, L:/192.168.30.92:64003 - R:120.78.91.227/120.78.91.227:6379] data: CommandData [promise=java.util.concurrent.CompletableFuture@331b138c[Not completed, 1 dependents], command=(INFO), params=[server], codec=org.redisson.client.codec.StringCodec]
... 23 common frames omitted
24-04-05.17:42:39.145 [RMI TCP Connection(16)-2.0.0.1] WARN HealthEndpointSupport - Health contributor org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration$AdaptedReactiveHealthContributors$1 (redis) took 10845ms to respond
24-04-05.18:05:52.824 [HikariPool-1 housekeeper] WARN HikariPool - HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=4m57s651ms233µs).
24-04-05.18:51:19.253 [redisson-netty-2-22] ERROR rejectedExecution - Failed to submit a listener notification task. Event loop shut down?
24-04-24.16:20:46.983 [redisson-netty-2-22] ERROR rejectedExecution - Failed to submit a listener notification task. Event loop shut down?
java.util.concurrent.RejectedExecutionException: event executor terminated
at io.netty.util.concurrent.SingleThreadEventExecutor.reject(SingleThreadEventExecutor.java:934)
at io.netty.util.concurrent.SingleThreadEventExecutor.offerTask(SingleThreadEventExecutor.java:351)
......
......@@ -3,9 +3,11 @@ package cn.bugstack;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@Configurable
@EnableScheduling
public class Application {
public static void main(String[] args){
......
......@@ -16,6 +16,11 @@
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<update id="updateStrategyAwardStock" parameterType="cn.bugstack.infrastructure.persistent.po.StrategyAward">
update strategy_award
set award_count_surplus = award_count_surplus - 1
where strategy_id = #{strategyId} and award_id = #{awardId} and award_count_surplus > 0
</update>
<select id="queryStrategyAwardList" resultMap="dataMap">
select strategy_id
......
package cn.bugstack.domain.strategy.model.valobj;
import cn.bugstack.types.common.Constants;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @description: 抽奖策略规则规则值对象;值对象,没有唯一ID,仅限于从数据库查询对象
* @author: hdr
......
package cn.bugstack.domain.strategy.model.valobj;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @description: 策略奖品库存Key标识值对象
* @author: hdr
* @PACKAGE_NAME: cn.bugstack.domain.strategy.model.valobj
* @DATE: 2024/4/23
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StrategyAwardStockKeyVO {
// 策略ID
private Long strategyId;
// 奖品ID
private Integer awardId;
}
......@@ -5,6 +5,7 @@ import cn.bugstack.domain.strategy.model.entity.StrategyEntity;
import cn.bugstack.domain.strategy.model.entity.StrategyRuleEntity;
import cn.bugstack.domain.strategy.model.valobj.RuleTreeVO;
import cn.bugstack.domain.strategy.model.valobj.StrategyAwardRuleModelVO;
import cn.bugstack.domain.strategy.model.valobj.StrategyAwardStockKeyVO;
import java.math.BigDecimal;
import java.util.HashMap;
......@@ -31,7 +32,7 @@ public interface IStrategyRepository {
* @param rateRange 概率
* @param shuffleStrategyAwardSearchRateTables 所有保存的概率
*/
void storeStrategyAwardSearchRateTables(String key, Integer rateRange, HashMap<Integer, Integer> shuffleStrategyAwardSearchRateTables);
void storeStrategyAwardSearchRateTables(String key, Integer rateRange, Map<Integer, Integer> shuffleStrategyAwardSearchRateTables);
/**
*
......@@ -103,5 +104,37 @@ public interface IStrategyRepository {
*/
RuleTreeVO queryRuleTreeVOByTreeId(String treeId);
/**
* 缓存奖品库存
*
* @param cacheKey key
* @param awardCount 库存值
*/
void cacheStrategyAwardCount(String cacheKey, Integer awardCount);
/**
* 缓存key,decr 方式扣减库存
* @param cacheKey 缓存key
* @return 扣减结果
*/
boolean subtractionAwardStock(String cacheKey);
/**
* 写入奖品库存消费对接
* @param strategyAwardStockKeyVO
*/
void awardStockConsumeSendQueue(StrategyAwardStockKeyVO strategyAwardStockKeyVO);
/**
* 获取奖品库存消费对列
*/
StrategyAwardStockKeyVO takeQueueValue() throws InterruptedException;
/**
* 更新奖品库存消耗
*
* @param strategyId 策略ID
* @param awardId 奖品ID
*/
void updateStrategyAwardStock(Long strategyId, Integer awardId);
}
package cn.bugstack.domain.strategy.service.rule;
package cn.bugstack.domain.strategy.service;
import cn.bugstack.domain.strategy.model.entity.RaffleAwardEntity;
import cn.bugstack.domain.strategy.model.entity.RaffleFactorEntity;
......@@ -19,7 +19,7 @@ import org.apache.commons.lang3.StringUtils;
* @DATE: 2024/4/23
*/
@Slf4j
public abstract class AbstractRaffleStrategy implements IRaffleStrategy {
public abstract class AbstractRaffleStrategy implements IRaffleStrategy, IRaffleStock {
// 策略仓储服务 -> domain层像一个大厨,仓储层提供米面粮油
protected IStrategyRepository repository;
// 策略调度服务 -> 只负责抽奖处理,通过新增接口的方式,隔离职责,不需要使用方关心或者调用抽奖的初始化
......
package cn.bugstack.domain.strategy.service;
import cn.bugstack.domain.strategy.model.valobj.StrategyAwardStockKeyVO;
/**
* @description: 抽奖库存相关服务,获取库存消耗队列
* @author: hdr
* @PACKAGE_NAME: cn.bugstack.domain.strategy.service
* @DATE: 2024/4/24
*/
public interface IRaffleStock {
/**
* 获取奖品库存消耗队列
*
* @return 奖品库存Key信息
* @throws InterruptedException 异常
*/
StrategyAwardStockKeyVO takeQueueValue() throws InterruptedException;
/**
* 更新奖品库存消耗记录
*
* @param strategyId 策略ID
* @param awardId 奖品ID
*/
void updateStrategyAwardStock(Long strategyId, Integer awardId);
}
......@@ -21,4 +21,13 @@ public interface IStrategyDispatch {
* @return
*/
Integer getRandomAwardId(Long strategyId, String ruleWeightValue);
/**
* 根据策略ID和奖品ID,扣减奖品缓存库存
*
* @param strategyId
* @param awardId
* @return
*/
boolean subtractionAwardStock(Long strategyId, Integer awardId);
}
......@@ -4,6 +4,7 @@ import cn.bugstack.domain.strategy.model.entity.StrategyAwardEntity;
import cn.bugstack.domain.strategy.model.entity.StrategyEntity;
import cn.bugstack.domain.strategy.model.entity.StrategyRuleEntity;
import cn.bugstack.domain.strategy.repository.IStrategyRepository;
import cn.bugstack.types.common.Constants;
import cn.bugstack.types.enums.ResponseCode;
import cn.bugstack.types.exception.AppException;
import com.alibaba.fastjson.JSONObject;
......@@ -30,21 +31,25 @@ public class StrategyArmory implements IStrategyArmory, IStrategyDispatch {
@Autowired
private IStrategyRepository repository;
public static void main(String[] args) {
StrategyArmory strategyArmory = new StrategyArmory();
strategyArmory.assembleLotteryStrategy(100001L);
}
@Override
public boolean assembleLotteryStrategy(Long strategy) {
// 1. 查询策略配置
List<StrategyAwardEntity> awardEntityList = repository.queryStrategyAwardList(strategy);
// 2. 缓存奖品库存【用于扣减库存使用】
for (StrategyAwardEntity strategyAward : awardEntityList) {
Integer awardId = strategyAward.getAwardId();
Integer awardCount = strategyAward.getAwardCount();
cacheStrategyAwardCount(strategy, awardId, awardCount);
}
// 3.1 默认装配配置【全量抽奖概率】
assembleLotteryStrategy(String.valueOf(strategy),awardEntityList);
// 2. 权重策略陪住 - 使用于 rule_weight 权重规则配置
StrategyEntity strategyEntity = repository.queryStrategyEntityByStrategyId(strategy);
if (StringUtils.isEmpty(strategyEntity.getRuleWeight())) return true;
String ruleWeight = strategyEntity.getRuleWeight();
// 2.2 查询策略规则
......@@ -65,6 +70,11 @@ public class StrategyArmory implements IStrategyArmory, IStrategyDispatch {
}
private void cacheStrategyAwardCount(Long strategyId, Integer awardId, Integer awardCount) {
String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_KEY + strategyId + Constants.UNDERLINE + awardId;
repository.cacheStrategyAwardCount(cacheKey,awardCount);
}
private void assembleLotteryStrategy(String key,List<StrategyAwardEntity> awardEntityList) {
// 1. 获取最小概率值
BigDecimal minAwardRate = awardEntityList.stream()
......@@ -72,37 +82,67 @@ public class StrategyArmory implements IStrategyArmory, IStrategyDispatch {
.min(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO);
// 2. 获取概率值总和
BigDecimal totalAwardRate = awardEntityList.stream()
.map(StrategyAwardEntity::getAwardRate)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 2. 循环计算找到概率范围值
BigDecimal rateRange = BigDecimal.valueOf(convert(minAwardRate.doubleValue()));
// 3. 用 1% 0.001获取概率范围,百分位、千分位、万分位
BigDecimal rateRange = totalAwardRate.divide(minAwardRate, 0, RoundingMode.CEILING);
// 3. 生成策略奖品概率查找表「这里指需要在list集合中,存放上对应的奖品占位即可,占位越多等于概率越高」
List<Integer> strategyAwardSearchRateTables = new ArrayList<>(rateRange.intValue());
// BigDecimal rateRange = totalAwardRate.divide(minAwardRate, 0, RoundingMode.CEILING);
ArrayList<Object> strategyAwardSearchRateTables = new ArrayList(rateRange.intValue());
for (StrategyAwardEntity strategyAwardEntity : awardEntityList) {
Integer awardId = strategyAwardEntity.getAwardId();
BigDecimal awardRate = strategyAwardEntity.getAwardRate();
// ArrayList<Object> strategyAwardSearchRateTables = new ArrayList(rateRange.intValue());
// 4. 计算出每个概率值需要存储到查找表的数量,循环填充
for (int i = 0; i < rateRange.multiply(awardRate).setScale(0, RoundingMode.CEILING).intValue(); i++) {
for (StrategyAwardEntity strategyAward : awardEntityList) {
Integer awardId = strategyAward.getAwardId();
BigDecimal awardRate = strategyAward.getAwardRate();
// 计算出每个概率值需要存放到查找表的数量,循环填充
for (int i = 0; i < rateRange.multiply(awardRate).intValue(); i++) {
strategyAwardSearchRateTables.add(awardId);
}
// 5. 乱序
}
// 4. 对存储的奖品进行乱序操作
Collections.shuffle(strategyAwardSearchRateTables);
// 6. 组装抽奖表
HashMap<Integer, Integer> shuffleStrategyAwardSearchRateTables = new HashMap<>();
// 5. 生成出Map集合,key值,对应的就是后续的概率值。通过概率来获得对应的奖品ID
Map<Integer, Integer> shuffleStrategyAwardSearchRateTable = new LinkedHashMap<>();
for (int i = 0; i < strategyAwardSearchRateTables.size(); i++) {
shuffleStrategyAwardSearchRateTables.put(i, (Integer) strategyAwardSearchRateTables.get(i));
shuffleStrategyAwardSearchRateTable.put(i, strategyAwardSearchRateTables.get(i));
}
// 6. 存放到 Redis
repository.storeStrategyAwardSearchRateTables(key, shuffleStrategyAwardSearchRateTable.size(), shuffleStrategyAwardSearchRateTable);
// for (StrategyAwardEntity strategyAwardEntity : awardEntityList) {
// Integer awardId = strategyAwardEntity.getAwardId();
// BigDecimal awardRate = strategyAwardEntity.getAwardRate();
//
// // 4. 计算出每个概率值需要存储到查找表的数量,循环填充
// for (int i = 0; i < rateRange.multiply(awardRate).setScale(0, RoundingMode.CEILING).intValue(); i++) {
// strategyAwardSearchRateTables.add(awardId);
// }
//
// // 5. 乱序
// Collections.shuffle(strategyAwardSearchRateTables);
//
// // 6. 组装抽奖表
// HashMap<Integer, Integer> shuffleStrategyAwardSearchRateTables = new HashMap<>();
// for (int i = 0; i < strategyAwardSearchRateTables.size(); i++) {
// shuffleStrategyAwardSearchRateTables.put(i, (Integer) strategyAwardSearchRateTables.get(i));
// }
//
// // 7. 存储到 redis中
//
// }
}
// 7. 存储到 redis中
repository.storeStrategyAwardSearchRateTables(key,shuffleStrategyAwardSearchRateTables.size(), shuffleStrategyAwardSearchRateTables);
private double convert(double min) {
double current = min;
double max = 1;
while (current < 1) {
current = current * 10;
max = max * 10;
}
return max;
}
@Override
......@@ -119,4 +159,11 @@ public class StrategyArmory implements IStrategyArmory, IStrategyDispatch {
// 通过生成的随机值,获取概率值奖品查找表的结果
return repository.getStrategyAwardAssemble(key, new SecureRandom().nextInt(rateRange));
}
@Override
public boolean subtractionAwardStock(Long strategyId, Integer awardId) {
String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_KEY + strategyId + Constants.UNDERLINE + awardId;
return repository.subtractionAwardStock(cacheKey);
}
}
package cn.bugstack.domain.strategy.service.raffle;
import cn.bugstack.domain.strategy.model.entity.RaffleFactorEntity;
import cn.bugstack.domain.strategy.model.entity.RuleActionEntity;
import cn.bugstack.domain.strategy.model.entity.RuleMatterEntity;
import cn.bugstack.domain.strategy.model.valobj.RuleLogicCheckTypeVO;
import cn.bugstack.domain.strategy.model.valobj.RuleTreeVO;
import cn.bugstack.domain.strategy.model.valobj.StrategyAwardRuleModelVO;
import cn.bugstack.domain.strategy.model.valobj.StrategyAwardStockKeyVO;
import cn.bugstack.domain.strategy.repository.IStrategyRepository;
import cn.bugstack.domain.strategy.service.armory.IStrategyDispatch;
import cn.bugstack.domain.strategy.service.rule.AbstractRaffleStrategy;
import cn.bugstack.domain.strategy.service.AbstractRaffleStrategy;
import cn.bugstack.domain.strategy.service.rule.chain.ILogicChain;
import cn.bugstack.domain.strategy.service.rule.chain.factory.DefaultChainFactory;
import cn.bugstack.domain.strategy.service.rule.tree.factory.DefaultTreeFactory;
import cn.bugstack.domain.strategy.service.rule.tree.factory.engine.IDecisionTreeEngine;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @description:
* @author: hdr
......@@ -60,4 +49,14 @@ public class DefaultRaffleStrategy extends AbstractRaffleStrategy {
return treeEngine.process(userId, strategyId, awardId);
}
@Override
public StrategyAwardStockKeyVO takeQueueValue() throws InterruptedException {
return repository.takeQueueValue();
}
@Override
public void updateStrategyAwardStock(Long strategyId, Integer awardId) {
repository.updateStrategyAwardStock(strategyId, awardId);
}
}
......@@ -10,6 +10,6 @@ import cn.bugstack.domain.strategy.service.rule.tree.factory.DefaultTreeFactory;
*/
public interface ILogicTreeNode {
DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId);
DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId, String ruleValue);
}
......@@ -44,7 +44,7 @@ public class DecisionTreeEngine implements IDecisionTreeEngine {
while (null != nextNode) {
ILogicTreeNode logicTreeNode = logicTreeNodeGroup.get(ruleTreeNode.getRuleKey());
DefaultTreeFactory.TreeActionEntity logicEntity = logicTreeNode.logic(userId, strategyId, awardId);
DefaultTreeFactory.TreeActionEntity logicEntity = logicTreeNode.logic(userId, strategyId, awardId, ruleTreeNode.getRuleValue());
RuleLogicCheckTypeVO ruleLogicCheckTypeVO = logicEntity.getRuleLogicCheckType();
strategyAwardData = logicEntity.getStrategyAwardVO();
log.info("决策树引擎【{}】treeId:{} node:{} code:{}", ruleTreeVO.getTreeName(), ruleTreeVO.getTreeId(), nextNode, ruleLogicCheckTypeVO.getCode());
......
......@@ -15,10 +15,29 @@ import org.springframework.stereotype.Component;
@Slf4j
@Component("rule_lock")
public class RuleLockLogicTreeNode implements ILogicTreeNode {
// 用户抽奖次数,后续完成这部分流程开发的时候,从数据库/Redis中读取
private Long userRaffleCount = 10L;
@Override
public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId) {
public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId, String ruleValue) {
log.info("规则过滤-次数锁 userId:{} strategyId:{} awardId:{}", userId, strategyId, awardId);
long raffleCount = 0L;
try {
raffleCount = Long.parseLong(ruleValue);
} catch (Exception e) {
throw new RuntimeException("规则过滤-次数锁异常 ruleValue: " + ruleValue + " 配置不正确");
}
// 用户抽奖次数大于规则限定值,规则放行
if (userRaffleCount >= raffleCount) {
return DefaultTreeFactory.TreeActionEntity.builder()
.ruleLogicCheckType(RuleLogicCheckTypeVO.ALLOW)
.build();
}
// 用户抽奖次数小于规则限定值,规则拦截
return DefaultTreeFactory.TreeActionEntity.builder()
.ruleLogicCheckType(RuleLogicCheckTypeVO.TAKE_OVER)
.build();
}
}
......@@ -3,6 +3,7 @@ package cn.bugstack.domain.strategy.service.rule.tree.impl;
import cn.bugstack.domain.strategy.model.valobj.RuleLogicCheckTypeVO;
import cn.bugstack.domain.strategy.service.rule.tree.ILogicTreeNode;
import cn.bugstack.domain.strategy.service.rule.tree.factory.DefaultTreeFactory;
import cn.bugstack.types.common.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
......@@ -16,13 +17,24 @@ import org.springframework.stereotype.Component;
@Component("rule_luck_award")
public class RuleLuckAwardLogicTreeNode implements ILogicTreeNode {
@Override
public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId) {
public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId, String ruleValue) {
log.info("规则过滤-兜底奖品 userId:{} strategyId:{} awardId:{} ruleValue:{}", userId, strategyId, awardId, ruleValue);
String[] split = ruleValue.split(Constants.COLON);
if (split.length == 0) {
log.error("规则过滤-兜底奖品,兜底奖品未配置告警 userId:{} strategyId:{} awardId:{}", userId, strategyId, awardId);
throw new RuntimeException("兜底奖品未配置 " + ruleValue);
}
// 兜底奖励配置
Integer luckAwardId = Integer.valueOf(split[0]);
String awardRuleValue = split.length > 1 ? split[1] : "";
// 返回兜底奖品
log.info("规则过滤-兜底奖品 userId:{} strategyId:{} awardId:{} awardRuleValue:{}", userId, strategyId, luckAwardId, awardRuleValue);
return DefaultTreeFactory.TreeActionEntity.builder()
.ruleLogicCheckType(RuleLogicCheckTypeVO.TAKE_OVER)
.strategyAwardVO(DefaultTreeFactory.StrategyAwardVO.builder()
.awardId(101)
.awardRuleValue("1,100")
.awardId(luckAwardId)
.awardRuleValue(awardRuleValue)
.build())
.build();
}
......
......@@ -17,7 +17,7 @@ import org.springframework.stereotype.Component;
@Component("rule_stock")
public class RuleStockLogicTreeNode implements ILogicTreeNode {
@Override
public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId) {
public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId, String ruleValue) {
return DefaultTreeFactory.TreeActionEntity.builder()
.ruleLogicCheckType(RuleLogicCheckTypeVO.TAKE_OVER)
......
......@@ -29,4 +29,10 @@ public interface IStrategyAwardDao {
* @return
*/
String queryStrategyAwardRuleModels(StrategyAward strategyAward);
/**
* 更新策略获奖奖品
* @param strategyAward
*/
void updateStrategyAwardStock(StrategyAward strategyAward);
}
......@@ -247,4 +247,14 @@ public interface IRedisService {
*/
<T> RBloomFilter<T> getBloomFilter(String key);
Boolean setNx(String lockKey);
/**
* 设置值
*
* @param key key 键
* @param value 值
*/
void setAtomicLong(String key, Integer value);
}
......@@ -13,7 +13,7 @@ import java.time.Duration;
* @DATE: 2024/4/5
*/
@Service("redissonService")
public class RedisService implements IRedisService {
public class RedissonService implements IRedisService {
@Resource
private RedissonClient redissonClient;
......@@ -158,5 +158,15 @@ public class RedisService implements IRedisService {
return redissonClient.getBloomFilter(key);
}
@Override
public Boolean setNx(String key) {
return redissonClient.getBucket(key).trySet("lock");
}
@Override
public void setAtomicLong(String key, Integer value) {
redissonClient.getAtomicLong(key).set(value);
}
}
......@@ -14,16 +14,21 @@ import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBlockingDeque;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RDelayedQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
......@@ -88,12 +93,13 @@ public class StrategyRepository implements IStrategyRepository {
}
@Override
public void storeStrategyAwardSearchRateTables(String key, Integer rateRange, HashMap<Integer, Integer> shuffleStrategyAwardSearchRateTables) {
public void storeStrategyAwardSearchRateTables(String key, Integer rateRange, Map<Integer, Integer> shuffleStrategyAwardSearchRateTables) {
// 1. 存储概率值
redisService.setValue(Constants.RedisKey.STRATEGY_RATE_RANGE_KEY + key, rateRange.intValue());
// 2. 存储概率查找表
Map<Integer, Integer> cacheRateTable = redisService.getMap(Constants.RedisKey.STRATEGY_RATE_TABLE_KEY + key);
cacheRateTable.putAll(shuffleStrategyAwardSearchRateTables);
}
......@@ -241,4 +247,51 @@ public class StrategyRepository implements IStrategyRepository {
return ruleTreeVODB;
}
@Override
public void cacheStrategyAwardCount(String cacheKey, Integer awardCount) {
if (redisService.isExists(cacheKey)) return;
redisService.setAtomicLong(cacheKey,awardCount);
}
@Override
public boolean subtractionAwardStock(String cacheKey) {
long suplus = redisService.decr(cacheKey);
if (suplus < 0) {
// 库存小于0,恢复为0个
redisService.setValue(cacheKey,0);
}
// 1. 按照cacheKey decr 后的值,如 99、98、97 和 key 组成为库存锁的key进行使用。
// 2. 加锁为了兜底,如果后续有恢复库存,手动处理等,也不会超卖。因为所有的可用库存key,都被加锁了。
String lockKey = cacheKey + Constants.UNDERLINE + suplus;
Boolean lock = redisService.setNx(lockKey);
if (!lock) {
log.info("策略奖品库存加锁失败 {}", lockKey);
}
return lock;
}
@Override
public void awardStockConsumeSendQueue(StrategyAwardStockKeyVO strategyAwardStockKeyVO) {
String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_QUERY_KEY;
RBlockingQueue<StrategyAwardStockKeyVO> blockingQueue = redisService.getBlockingQueue(cacheKey);
RDelayedQueue<StrategyAwardStockKeyVO> delayedQueue = redisService.getDelayedQueue(blockingQueue);
delayedQueue.offer(strategyAwardStockKeyVO, 3, TimeUnit.SECONDS);
}
@Override
public StrategyAwardStockKeyVO takeQueueValue() throws InterruptedException {
String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_QUERY_KEY;
RBlockingQueue<StrategyAwardStockKeyVO> destinationQueue = redisService.getBlockingQueue(cacheKey);
return destinationQueue.poll();
}
@Override
public void updateStrategyAwardStock(Long strategyId, Integer awardId) {
StrategyAward strategyAward = new StrategyAward();
strategyAward.setStrategyId(strategyId);
strategyAward.setAwardId(awardId);
strategyAwardDao.updateStrategyAwardStock(strategyAward);
}
}
package cn.bugstack.trigger.job;
import cn.bugstack.domain.strategy.model.valobj.StrategyAwardStockKeyVO;
import cn.bugstack.domain.strategy.service.IRaffleStock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @description:
* @author: hdr
* @PACKAGE_NAME: cn.bugstack.trigger.job
* @DATE: 2024/4/24
*/
@Slf4j
@Component
public class UpdateAwardStockJob {
@Resource
private IRaffleStock raffleStock;
@Scheduled(cron = "0/5 * * * * ?")
public void exec() {
try {
log.info("定时任务,更新奖品消耗库存【延迟队列获取,降低对数据库的更新频次,不要产生竞争】");
StrategyAwardStockKeyVO strategyAwardStockKeyVO = raffleStock.takeQueueValue();
if (null == strategyAwardStockKeyVO) return;
log.info("定时任务,更新奖品消耗库存 strategyId:{} awardId:{}", strategyAwardStockKeyVO.getStrategyId(), strategyAwardStockKeyVO.getAwardId());
raffleStock.updateStrategyAwardStock(strategyAwardStockKeyVO.getStrategyId(), strategyAwardStockKeyVO.getAwardId());
} catch (Exception e) {
log.error("定时任务,更新奖品消耗库存失败", e);
}
}
}
......@@ -5,6 +5,7 @@ public class Constants {
public final static String SPLIT = ",";
public final static String COLON = ":";
public final static String SPACE = " ";
public final static String UNDERLINE = "_";
public static class RedisKey {
public static final String STRATEGY_KEY = "big_market_strategy_key_";
......@@ -12,6 +13,8 @@ public class Constants {
public static String STRATEGY_RATE_TABLE_KEY = "big_market_strategy_rate_table_key_";
public static String STRATEGY_RATE_RANGE_KEY = "big_market_strategy_rate_range_key_";
public static String RULE_TREE_VO_KEY = "rule_tree_vo_key_";
public static String STRATEGY_AWARD_COUNT_KEY = "strategy_award_count_key_";
public static String STRATEGY_AWARD_COUNT_QUERY_KEY = "strategy_award_count_query_key";
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册