DDD分层架构设计
- 领域层 Domain
- Entity-奖品策略的实体:ID/库存/概率/...
- Service-抽奖策略核心业务逻辑 StrategyArmory
- 仓储接口
- 基础设施 Infrastructure
- po-数据库字段映射 StrategyAward
- 仓储实现 StrategyRepository
- 缓存策略
- “抽”的环节:生成随机数-查概率表
- po和领域实体转换
缓存失效从数据库获取--->mapper动手脚
Dao层是唯一与数据库交互的
strategyAwardDao.queryStrategyAwardListByStrategyId(strategyId);
应用层调用链路:
Service层 -> Repository接口层 -> DAO接口层 -> Mapper.xml -> 数据库
- Mapper.xml 文件定义了 SQL 语句和数据库表字段与 PO(持久化对象)之间的映射关系
- DAO接口声明了数据库操作的方法
- MyBatis 框架会自动将 Mapper.xml 和 DAO 接口进行绑定
所以如果要:
- 修改数据库查询逻辑 -> 修改 Mapper.xml
- 添加新的数据库操作 -> 在 DAO 接口添加方法,同时在 Mapper.xml 添加对应的 SQL
- 更改字段映射关系 -> 修改 Mapper.xml 中的 resultMap
mapper.strategy_award_mapper.xml
<resultMap id="dataMap" type="org.example.infrastructure.persistent.po.StrategyAward">
<id column="id" property="id"/>
<result column="strategy_id" property="strategyId"/>
<result column="award_id" property="awardId"/>
<result column="award_title" property="awardTitle"/>
<result column="award_subtitle" property="awardSubtitle"/>
<result column="award_count" property="awardCount"/>
<result column="award_count_surplus" property="awardCountSurplus"/>
<result column="award_rate" property="awardRate"/>
<result column="rule_models" property="ruleModels"/>
<result column="sort" property="sort"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<select id="queryStrategyAwardList" resultMap="dataMap">
SELECT strategy_id
FROM strategy_award
LIMIT 10
</select>
<!-- 与 IStrategyAwardDao 接口的实现相对应,确保在缓存失效的情况下可以从数据库获取完整的数据 -->
<select id="queryStrategyAwardListByStrategyId" resultMap="dataMap">
SELECT strategy_id, award_id, award_count, award_count_surplus, award_rate
FROM strategy_award
WHERE strategy_id = #{strategyId}
</select>
PO(Persistent Object,持久化对象)到领域实体的转换过程
DDD(领域驱动设计)中常见的操作
- 将数据库来的StrategyAward转换为对象列表
- 新建列表
- 创建对象
- 把数据库字段-映射-实体对象
- 注意类型统一:strategy.modle.entity.StrategyAwardEntity与Po层对象与phpAdmain 数据库字段
- 把对象填入列表
- get字段() 是Po层 @Data为字段对象提供的getter&setter
- 把数据库字段-映射-实体对象
字段(strategyAward.get字段())
- 将对象列表写入缓存
StrategyRepository
@Override
public List<StrategyAwardEntity> queryStrategyAwardList(Long strategyId) {
//根据ID构建redis key
String cacheKey = Constants.RedisKey.STRATEGY_AWARD_KEY + strategyId;
//从redis中获取数据
List<StrategyAwardEntity> strategyAwardEntities = redisService.getValue(cacheKey);
//缓存中有数据-不为null且不为空
if(null != strategyAwardEntities && !strategyAwardEntities.isEmpty()){
return strategyAwardEntities;//直接返回缓存
}
//缓存中没有数据-从数据库中获取,根据ID查
List<StrategyAward> strategyAwards = strategyAwardDao.queryStrategyAwardListByStrategyId(strategyId);
//将数据库来的StrategyAward转换为对象列表
//1.新建列表
strategyAwardEntities = new ArrayList<>(strategyAwards.size());
//2.遍历数据库来的数据,转换为对象
for(StrategyAward strategyAward: strategyAwards){
//3.建造者模式-创建对象
StrategyAwardEntity strategyAwardEntity = StrategyAwardEntity.builder()
//4.数据库对象属性-映射-实体对象
.strategyId(strategyAward.getStrategyId())
.awardId(strategyAward.getAwardId())
.awardCount(strategyAward.getAwardCount())
.awardCountSurplus(strategyAward.getAwardCountSurplus())
.awardRate(strategyAward.getAwardRate())
.build();
//5.将对象添加到列表
strategyAwardEntities.add(strategyAwardEntity);
}
//5.将对象列表写入缓存
redisService.setValue(cacheKey, strategyAwardEntities);
return strategyAwardEntities;
}
抽奖算法
两种计算方式,本质都是区间和奖品映射。第一种每次都要进行区间判断;第二种类似哈希表,把区间变成索引。
存储
第二种用本地内存guava或redis存储-放到Map
- 本地内存
- 单机环境
- 多台机器分布式部署,若要数据同步需要引入配置中心,考虑启动前后数据加载处理
- eg. 低延迟的实时抽奖
- Redis存储
- 高并发共享数据
- 需要网络通信来获取数据,可能会引入延迟
- 需要对数据过期、持久化等进行合理配置
功能实现
- 需要扩展:Redis配置、库表操作、仓储层实现数据调用
- 策略装配:
- 查询策略配置
- 存放到 Redis
- 生成出Map集合,key值,对应的就是后续的概率值。通过概率来获得对应的奖品ID
- 对存储的奖品进行乱序操作。避免顺序生成的随机数前面是固定的奖品。
- 生成策略奖品概率查找表「这里指需要在list集合中,存放上对应的奖品占位即可,占位越多等于概率越高」
- 用 1 % 0.0001 获得概率范围,百分位、千分位、万分位
- 获取概率值总和
- 获取最小概率值
Q1:为何要获取概率总和?
为了灵活控制抽奖,不需要为了数学上的百分百从而导致奖品分布的难堪。比如总概率/最小概率:1.01/0.0001=100100,可以拆分成10000份随机小奖品和100份一/二/三等奖。如果让奖品平分1就会出现小数点,这个小数点会让分配不均,没有道理可言。数学模型应该为业务服务,怎么方便怎么来。 多一点或少一点的总概率并不会影响最终的分配效果 。
Q2:为什么要计算总概率除以最小概率,不选择保留小数向上取整,确定奖品份额?
奖品份额不能是小数,向上取证会有小概率超出索引,向下取整奖品可能直接=0,不合适。
convert() 把最小概率的最小单位扩大为份额范围,使用总概率计算范围而不是1,就不会导致超出或空缺问题。随之而来的,如果概率非常小,份额范围就会非常大,生成超大映射表,机器负载过高。因此需要调正概率值。
- 向上取整的必要性
假设概率是 0.001 (0.1%),rateRange awardRate = 100 *0.001 = 0.1。如果不向上取整,intValue() 会得到 0,导致这个奖品完全没有机会被抽中,使用 CEILING 后会得到 1,保证即使是小概率奖品也至少有一个位置
实际例子:
奖品A:概率0.367 -> 37个位置
奖品B:概率0.003 -> 1个位置 (如果不向上取整会变成0,导致永远抽不到)
奖品C:概率0.63 -> 63个位置
总计:101个位置
这样处理可以确保:不会因为小数截断而丢失概率,保证小概率奖品也有被抽中的机会
domain.service.StrategyArmory.java
@Slf4j
@Service
public class SrategyArmory implements IStrategyArmory {
@Resource
private IStrategyRepository repository;
@Override
public void 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<Integer> strategyAwardSearchRateTables = new ArrayList<>(rateRange.intValue());
for(StrategyAwardEntity strategyAward: strategyAwardEntities){
Integer awardId = strategyAward.getAwardId(); //奖品ID
BigDecimal awardRate = strategyAward.getAwardRate(); //奖品概率
//每个概率值的占位份额,循环填充
for(int i = 0; i < rateRange.multiply(awardRate) //100*0.3
.setScale(0, RoundingMode.CEILING).intValue(); i++){ //最小位数
strategyAwardSearchRateTables.add(awardId);
}
}
//6.乱序排列奖品
Collections.shuffle(strategyAwardSearchRateTables);
//7.生成Map-奖品ID与占位值的映射关系
HashMap<Integer, Integer> shuffleStrategyAwardSearchRateTable = new HashMap<>();
for(int i = 0; i < strategyAwardSearchRateTables.size(); i++){
//key-索引,value-ID
shuffleStrategyAwardSearchRateTable.put(i,strategyAwardSearchRateTables.get(i));
}
//8.存放到Redis-ID、表大小、表内容
repository.storeStrategyAwardSearchRateTables(strategyId,
BigDecimal.valueOf(shuffleStrategyAwardSearchRateTable.size()),
shuffleStrategyAwardSearchRateTable);
}
//9. 获取随机奖品ID
@Override
public Integer getRandomAwardId(Long strategyId) {
int rateRange = repository.getRateTange(strategyId); //策略范围
return repository.getRandomAwardId(strategyId, new SecureRandom().nextInt(rateRange)); //根据随机数获取奖品ID
}
}