抢红包设计

抢红包大致可以分为2步:1 发红包 ;2 抢红包

发红包流程
抢红包设计_第1张图片
为了突出红包设计主题,以下设计会忽略支付流程、24H过期退款剩余金额、用户领取红包余额到账等业务,则简化后的相关表设计如下:

CREATE TABLE `red_record` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL COMMENT '用户id',
  `total` int(11) NOT NULL COMMENT '人数',
  `amount` int(11) NOT NULL COMMENT '总金额(单位为分)',
  `status` tinyint(4) DEFAULT '1' COMMENT '状态:1 已发送,2 已抢完',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8 COMMENT='发红包记录';
// 忽略 支付流程 和 24H过期退还剩余金额

CREATE TABLE `red_rob_record` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULLL COMMENT '用户ID',
  `red_record_id` int(11)  COMMENT '红包ID',
  `amount` int(11) NOT NULL COMMENT '红包金额(单位为分)',
  `rob_time` datetime DEFAULT NULL COMMENT '时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=118 DEFAULT CHARSET=utf8 COMMENT='抢红包记录';

红包算法选取二倍均值算法,保证无论用户是先抢还是后抢都保证抢到金额的概率一致。
二倍均值算法逻辑实现图:
抢红包设计_第2张图片
算法代码实现:

/**
     * 二倍均值法计算红包金额
     * @param totalAmount 总金额 (单位分)
     * @param peopleNum 总人数
     * @return
     */
    public static List divideRedPackage(Integer totalAmount,Integer peopleNum) {
        List list = new ArrayList<>();
        //金额和人数都必须大于0
        if (totalAmount > 0 && peopleNum > 0) {
            //重置入参
            Integer restAmount = totalAmount;
            Integer restNum = peopleNum;
            //计算红包
            while (restNum - 1 > 0) {
                //计算红包金额  高效随机数 随机范围 左闭右开 [1,restAmount / restNum * 2)
                //随机数使用高效的ThreadLocalRandom
                int amount = ThreadLocalRandom.current().nextInt(restAmount / restNum * 2 - 1) + 1;
                //金额减少 人数减一
                restAmount -= amount;
                restNum--;
                list.add(amount);
            }
            //最后一个人的红包
            list.add(restAmount);
        }
        return list;
    }

发红包代码实现:

/**
     * 发红包
     * @param totalAmount 总金额 (单位分)
     * @param peopleNum 总人数
     * @param userId 用户id
     * @return
     */
    public void sendRedPackage(Integer totalAmount,Integer peopleNum,Long userId) {
      	//金额和人数必须要大于0

      	//持久化发红包信息
       	RedRecordDO redRecordDO = new RedRecordDO();
			 	redRecordDO.setUserId(userId);
			 	redRecordDO.setAmount(totalAmount);
      	redRecordDO.setTotal(peopleNum);
      	//忽略支付流程 默认已支付
      	redRecordDO.setStatus(1);
      	redRecordService.save(redRecordDO);
      	//计算发红包明细金额
      	List list = RedPackageUtil.divideRedPackage(totalAmount,peopleNum);
      	//生成redis的key 
				String key = "m:r:"+redId;
      	//存储到redis中
				redisService.getListOps().rightPushAll(key,list);
    }

抢红包流程
抢红包设计_第3张图片抢红包直接使用redis作为数据源码,利用redis高吞吐量的特性,在发红包阶段先用红包算法将其拆分,存储到redis中的list类型中。
主要利用redis的list数据类型的lpop原子操作(移除并获取列表第一个元素)
可以在redis客户端上简单看一下 list的lpop操作:
抢红包设计_第4张图片
抢红包设计_第5张图片

具体代码实现如下:

    /**
     * 抢红包
     * @param redId 红包id
     * @param userId 领取红包的用户id
     * @return null 表示红包已抢完
     */
    public Integer robRedPackage(Long redId,Long userId){
      	//redis key ,key建议缩写 m表示系统缩写 r 表示红包缩写
      	String key = "m:r:"+redId;
        //先判断key是否存在
        if(redisService.exists(key)){
            //直接弹出list中的元素 lpop是原子性 ,且弹出list所有元素后会删除这个key
            Integer amount = (Integer)redisService.getListOps().leftPop(key);
            //元素不为空,exists不是原子性指令,则 amount可能为null 
            if(Objects.nonNull(amount)){
                //元素不为空则表示抢红包成功,异步更新红包流水
                redRobRecordService.robRecord(redId,userId,amount);
            }
            return amount;
        }
        //直接返回红包已被抢光
        return null;
    }

你可能感兴趣的:(电商,营销,红包)