大营销03策略概率装配处理 | 领域层+基础层设计

DDD分层架构设计

  1. 领域层 Domain
    1. Entity-奖品策略的实体:ID/库存/概率/...
    2. Service-抽奖策略核心业务逻辑 StrategyArmory
    3. 仓储接口
  2. 基础设施 Infrastructure
    1. po-数据库字段映射 StrategyAward
    2. 仓储实现  StrategyRepository
      1. 缓存策略
      2. “抽”的环节:生成随机数-查概率表
      3. 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存储
    • 高并发共享数据
    • 需要网络通信来获取数据,可能会引入延迟
    • 需要对数据过期、持久化等进行合理配置

功能实现

  1. 需要扩展:Redis配置、库表操作、仓储层实现数据调用
  2. 策略装配:
  • 查询策略配置
  • 存放到 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
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值