Redis拾遗(九)——一个简单的抢红包实例

本篇博客打算在Redis中间件的基础上,引入一个发抢红包的流程,借助一个简单的实例,熟悉Redis的一些使用。

简单流程说明

主要分为发红包和抢红包流程,针对红包的操作记录,我们需要定义三张数据表,分别如下

红包详情表

-- 红包的详情记录,随机分成的每个小金额的红包都会在这个表格中记录
CREATE TABLE `red_detail` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `record_id` int(11) NOT NULL COMMENT '红包记录id',
  `amount` decimal(8,2) DEFAULT NULL COMMENT '每个小红包的金额(单位为分)',
  `is_active` tinyint(4) DEFAULT '1',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=193 DEFAULT CHARSET=utf8 COMMENT='红包明细金额';

发送红包记录表

-- 发送红包的记录表,每个发送红包的个数和金额都会在这个表中记录
CREATE TABLE `red_record` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '用户id',
  `red_packet` varchar(255) CHARACTER SET utf8mb4 NOT NULL COMMENT '红包全局唯一标识串',
  `total` int(11) NOT NULL COMMENT '人数',
  `amount` decimal(10,2) DEFAULT NULL COMMENT '总金额(单位为分)',
  `is_active` tinyint(4) DEFAULT '1',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8 COMMENT='发红包记录';

抢红包记录表

-- 抢红包的记录表,什么客户抢了那一个红包,都会在这张表中记录
CREATE TABLE `red_rob_record` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL COMMENT '用户账号',
  `red_packet` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '红包标识串',
  `amount` decimal(8,2) DEFAULT NULL COMMENT '红包金额(单位为分)',
  `rob_time` datetime DEFAULT NULL COMMENT '时间',
  `is_active` tinyint(4) DEFAULT '1',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=178 DEFAULT CHARSET=utf8 COMMENT='抢红包记录';

发红包流程

用户在输入红包个数和金额之后,后台自动生成红包唯一标识串,然后自动分派好每个红包的金额,异步记录每个红包金额记录。

整体流程如下所示:

Redis拾遗(九)——一个简单的抢红包实例_第1张图片

其实流程并不复杂,客户在页面上输入红包个数和金额,后续的矩形部分才是我们需要重点关注的,需要按照指定个数生成随机金额的红包。然后异步记录红包的发送记录到数据库和缓存。

代码如下:

@Transactional(rollbackFor = Exception.class)
public String handOut(RedPacketDto redPacketDto) {
     
    if (redPacketDto.getCount() > 0 && redPacketDto.getAmount() > 0) {
     
        //生成全局的红包id
        String redKey = String.format(RedPacketKey, redPacketDto.getUserId(), SNOWFLAKE.nextIdStr());
        //生成红包的随机金额
        List<Integer> redAmountList = RedPacketUtil.divideRedPackage(redPacketDto.getCount(), redPacketDto.getAmount());
        recordRedPacket(redPacketDto, redAmountList, redKey);
        //红包金额存入缓存
        saveRedAmount2Cache(redPacketDto, redAmountList, redKey);
        return redKey;
    }
    return null;
}

/**
保存发红包的记录,和记录生成的每个随机金额的红包
*/
@Async
public void recordRedPacket(RedPacketDto redPacketDto, List<Integer> redAmountList, String redKey) {
     
    RedRecord entity = new RedRecord();
    entity.setCreateTime(DateTime.now().toSqlDate());
    entity.setUserId(redPacketDto.getUserId());
    entity.setRedPacket(redKey);
    entity.setTotal(redPacketDto.getCount());
    entity.setAmount(BigDecimal.valueOf(redPacketDto.getAmount()));
    redRecordMapper.insertSelective(entity);//保存发红包人所发的金额记录.

    //保存每一个红包记录
    redAmountList.parallelStream().forEach(redAmount -> {
     
        RedDetail detail = new RedDetail();
        detail.setRecordId(entity.getId());
        detail.setAmount(BigDecimal.valueOf(redAmount));
        redDetailMapper.insertSelective(detail);
    });
}

/**
将生成的红包个数记录到缓存
*/
@Async
public void saveRedAmount2Cache(RedPacketDto redPacketDto, List<Integer> redAmountList, String redPacketKey) {
     
    //存入中的红包金额
    redisTemplate.opsForValue().set(redPacketKey + ":total", redPacketDto.getCount());
    //用一个list存放所有的小红包金额
    redisTemplate.opsForList().leftPushAll(redPacketKey, redAmountList);
}

红包的金额生成

红包金额的生成采用二倍均值法,每次用红包所剩金额处于待分配红包个数的两倍,即可,算法比较简单,并不复杂,具体流程如下所示:

Redis拾遗(九)——一个简单的抢红包实例_第2张图片

将这个算法流程变成代码,也并不复杂

@Slf4j
public class RedPacketUtil {
     

    /**
     * 拆分红包的方法
     *
     * @param totalCount 拆分成的红包个数
     * @param totalMoney 总的红包金额
     * @return
     */
    public static List<Integer> divideRedPackage(int totalCount, int totalMoney) {
     
        List<Integer> amountList = Lists.newLinkedList();
        if (totalCount > 0 && totalMoney > 0) {
     
            int restMoney = totalMoney;
            int restCount = totalCount;
            int redAmount = 0;
            Random random = new Random();
            while (restCount > 1) {
     //由于随机数本身的机制,无法在固定的循环次数之内分配完成所有金额,因此这里循环n-1次
                redAmount = random.nextInt(restMoney / restCount * 2 - 1) + 1;
                amountList.add(redAmount);
                restMoney -= redAmount;
                restCount--;
            }
            //这里将剩余金额作为最后一个红包。
            amountList.add(restMoney);
        }
        log.info("最终分配的结果为:{}",amountList);
        return amountList;
    }
}

红包id的生成

这里只是顺便提一句,为了保证生成的红包id唯一,这里用到了雪花算法,相关算法代码百度即可找到,这里为了篇幅不再贴出。

抢红包流程

基本的抢红包流程其实也不复杂,先判断用户是否抢过红包以及红包是否存在的校验,如果不存在则直接返回。之后从缓存中随机获取一个红包金额,如果金额大于0,表示抢到了红包,这个时候减少红包个数,并且异步记录抢红包的记录。

Redis拾遗(九)——一个简单的抢红包实例_第3张图片

直接上代码

/**
 * 抢红包
 * @param userId
 * @param redPacketKey
 * @return
 */
public Integer robRedPacket(final Integer userId,final String redPacketKey){
     
    ValueOperations valueOperation = redisTemplate.opsForValue();

    //判断当前用户是否抢过红包
    final String redUserRobbedKey = redPacketKey+userId+":rob";
    Object isRobbedAmount = valueOperation.get(redUserRobbedKey);
    if(isRobbedAmount!=null){
     
        log.info("用户:{},已经抢过该红包了",userId);
        return 0;
    }

    boolean existRedPacket = isExistRedPacket(redPacketKey);
    if(existRedPacket){
     
        Object getRedValue = redisTemplate.opsForList().rightPop(redPacketKey);
        if(getRedValue!=null){
     
            //红包个数减一
            final String redCountKey = redPacketKey+":total";
            valueOperation.increment(redCountKey,-1L);

            //抢红包记录入库
            final Integer getRedAmount = Integer.valueOf(String.valueOf(getRedValue));
            recordRobRedPacket(userId,redPacketKey,getRedAmount);

            //记录当前用户获取的红包记录,避免同一个用户重复获取红包
            valueOperation.set(redUserRobbedKey,getRedValue,24L,TimeUnit.HOURS);
            log.info("用户:{},抢到了红包,红包id为:{},抢到的金额为:{}",userId,redPacketKey,getRedAmount);
            return getRedAmount;
        }
    }else{
     
        log.info("用户:{},来晚一步,红包被抢光了",userId);
    }
    return 0;
}

/**
 * 判断缓存中红包的个数是否大于0
 * @param redPacketKey
 * @return
 */
private boolean isExistRedPacket(final String redPacketKey) {
     
    Object total = redisTemplate.opsForValue().get(redPacketKey + ":total");
    if (total != null && Integer.valueOf(String.valueOf(total)) > 0) {
     
        return true;
    }
    return false;
}

/**
 * 保存抢红包的金额记录,异步记录
 * @param userId
 * @param redPacketKey
 * @param amount
 */
@Async
public void recordRobRedPacket(final Integer userId,final String redPacketKey,final Integer amount){
     
    RedRobRecord entity = new RedRobRecord();
    entity.setUserId(userId);
    entity.setRedPacket(redPacketKey);
    entity.setAmount(BigDecimal.valueOf(amount));
    entity.setRobTime(DateTime.now());
    redRobRecordMapper.insertSelective(entity);
}

一些问题

上述代码其实还有些问题,通过JMeter进行压力测试,在1秒内并发2000个请求(微信群最多500人,这里2000的并发请求,算是比较极限了),会出现同一个客户抢同一个红包多次的情况,如下所示(下述结果直接取自数据库)。

Redis拾遗(九)——一个简单的抢红包实例_第4张图片

为了解决这个问题,还是需要引入分布式锁,关于Redis实现分布式锁其实比较简单,之前总结过 ——基于Redis实现分布式锁实例。

引入分布式锁的解决方案

/**
 * 一个小的随机红包 每个人只能抢一次;一个人每次只能抢到一个小的随机红包金额 - 永远保证1:1的关系
 * @param userId
 * @param redPacketKey
 * @return
 */
public Integer robRedPackageVersion03(final Integer userId,final String redPacketKey){
     
    ValueOperations valueOperations = redisTemplate.opsForValue();
    final String redUserRobbedKey = redPacketKey+userId+":rob";
    Object isRobbedAmount = valueOperations.get(redUserRobbedKey);
    if(isRobbedAmount!=null){
     
        log.info("用户:{},已经抢过该红包了",userId);
        return 0;
    }

    boolean existRed = isExistRedPacket(redPacketKey);
    if(existRed){
     
        final String lockKey = redPacketKey+userId+"-lock";
        boolean lock =valueOperations.setIfAbsent(lockKey,redPacketKey);//底层调用的是setNx
        try{
     
			//TODO:抢红包的部门加入分布式锁
            if(lock){
     
                redisTemplate.expire(lockKey,48L,TimeUnit.HOURS);
                Object getRedValue = redisTemplate.opsForList().rightPop(redPacketKey);
                if(getRedValue!=null){
     
                    //红包个数减一
                    final String redCountKey = redPacketKey+":total";
                    valueOperations.increment(redCountKey,-1L);

                    //抢红包记录入库
                    final Integer getRedAmount = Integer.valueOf(String.valueOf(getRedValue));
                    recordRobRedPacket(userId,redPacketKey,getRedAmount);

                    //记录当前用户获取的红包记录,避免同一个用户重复获取红包
                    valueOperations.set(redUserRobbedKey,getRedValue,24L,TimeUnit.HOURS);
                    log.info("用户:{},抢到了红包,红包id为:{},抢到的金额为:{}",userId,redPacketKey,getRedAmount);
                    return getRedAmount;
                }
            }
        }catch (Exception e){
     

        }finally{
     
            //TODO:这里不需要释放锁,因为红包一发出去,就是一个新的key;一旦被抢完,生命周期永远终止
        }
    }
    return 0;
}

到这一步,才完成真正的抢红包操作。注意,这里并不需要释放锁(毕竟抢红包针对一个用户也只是一次性的操作),只需要给分布式锁设置一个过期时间,到时候会自动释放。

其实分布式锁其实还有其他的实现方式Redission和zookeeper的实现方式。Redission实现起来更为简介,这个后面会对这个中间级进行总结。

遗留问题

上述实例代码中,都是基于红包被抢光的前提。但是真正的实际操作中,客户发了红包之后,24小时之后会回退,这个功能可以通过定时任务和消息中间件的延迟队列来实现,这里篇幅所限不再总结。至此,关于Redis中大部分的总结算是告一段落,还有一个AOF的持久化机制,这个其实我觉得如果不是专业的运维人员简单了解一下就行。本系列涉及的springboot中操作Redis的源码提供免积分下载,下载地址为
springboot-reids 代码实例下载地址

你可能感兴趣的:(#,redis)