抽奖策略装配
2024年11月16日大约 4 分钟
抽奖策略装配
功能概述
抽奖算法目前主要有两种实现方案:
- 空间换时间
- 提前计算好各个奖品的概率分布区间
- 使用本地内存(Guava)或Redis存储概率分布数据
- 抽奖时通过随机数在概率空间内O(1)定位
- 优点是性能高,缺点是占用内存空间
- 时间换空间
- 抽奖时动态生成随机数
- 通过for循环遍历概率区间匹配奖品
- 适用于概率分布数据量大的场景
具体使用哪种方案需要根据业务场景、性能要求、成本等因素综合考虑。这里我采用空间换时间的方案,提前计算好各个奖品的概率分布区间,使用Redis存储概率分布数据。

具体实现
概率策略装配
@Slf4j
@Service
public class StrategyArmory implements IStrategyArmory {
@Resource
private IStrategyRepository repository;
@Override
public boolean assembleLotteryStrategy(Long strategyId) {
// 1. 查询策略配置
List<StrategyAwardEntity> strategyAwardEntities = repository.queryStrategyAwardList(strategyId);
// 2. 获取最小概率值
BigDecimal minAwardRate = strategyAwardEntities.stream()
.map(StrategyAwardEntity::getAwardRate)
.min(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO);
// 3. 获取概率值总和
BigDecimal totalAwardRate = strategyAwardEntities.stream()
.map(StrategyAwardEntity::getAwardRate)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 4. 用 1 % 0.0001 获得概率范围,百分位、千分位、万分位
BigDecimal rateRange = totalAwardRate.divide(minAwardRate, 0, RoundingMode.CEILING);
// 5. 生成策略奖品概率查找表「这里指需要在list集合中,存放上对应的奖品占位即可,占位越多等于概率越高」
List<Integer> 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).setScale(0, RoundingMode.CEILING).intValue(); i++) {
strategyAwardSearchRateTables.add(awardId);
}
}
// 6. 对存储的奖品进行乱序操作
Collections.shuffle(strategyAwardSearchRateTables);
// 7. 生成出Map集合,key值,对应的就是后续的概率值。通过概率来获得对应的奖品ID
Map<Integer, Integer> shuffleStrategyAwardSearchRateTable = new LinkedHashMap<>();
for (int i = 0; i < strategyAwardSearchRateTables.size(); i++) {
shuffleStrategyAwardSearchRateTable.put(i, strategyAwardSearchRateTables.get(i));
}
// 8. 存放到 Redis
repository.storeStrategyAwardSearchRateTable(strategyId, shuffleStrategyAwardSearchRateTable.size(), shuffleStrategyAwardSearchRateTable);
return true;
}
@Override
public Integer getRandomAwardId(Long strategyId) {
// 分布式部署下,不一定为当前应用做的策略装配。也就是值不一定会保存到本应用,而是分布式应用,所以需要从 Redis 中获取。
int rateRange = repository.getRateRange(strategyId);
// 通过生成的随机值,获取概率值奖品查找表的结果
return repository.getStrategyAwardAssemble(strategyId, new SecureRandom().nextInt(rateRange));
}
}为什么要计算一遍概率和因为我在抽奖策略中,奖品概率总和不强制要求为100%,主要考虑以下场景:
大于100%场景
- 如总概率1.01/最小概率0.0001=10100随机数范围
- 典型场景:10000份随机积分(90%) + 100份一等奖(5%)/二等奖(3%)/三等奖(3%)
小于100%场景
- 如总概率0.5表示整体中奖率50%
- 未中奖概率为1-总中奖概率
系统会根据总概率/最小概率计算随机范围,按权重分配实际概率,保证数据健壮性。 这种设计使得抽奖策略配置更灵活,能够适应不同的营销场景需求。
权重策略装配
@Override
public boolean assembleLotteryStrategy(Long strategyId) {
// 1. 查询策略配置
List<StrategyAwardEntity> strategyAwardEntities = repository.queryStrategyAwardList(strategyId);
assembleLotteryStrategy(String.valueOf(strategyId), strategyAwardEntities);
// 2. 权重策略配置 - 适用于 rule_weight 权重规则配置
StrategyEntity strategyEntity = repository.queryStrategyEntityByStrategyId(strategyId);
String ruleWeight = strategyEntity.getRuleWeight();
if (null == ruleWeight) return true;
StrategyRuleEntity strategyRuleEntity = repository.queryStrategyRule(strategyId, ruleWeight);
if (null == strategyRuleEntity) {
throw new AppException(ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getCode(), ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getInfo());
}
Map<String, List<Integer>> ruleWeightValueMap = strategyRuleEntity.getRuleWeightValues();
Set<String> keys = ruleWeightValueMap.keySet();
for (String key : keys) {
List<Integer> ruleWeightValues = ruleWeightValueMap.get(key);
ArrayList<StrategyAwardEntity> strategyAwardEntitiesClone = new ArrayList<>(strategyAwardEntities);
strategyAwardEntitiesClone.removeIf(entity -> !ruleWeightValues.contains(entity.getAwardId()));
assembleLotteryStrategy(String.valueOf(strategyId).concat("_").concat(key), strategyAwardEntitiesClone);
}
return true;
}
/**
* 获取权重值
* 数据案例;4000:102,103,104,105 5000:102,103,104,105,106,107 6000:102,103,104,105,106,107,108,109
*/
public Map<String, List<Integer>> getRuleWeightValues() {
if (!"rule_weight".equals(ruleModel)) return null;
String[] ruleValueGroups = ruleValue.split(Constants.SPACE);
Map<String, List<Integer>> resultMap = new HashMap<>();
for (String ruleValueGroup : ruleValueGroups) {
// 检查输入是否为空
if (ruleValueGroup == null || ruleValueGroup.isEmpty()) {
return resultMap;
}
// 分割字符串以获取键和值
String[] parts = ruleValueGroup.split(Constants.COLON);
if (parts.length != 2) {
throw new IllegalArgumentException("rule_weight rule_rule invalid input format" + ruleValueGroup);
}
// 解析值
String[] valueStrings = parts[1].split(Constants.SPLIT);
List<Integer> values = new ArrayList<>();
for (String valueString : valueStrings) {
values.add(Integer.parseInt(valueString));
}
// 将键和值放入Map中
resultMap.put(ruleValueGroup, values);
}
return resultMap;
}后期拓展在户抽奖N积分后,可中奖范围的设定。也就是说用户总共消耗了6000积分抽奖了,那么接下来的抽奖就会有圈定到固定的奖品范围,不会让用户再抽到过低价值的奖品。所以我在设计系统实现的时候,处理不同策略规则 权重的概率装配。
如果这条策略规则有配了权重,那么就会一并将权重规则的奖品概率装配到Redis中。
