diff --git a/bigmarket-app/src/main/resources/mybatis/mapper/strategy_award_mapper.xml b/bigmarket-app/src/main/resources/mybatis/mapper/strategy_award_mapper.xml index 192a902a22e095ce08a0e8ee09eae6d6f1fbe512..2ba6bec126d8ae582272f081e9292f107bec1a85 100644 --- a/bigmarket-app/src/main/resources/mybatis/mapper/strategy_award_mapper.xml +++ b/bigmarket-app/src/main/resources/mybatis/mapper/strategy_award_mapper.xml @@ -46,5 +46,9 @@ from strategy_award where strategy_id = #{strategyId} and award_id = #{awardId} + diff --git a/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/repository/IStrategyRepository.java b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/repository/IStrategyRepository.java index 616a59546176f649775f4ca8458838a33120dbb6..d851ad11486d951032259dd2f57656a2d55cc2e9 100644 --- a/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/repository/IStrategyRepository.java +++ b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/repository/IStrategyRepository.java @@ -21,12 +21,14 @@ public interface IStrategyRepository { List queryStrategyAwardList(Long strategyId); - void storeStrategyAwardSearchRateTable(String key, Integer rateRange, Map strategyAwardSearchRateTable); + void storeStrategyAwardSearchRateTable(String key, Integer rateRange, Map strategyAwardSearchRateTable); + Map getMap(String key); Integer getStrategyAwardAssemble(String key, Integer rateKey); int getRateRange(Long strategyId); + int getRateRange(String key); StrategyEntity queryStrategyEntityByStrategyId(Long strategyId); @@ -142,5 +144,27 @@ public interface IStrategyRepository { * @return 权重规则 */ List queryAwardRuleWeight(Long strategyId); + /** + * 查询有效活动的奖品配置 + * + * @return 奖品配置列表 + */ + List queryOpenActivityStrategyAwardList(); + + /** + * 存储抽奖策略对应的Bean算法 + * + * @param key 策略ID + * @param beanName 策略对象名称 + */ + void cacheStrategyArmoryAlgorithm(String key, String beanName); + + /** + * 获取存储抽奖策略对应的Bean算法 + * + * @param key 策略ID + * @return 策略对象名称 + */ + String queryStrategyArmoryAlgorithmFromCache(String key); } diff --git a/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/AbstractStrategyAlgorithm.java b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/AbstractStrategyAlgorithm.java new file mode 100644 index 0000000000000000000000000000000000000000..a25d09abb4332f229f17d2df34f454cacfe3529b --- /dev/null +++ b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/AbstractStrategyAlgorithm.java @@ -0,0 +1,130 @@ +package cn.bugstack.domain.strategy.service.armory; + +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 javax.annotation.Resource; +import java.math.BigDecimal; +import java.security.SecureRandom; +import java.util.*; +/** + * @ClassName: AbstractStrageyAlgorithm + * @Description: 抽奖策略算法抽象类 + * @Author: zhaoyongfeng + * @Date: 2025/1/23 22:22 + */ +public abstract class AbstractStrategyAlgorithm implements IStrategyArmory, IStrategyDispatch { + @Resource + protected IStrategyRepository repository; + protected final SecureRandom secureRandom = new SecureRandom(); + + @Override + public boolean assembleLotteryStrategyByActivityId(Long activityId){ + Long strategyId = repository.queryStrategyIdByActivityId(activityId); + return assembleLotteryStrategy(strategyId); + } + @Override + public boolean assembleLotteryStrategy(Long strategyId){ + // 1.查询策略配置 + List strategyAwardEntities = repository.queryStrategyAwardList(strategyId); + // 2. 缓存奖品库存【用于decr扣减库存使用】 + for (StrategyAwardEntity strategyAward : strategyAwardEntities) { + Integer awardId = strategyAward.getAwardId(); + Integer awardCountSurplus = strategyAward.getAwardCountSurplus(); + cacheStrategyAwardCount(strategyId,awardId,awardCountSurplus); + } + // 3.1 默认装配配置【全量抽奖概率】 + armoryAlgorithm(String.valueOf(strategyId),strategyAwardEntities); + // 3.2 权重策略配置 - 适用于 rule_weight 权重规则配置【4000:102,103,104,105 5000:102,103,104,105,106,107 6000:102,103,104,105,106,107,108,109】 + StrategyEntity strategyEntity = repository.queryStrategyEntityByStrategyId(strategyId); + String ruleWeight = strategyEntity.getRuleWeight(); + // 为空抛出异常,设定异常 + if(null == ruleWeight) return true; + StrategyRuleEntity strategyRuleEntity = repository.queryStrategyRule(strategyId, ruleWeight); + // 业务异常,策略规则中 rule_weight 权重规则已适用但未配置 + if(null == strategyRuleEntity){ + throw new AppException(ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getCode(), ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getInfo()); + } + Map> ruleWeightValueMap = strategyRuleEntity.getRuleWeightValues(); + for (String key : ruleWeightValueMap.keySet()) { + List ruleWeightValues = ruleWeightValueMap.get(key); + ArrayList strategyAwardEntitiesClone = new ArrayList<>(strategyAwardEntities); + strategyAwardEntitiesClone.removeIf(entity -> !ruleWeightValues.contains(entity.getAwardId())); + armoryAlgorithm(String.valueOf(strategyId).concat(Constants.UNDERLINE).concat(key), strategyAwardEntitiesClone); + } + return true; + } + + /* + * @description: 装配算法 + * @param: key 为策略ID、权重ID、strategyAwardEntities 对应的奖品概率 + * @return: void + * @author zhaoyongfeng + * @date: 2025/1/23 23:02 + */ + protected abstract void armoryAlgorithm(String key, List strategyAwardEntities); + /* + * @description: 缓存奖品库存到Redis + * @param: strategyId 策略ID awardId 奖品ID awardCount 奖品库存 + * @author zhaoyongfeng + * @date: 2025/1/23 23:04 + */ + 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); + } + protected abstract Integer dispatchAlgorithm(String key); + @Override + public Integer getRandomAwardId(Long strategyId) { + return dispatchAlgorithm(String.valueOf(strategyId)); + } + @Override + public Integer getRandomAwardId(Long strategyId, String ruleWeightValue) { + String key = String.valueOf(strategyId).concat(Constants.UNDERLINE).concat(ruleWeightValue); + return getRandomAwardId(key); + } + @Override + public Integer getRandomAwardId(String key) { + return dispatchAlgorithm(key); + } + + @Override + public Boolean subtractionAwardStock(Long strategyId, Integer awardId, Date endDateTime) { + String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_KEY + strategyId + Constants.UNDERLINE + awardId; + return repository.subtractionAwardStock(cacheKey, endDateTime); + } + // 概率最小值 + protected BigDecimal minAwardRate(List strategyAwardEntities){ + return strategyAwardEntities.stream() + .map(StrategyAwardEntity::getAwardRate) + .min(BigDecimal::compareTo) + .orElse(BigDecimal.ZERO); + } + // 概率范围值,百分位、千分位、万分位 + protected double convert(double min) { + if (0 == min) return 1D; + + String minStr = String.valueOf(min); + + // 小数点前 + String beginVale = minStr.substring(0, minStr.indexOf(".")); + int beginLength = 0; + if (Double.parseDouble(beginVale) > 0) { + beginLength = minStr.substring(0, minStr.indexOf(".")).length(); + } + + // 小数点后 + String endValue = minStr.substring(minStr.indexOf(".") + 1); + int endLength = 0; + if (Double.parseDouble(endValue) > 0) { + endLength = minStr.substring(minStr.indexOf(".") + 1).length(); + } + + return Math.pow(10, beginLength + endLength); + } +} diff --git a/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/StrategyArmoryDispatch.java b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/StrategyArmoryDispatch.java index 9aaf28db29b1c2b90b6dca495895fdf7b7f65ab6..daf7a06d9a4af338a17544c0d07bdcf61bb8608c 100644 --- a/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/StrategyArmoryDispatch.java +++ b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/StrategyArmoryDispatch.java @@ -1,207 +1,59 @@ package cn.bugstack.domain.strategy.service.armory; 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 cn.bugstack.domain.strategy.service.armory.algorithm.AbstractAlgorithm; +import cn.bugstack.domain.strategy.service.armory.algorithm.IAlgorithm; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import javax.annotation.Resource; import java.math.BigDecimal; -import java.security.SecureRandom; -import java.util.*; +import java.util.List; +import java.util.Map; /** - * @author Fuzhengwei bugstack.cn @小傅哥 - * @description 策略装配库(兵工厂),负责初始化策略计算 - * @create 2023-12-23 10:02 + * @ClassName: StrategyArmoryDispatch + * @Description: + * @Author: zhaoyongfeng + * @Date: 2025/1/23 23:26 */ @Slf4j -@Service -public class StrategyArmoryDispatch implements IStrategyArmory, IStrategyDispatch { +@Service("strategyArmoryDispatch") +public class StrategyArmoryDispatch extends AbstractStrategyAlgorithm{ + // 抽奖策略算法 + private final Map algorithmMap; + // 抽奖算法阈值,在多少范围内开始选择不同选择 + private final Integer ALGORITHM_THRESHOLD_VALUE = 10000; - @Resource - private IStrategyRepository repository; - - private final SecureRandom secureRandom = new SecureRandom(); - - @Override - public boolean assembleLotteryStrategyByActivityId(Long activityId) { - Long strategyId = repository.queryStrategyIdByActivityId(activityId); - return assembleLotteryStrategy(strategyId); + public StrategyArmoryDispatch(Map algorithmMap) { + this.algorithmMap = algorithmMap; } - @Override - public boolean assembleLotteryStrategy(Long strategyId) { - // 1. 查询策略配置 - List strategyAwardEntities = repository.queryStrategyAwardList(strategyId); - - // 2 缓存奖品库存【用于decr扣减库存使用】 - for (StrategyAwardEntity strategyAward : strategyAwardEntities) { - Integer awardId = strategyAward.getAwardId(); - Integer awardCount = strategyAward.getAwardCountSurplus(); - cacheStrategyAwardCount(strategyId, awardId, awardCount); - } - - // 3.1 默认装配配置【全量抽奖概率】 - assembleLotteryStrategy(String.valueOf(strategyId), strategyAwardEntities); - - // 3.2 权重策略配置 - 适用于 rule_weight 权重规则配置【4000:102,103,104,105 5000:102,103,104,105,106,107 6000:102,103,104,105,106,107,108,109】 - StrategyEntity strategyEntity = repository.queryStrategyEntityByStrategyId(strategyId); - String ruleWeight = strategyEntity.getRuleWeight(); - if (null == ruleWeight) return true; - - StrategyRuleEntity strategyRuleEntity = repository.queryStrategyRule(strategyId, ruleWeight); - // 业务异常,策略规则中 rule_weight 权重规则已适用但未配置 - if (null == strategyRuleEntity) { - throw new AppException(ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getCode(), ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getInfo()); - } - - Map> ruleWeightValueMap = strategyRuleEntity.getRuleWeightValues(); - for (String key : ruleWeightValueMap.keySet()) { - List ruleWeightValues = ruleWeightValueMap.get(key); - ArrayList strategyAwardEntitiesClone = new ArrayList<>(strategyAwardEntities); - strategyAwardEntitiesClone.removeIf(entity -> !ruleWeightValues.contains(entity.getAwardId())); - assembleLotteryStrategy(String.valueOf(strategyId).concat(Constants.UNDERLINE).concat(key), strategyAwardEntitiesClone); - } - - return true; - } - - /** - * 计算公式; - * 1. 找到范围内最小的概率值,比如 0.1、0.02、0.003,需要找到的值是 0.003 - * 2. 基于1找到的最小值,0.003 就可以计算出百分比、千分比的整数值。这里就是1000 - * 3. 那么「概率 * 1000」分别占比100个、20个、3个,总计是123个 - * 4. 后续的抽奖就用123作为随机数的范围值,生成的值100个都是0.1概率的奖品、20个是概率0.02的奖品、最后是3个是0.003的奖品。 - */ - private void assembleLotteryStrategy(String key, List strategyAwardEntities) { - // 1. 获取最小概率值 - BigDecimal minAwardRate = strategyAwardEntities.stream() - .map(StrategyAwardEntity::getAwardRate) - .min(BigDecimal::compareTo) - .orElse(BigDecimal.ZERO); - - // 2. 循环计算找到概率范围值 - BigDecimal rateRange = BigDecimal.valueOf(convert(minAwardRate.doubleValue())); - - // 3. 生成策略奖品概率查找表「这里指需要在list集合中,存放上对应的奖品占位即可,占位越多等于概率越高」 - List strategyAwardSearchRateTables = new ArrayList<>(rateRange.intValue()); - for (StrategyAwardEntity strategyAward : strategyAwardEntities) { - Integer awardId = strategyAward.getAwardId(); - BigDecimal awardRate = strategyAward.getAwardRate(); - // 计算出每个概率值需要存放到查找表的数量,循环填充 - for (int i = 0; i < rateRange.multiply(awardRate).intValue(); i++) { - strategyAwardSearchRateTables.add(awardId); - } - } - - // 4. 对存储的奖品进行乱序操作 - Collections.shuffle(strategyAwardSearchRateTables); - - // 5. 生成出Map集合,key值,对应的就是后续的概率值。通过概率来获得对应的奖品ID - Map shuffleStrategyAwardSearchRateTable = new LinkedHashMap<>(); - for (int i = 0; i < strategyAwardSearchRateTables.size(); i++) { - shuffleStrategyAwardSearchRateTable.put(i, strategyAwardSearchRateTables.get(i)); - } - - // 6. 存放到 Redis - repository.storeStrategyAwardSearchRateTable(key, shuffleStrategyAwardSearchRateTable.size(), shuffleStrategyAwardSearchRateTable); - } - - /** - * 转换计算,只根据小数位来计算。如【0.01返回100】、【0.009返回1000】、【0.0018返回10000】 - */ - private double convertOld1(double min) { - if (0 == min) return 1D; - - double current = min; - double max = 1; - while (current % 1 != 0) { - current = current * 10; - max = max * 10; - } - return max; - } - - private double convertOld2(double min) { - if (min == 0) return 1D; - - String minStr = Double.toString(min); - int decimalPlaces = 0; - - int decimalPointIndex = minStr.indexOf('.'); - if (decimalPointIndex != -1) { - decimalPlaces = minStr.length() - decimalPointIndex - 1; - } - - return Math.pow(10, decimalPlaces); - } - - private double convert(double min) { - if (0 == min) return 1D; - - String minStr = String.valueOf(min); - - // 小数点前 - String beginVale = minStr.substring(0, minStr.indexOf(".")); - int beginLength = 0; - if (Double.parseDouble(beginVale) > 0) { - beginLength = minStr.substring(0, minStr.indexOf(".")).length(); - } - - // 小数点后 - String endValue = minStr.substring(minStr.indexOf(".") + 1); - int endLength = 0; - if (Double.parseDouble(endValue) > 0) { - endLength = minStr.substring(minStr.indexOf(".") + 1).length(); - } - - return Math.pow(10, beginLength + endLength); - } - - /** - * 缓存奖品库存到Redis - * - * @param strategyId 策略ID - * @param awardId 奖品ID - * @param awardCount 奖品库存 - */ - 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); - } @Override - public Integer getRandomAwardId(Long strategyId) { - // 分布式部署下,不一定为当前应用做的策略装配。也就是值不一定会保存到本应用,而是分布式应用,所以需要从 Redis 中获取。 - int rateRange = repository.getRateRange(strategyId); - // 通过生成的随机值,获取概率值奖品查找表的结果 - return repository.getStrategyAwardAssemble(String.valueOf(strategyId), secureRandom.nextInt(rateRange)); - } - - @Override - public Integer getRandomAwardId(Long strategyId, String ruleWeightValue) { - String key = String.valueOf(strategyId).concat(Constants.UNDERLINE).concat(ruleWeightValue); - return getRandomAwardId(key); + protected void armoryAlgorithm(String key, List strategyAwardEntities) { + // 1. 概率最小值 + BigDecimal minAwardRate = minAwardRate(strategyAwardEntities); + // 2. 概率范围值 + double rateRange = convert(minAwardRate.doubleValue()); + // 3. 根据概率值范围选择算法 + if (rateRange <= ALGORITHM_THRESHOLD_VALUE) { + IAlgorithm o1Algorithm = algorithmMap.get(AbstractAlgorithm.Algorithm.O1.getKey()); + o1Algorithm.armoryAlgorithm(key, strategyAwardEntities, new BigDecimal(rateRange)); + repository.cacheStrategyArmoryAlgorithm(key, AbstractAlgorithm.Algorithm.O1.getKey()); + } else { + IAlgorithm oLogNAlgorithm = algorithmMap.get(AbstractAlgorithm.Algorithm.OLogN.getKey()); + oLogNAlgorithm.armoryAlgorithm(key, strategyAwardEntities, new BigDecimal(rateRange)); + repository.cacheStrategyArmoryAlgorithm(key, AbstractAlgorithm.Algorithm.OLogN.getKey()); + } } @Override - public Integer getRandomAwardId(String key) { - // 分布式部署下,不一定为当前应用做的策略装配。也就是值不一定会保存到本应用,而是分布式应用,所以需要从 Redis 中获取。 - int rateRange = repository.getRateRange(key); - // 通过生成的随机值,获取概率值奖品查找表的结果 - return repository.getStrategyAwardAssemble(key, secureRandom.nextInt(rateRange)); + protected Integer dispatchAlgorithm(String key) { + String beanName = repository.queryStrategyArmoryAlgorithmFromCache(key); + if (null == beanName) throw new RuntimeException("key " + key + " beanName is " + beanName); + IAlgorithm algorithm = algorithmMap.get(beanName); + return algorithm.dispatchAlgorithm(key); } - @Override - public Boolean subtractionAwardStock(Long strategyId, Integer awardId, Date endDateTime) { - String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_KEY + strategyId + Constants.UNDERLINE + awardId; - return repository.subtractionAwardStock(cacheKey, endDateTime); - } } diff --git a/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/algorithm/AbstractAlgorithm.java b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/algorithm/AbstractAlgorithm.java new file mode 100644 index 0000000000000000000000000000000000000000..d10267b177122bf90527553183cbdf10317845ce --- /dev/null +++ b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/algorithm/AbstractAlgorithm.java @@ -0,0 +1,31 @@ +package cn.bugstack.domain.strategy.service.armory.algorithm; + +import cn.bugstack.domain.strategy.repository.IStrategyRepository; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.annotation.Resource; +import java.security.SecureRandom; + +/** + * @ClassName: AbstractAlgorithm + * @Description: 抽奖算法抽象类 + * @Author: zhaoyongfeng + * @Date: 2025/1/23 23:32 + */ + +public abstract class AbstractAlgorithm implements IAlgorithm{ + @Resource + protected IStrategyRepository repository; + + protected final SecureRandom secureRandom = new SecureRandom(); + @Getter + @AllArgsConstructor + @NoArgsConstructor + public enum Algorithm { + O1("o1Algorithm"), OLogN("oLogNAlgorithm"); + + private String key; + } +} diff --git a/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/algorithm/IAlgorithm.java b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/algorithm/IAlgorithm.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd92080d4b63f9da4ab199f066e8bf20775ccd5 --- /dev/null +++ b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/algorithm/IAlgorithm.java @@ -0,0 +1,16 @@ +package cn.bugstack.domain.strategy.service.armory.algorithm; + +import cn.bugstack.domain.strategy.model.entity.StrategyAwardEntity; + +import java.math.BigDecimal; +import java.util.List; +/** + * @author + * @description 抽奖算法 + * @create 2024-08-24 13:02 + */ +public interface IAlgorithm { + void armoryAlgorithm(String key, List strategyAwardEntities, BigDecimal rateRange); + + Integer dispatchAlgorithm(String key); +} diff --git a/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/algorithm/impl/O1Algorithm.java b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/algorithm/impl/O1Algorithm.java new file mode 100644 index 0000000000000000000000000000000000000000..d742f318d5498679c85a4ef3800832a8af3c99f9 --- /dev/null +++ b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/algorithm/impl/O1Algorithm.java @@ -0,0 +1,55 @@ +package cn.bugstack.domain.strategy.service.armory.algorithm.impl; + +import cn.bugstack.domain.strategy.model.entity.StrategyAwardEntity; +import cn.bugstack.domain.strategy.service.armory.algorithm.AbstractAlgorithm; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.*; + +/** + * @ClassName: O1Algorithm + * @Description: o(1) 时间复杂度算法,将抽奖概率分配到 Redis Map 结构中,1个概率对应的奖品就是一个Key,抽奖时直接通过 get 获取。 + * @Author: zhaoyongfeng + * @Date: 2025/1/23 23:34 + */ +@Slf4j +@Component("o1Algorithm") +public class O1Algorithm extends AbstractAlgorithm { + @Override + public void armoryAlgorithm(String key, List strategyAwardEntities, BigDecimal rateRange) { + log.info("抽奖算法 O(1) 装配 key:{}", key); + // 1. 生成策略奖品概率查找表「这里指需要在list集合中,存放上对应的奖品占位即可,占位越多等于概率越高」 + List strategyAwardSearchRateTables = new ArrayList<>(rateRange.intValue()); + for (StrategyAwardEntity strategyAward : strategyAwardEntities) { + Integer awardId = strategyAward.getAwardId(); + BigDecimal awardRate = strategyAward.getAwardRate(); + // 计算出每个概率值需要存放到查找表的数量,循环填充 + for (int i = 0; i < rateRange.multiply(awardRate).intValue(); i++) { + strategyAwardSearchRateTables.add(awardId); + } + } + + // 2. 对存储的奖品进行乱序操作 + Collections.shuffle(strategyAwardSearchRateTables); + + // 3. 生成出Map集合,key值,对应的就是后续的概率值。通过概率来获得对应的奖品ID + Map shuffleStrategyAwardSearchRateTable = new LinkedHashMap<>(); + for (int i = 0; i < strategyAwardSearchRateTables.size(); i++) { + shuffleStrategyAwardSearchRateTable.put(i, strategyAwardSearchRateTables.get(i)); + } + + // 4. 存放到 Redis + repository.storeStrategyAwardSearchRateTable(key, shuffleStrategyAwardSearchRateTable.size(), shuffleStrategyAwardSearchRateTable); + } + + @Override + public Integer dispatchAlgorithm(String key) { + log.info("抽奖算法 O(1) 抽奖计算 key:{}", key); + // 分布式部署下,不一定为当前应用做的策略装配。也就是值不一定会保存到本应用,而是分布式应用,所以需要从 Redis 中获取。 + int rateRange = repository.getRateRange(key); + // 通过生成的随机值,获取概率值奖品查找表的结果 + return repository.getStrategyAwardAssemble(key, secureRandom.nextInt(rateRange)); + } +} diff --git a/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/algorithm/impl/OLogNAlgorithm.java b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/algorithm/impl/OLogNAlgorithm.java new file mode 100644 index 0000000000000000000000000000000000000000..da3ffde1b8b534c5e09953faae93cf2b7287cca4 --- /dev/null +++ b/bigmarket-domain/src/main/java/cn/bugstack/domain/strategy/service/armory/algorithm/impl/OLogNAlgorithm.java @@ -0,0 +1,158 @@ +package cn.bugstack.domain.strategy.service.armory.algorithm.impl; + +import cn.bugstack.domain.strategy.model.entity.StrategyAwardEntity; +import cn.bugstack.domain.strategy.service.armory.algorithm.AbstractAlgorithm; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +/** + * @ClassName: OLogNAlgorithm + * @Description: 循环、二分、多线程,查找抽奖算法 + * @Author: zhaoyongfeng + * @Date: 2025/1/23 23:37 + */ +@Slf4j +@Component("oLogNAlgorithm") +public class OLogNAlgorithm extends AbstractAlgorithm { + + @Resource + private ThreadPoolExecutor threadPoolExecutor; + + /** + * 预热活动概率值 + * 如概率值为;3、4、2、9,存储为 [1~3]、[4~7]、[8~9]、[10~18],抽奖时,for循环匹配。 + * + * @param key 为策略ID、权重ID + * @param strategyAwardEntities 对应的奖品概率 + */ + @Override + public void armoryAlgorithm(String key, List strategyAwardEntities, BigDecimal rateRange) { + log.info("抽奖算法 OLog(n) 装配 key:{}", key); + int from = 1; + int to = 0; + + Map, Integer> table = new HashMap<>(); + for (StrategyAwardEntity strategyAward : strategyAwardEntities) { + Integer awardId = strategyAward.getAwardId(); + BigDecimal awardRate = strategyAward.getAwardRate(); + to += rateRange.multiply(awardRate).intValue(); + + Map ft = new HashMap<>(); + ft.put(from, to); + table.put(ft, awardId); + from = to + 1; + } + + repository.storeStrategyAwardSearchRateTable(key, to, table); + } + + @Override + public Integer dispatchAlgorithm(String key) { + int rateRange = repository.getRateRange(key); + Map, Integer> table = repository.getMap(key); + // 小于等于8 for循环、小于等于16 二分查找、更多检索走多线程 + if (table.size() <= 8) { + log.info("抽奖算法 OLog(n) 抽奖计算(循环) key:{}", key); + return forSearch(secureRandom.nextInt(rateRange), table); + } else if (table.size() <= 16) { + log.info("抽奖算法 OLog(n) 抽奖计算(二分) key:{}", key); + return binarySearch(secureRandom.nextInt(rateRange), table); + } else { + log.info("抽奖算法 OLog(n) 抽奖计算(多线程) key:{}", key); + return threadSearch(secureRandom.nextInt(rateRange), table); + } + } + + private Integer forSearch(int rateKey, Map, Integer> table) { + Integer awardId = null; + for (Map.Entry, Integer> entry : table.entrySet()) { + Map rangeMap = entry.getKey(); + + for (Map.Entry range : rangeMap.entrySet()) { + int start = Integer.parseInt(range.getKey()); + int end = range.getValue(); + + if (rateKey >= start && rateKey <= end) { + awardId = entry.getValue(); + break; + } + } + + if (awardId != null) { + break; + } + } + + return awardId; + } + + private Integer binarySearch(int rateKey, Map, Integer> table) { + List, Integer>> entries = new ArrayList<>(table.entrySet()); + entries.sort(Comparator.comparingInt(e -> Integer.parseInt(e.getKey().keySet().iterator().next()))); + + int left = 0; + int right = entries.size() - 1; + + while (left <= right) { + int mid = left + (right - left) / 2; + Map.Entry, Integer> entry = entries.get(mid); + Map rangeMap = entry.getKey(); + Map.Entry range = rangeMap.entrySet().iterator().next(); + + int start = Integer.parseInt(range.getKey()); + int end = range.getValue(); + + if (rateKey < start) { + right = mid - 1; + } else if (rateKey > end) { + left = mid + 1; + } else { + return entry.getValue(); + } + } + + return null; + } + + private Integer threadSearch(int rateKey, Map, Integer> table) { + List, Integer>>> futures = table.entrySet().stream() + .map(entry -> CompletableFuture.supplyAsync(() -> { + Map rangeMap = entry.getKey(); + for (Map.Entry rangeEntry : rangeMap.entrySet()) { + int start = Integer.parseInt(rangeEntry.getKey()); + int end = rangeEntry.getValue(); + if (rateKey >= start && rateKey <= end) { + return entry; + } + } + return null; + }, threadPoolExecutor)) + .collect(Collectors.toList()); + + CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + + try { + // 等待所有异步任务完成,同时返回第一个匹配的结果 + allFutures.join(); + for (CompletableFuture, Integer>> future : futures) { + Map.Entry, Integer> result = future.getNow(null); + if (result != null) { + return result.getValue(); + } + } + } catch (CompletionException e) { + e.printStackTrace(); + } + + return null; + } + +} \ No newline at end of file diff --git a/bigmarket-infrastructure/src/main/java/cn/bugstack/infrastructure/persistent/dao/IStrategyAwardDao.java b/bigmarket-infrastructure/src/main/java/cn/bugstack/infrastructure/persistent/dao/IStrategyAwardDao.java index eb054a0acdc6f2c3c762ff3345d2ebcae5bf0b4a..d66e621f8e3f5c941c54aa480b9345e4a2c7beaa 100644 --- a/bigmarket-infrastructure/src/main/java/cn/bugstack/infrastructure/persistent/dao/IStrategyAwardDao.java +++ b/bigmarket-infrastructure/src/main/java/cn/bugstack/infrastructure/persistent/dao/IStrategyAwardDao.java @@ -22,5 +22,5 @@ public interface IStrategyAwardDao { void updateStrategyAwardStock(StrategyAward strategyAward); StrategyAward queryStrategyAward(StrategyAward strategyAwardReq); - + List queryOpenActivityStrategyAwardList(); } diff --git a/bigmarket-infrastructure/src/main/java/cn/bugstack/infrastructure/persistent/repository/StrategyRepository.java b/bigmarket-infrastructure/src/main/java/cn/bugstack/infrastructure/persistent/repository/StrategyRepository.java index cb85187ed98d5a0ec9857477b98a677933d9e430..5c3aa3aef2594c0f3e844ab3e0a2e359afc71d7e 100644 --- a/bigmarket-infrastructure/src/main/java/cn/bugstack/infrastructure/persistent/repository/StrategyRepository.java +++ b/bigmarket-infrastructure/src/main/java/cn/bugstack/infrastructure/persistent/repository/StrategyRepository.java @@ -87,14 +87,22 @@ public class StrategyRepository implements IStrategyRepository { * 简单来说,getMap 方法返回的 RMap 对象是懒加载的,只有在你实际进行操作时,Redis 数据库中的数据结构才会被创建或修改。 */ @Override - public void storeStrategyAwardSearchRateTable(String key, Integer rateRange, Map strategyAwardSearchRateTable) { + public void storeStrategyAwardSearchRateTable(String key, Integer rateRange, Map strategyAwardSearchRateTable) { // 1. 存储抽奖策略范围值,如10000,用于生成1000以内的随机数 redisService.setValue(Constants.RedisKey.STRATEGY_RATE_RANGE_KEY + key, rateRange); - // 2. 存储概率查找表 - Map cacheRateTable = redisService.getMap(Constants.RedisKey.STRATEGY_RATE_TABLE_KEY + key); + // 2. 存储概率查找表 - 存在则删除重新装配 + String tableCacheKey = Constants.RedisKey.STRATEGY_RATE_TABLE_KEY + key; + if (redisService.isExists(tableCacheKey)) { + redisService.remove(tableCacheKey); + } + Map cacheRateTable = redisService.getMap(Constants.RedisKey.STRATEGY_RATE_TABLE_KEY + key); cacheRateTable.putAll(strategyAwardSearchRateTable); } + @Override + public Map getMap(String key) { + return redisService.getMap(Constants.RedisKey.STRATEGY_RATE_TABLE_KEY + key); + } @Override public Integer getStrategyAwardAssemble(String key, Integer rateKey) { return redisService.getFromMap(Constants.RedisKey.STRATEGY_RATE_TABLE_KEY + key, rateKey); @@ -404,5 +412,31 @@ public class StrategyRepository implements IStrategyRepository { return ruleWeightVOS; } + @Override + public List queryOpenActivityStrategyAwardList() { + List strategyAwards = strategyAwardDao.queryOpenActivityStrategyAwardList(); + if (null == strategyAwards || strategyAwards.isEmpty()) return null; + + List strategyAwardStockKeyVOS = new ArrayList<>(); + for (StrategyAward strategyAward : strategyAwards) { + StrategyAwardStockKeyVO strategyAwardStockKeyVO = StrategyAwardStockKeyVO.builder() + .strategyId(strategyAward.getStrategyId()) + .awardId(strategyAward.getAwardId()) + .build(); + strategyAwardStockKeyVOS.add(strategyAwardStockKeyVO); + } + return strategyAwardStockKeyVOS; + } + @Override + public void cacheStrategyArmoryAlgorithm(String key, String beanName) { + String cacheKey = Constants.RedisKey.STRATEGY_ARMORY_ALGORITHM_KEY + key; + redisService.setValue(cacheKey, beanName); + } + @Override + public String queryStrategyArmoryAlgorithmFromCache(String key) { + String cacheKey = Constants.RedisKey.STRATEGY_ARMORY_ALGORITHM_KEY + key; + if (!redisService.isExists(cacheKey)) return null; + return redisService.getValue(cacheKey); + } } diff --git a/bigmarket-types/src/main/java/cn/bugstack/types/common/Constants.java b/bigmarket-types/src/main/java/cn/bugstack/types/common/Constants.java index 06ab2d6ce42dfc2b8f51644c0e2360706464f9e0..2b97400f5018bb545f7b4419646afb30c10b5344 100644 --- a/bigmarket-types/src/main/java/cn/bugstack/types/common/Constants.java +++ b/bigmarket-types/src/main/java/cn/bugstack/types/common/Constants.java @@ -29,6 +29,7 @@ public class Constants { public static String ACTIVITY_ACCOUNT_LOCK = "activity_account_lock_"; public static String ACTIVITY_ACCOUNT_UPDATE_LOCK = "activity_account_update_lock_"; public static String USER_CREDIT_ACCOUNT_LOCK = "user_credit_account_lock_"; + public static String STRATEGY_ARMORY_ALGORITHM_KEY = "strategy_armory_algorithm_key_"; }