微信红包实现原理

  我们平时在用微信的时候,经常会用到‘抢红包’的功能。那么这样一个需求给我们的话,具体又应该怎么实现呢?

  

需求分析

  1 发红包:在db、cache各新增一条记录
  2 抢红包:有人发红包之后,肯定很多人同时去抢,所以应该请求访问cache,剩余红包个数大于0就可以点击拆开红包;反之提醒红包已经被抢完了
  3 拆红包:总金额每次都是递减,可以用redis的decreby来做。
  4 查看红包记录:用户直接查db即可。
  这里面就会涉及到2个问题:
    我只发了100个红包,并发下如何保证抢到红包的人数不会超过100.
    红包总金额1w元,如何分配才能让金额不超出这个数,如何保证最后一个人一定能抢到钱.

  

数据库表设计

红包信息表主要字段:    谁发的红包,发红包时间,红包总个数、总金额、剩余红包信息、最后一次被抢红包时间

CREATE TABLE `red_packet_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT, 
  `red_packet_id` bigint(11) NOT NULL DEFAULT  0 COMMENT '红包id,采用timestamp+5位随机数', 
  `total_amount` int(11) NOT NULL DEFAULT 0 COMMENT '红包总金额,单位分',
  `total_packet` int(11) NOT NULL DEFAULT 0 COMMENT '红包总个数',
  `remaining_amount` int(11) NOT NULL DEFAULT 0 COMMENT '剩余红包金额,单位分',
  `remaining_packet` int(11) NOT NULL DEFAULT 0 COMMENT '剩余红包个数',
  `uid` int(20) NOT NULL DEFAULT 0 COMMENT '新建红包用户的用户标识',
  `create_time` timestamp  COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='红包信息表,新建一个红包插入一条记录';

抢红包记录主要字段:    红包信息、抢红包人信息,抢红包时间    (主要都是查看抢红包记录列表的那些字段)

CREATE TABLE `red_packet_record` (
  `id` int(11) NOT NULL AUTO_INCREMENT, 
  `amount` int(11) NOT NULL DEFAULT '0' COMMENT '抢到红包的金额',
  `nick_name` varchar(32) NOT NULL DEFAULT '0' COMMENT '抢到红包的用户的用户名',
  `img_url` varchar(255) NOT NULL DEFAULT '0' COMMENT '抢到红包的用户的头像',
  `uid` int(20) NOT NULL DEFAULT '0' COMMENT '抢到红包用户的用户标识',
  `red_packet_id` bigint(11) NOT NULL DEFAULT '0' COMMENT '红包id,采用timestamp+5位随机数', 
  `create_time` timestamp  COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='抢红包记录表,抢一个红包插入一条记录';

编码实现

发红包

发红包之后,肯定立马会有很多人来抢,如果直接操作数据库会有很大的压力,所以我们把数据放到缓存里面去。

    /***
     * 发红包
     * @param uid 发红包的用户id
     * @param totalNum 红包金额
     * @param totalAmount 红包总个数
     * @return
     */
    @GetMapping("/addPacket")
    public String saveRedPacket(Integer uid, Integer totalNum, Integer totalAmount) {
        // 组装数据
        RedPacketInfo record = new RedPacketInfo();
        record.setUid(uid);
        record.setTotalAmount(totalAmount);
        record.setTotalPacket(totalNum);
        record.setCreateTime(new Date());
        record.setRemainingAmount(totalAmount);
        record.setRemainingPacket(totalNum);
        // 雪花算法生成唯一id
        long redPacketId = new SnowflakeDistributeId(0, 0).nextId();
        record.setRedPacketId(redPacketId);
        // 红包保存到数据库
        redPacketInfoMapper.insert(record);
        // 红包个数和总金额存入缓存
        redisService.set(redPacketId + "_totalNum", totalNum + "");
        redisService.set(redPacketId + "_totalAmount", totalAmount + "");
        return "success";
    }

抢红包

用户点击红包之后,就查看红包数量,如果为0的话,点击拆红包就提示红包被抢完了;反之获取到红包金额数量

    /**
     * 抢红包
     * @param redPacketId 红包id
     * @param uid 用户id
     * @return
     */
    @GetMapping("/getPacket")
    public String getRedPacket(long redPacketId, Integer uid) {
        Object record = redisService.get(uid + RECORD + redPacketId);
        // 如果用户已经抢过红包了,那点击抢红包就应该是查看抢红包的详细记录
        if (StringUtils.isNotBlank((String)record)){
            return "红包详细记录";
        }
        // 查询红包剩余个数
        String redPacketName = redPacketId + TOTAL_NUM;
        String num = (String) redisService.get(redPacketName);
        if (StringUtils.isNotBlank(num)) {
            return num;
        }
        return "0";
    }

拆红包(核心)

这是重点也是难点,我们要保证领取红包的人数不能超过设置的红包个数,还要保证每一个人的红包都能抢到钱、还不能超过总金额。这就会涉及到线程安全问题。现在我们就来来想想,如何合理的生成红包随机金额数量。
  1. 剩余总金额/剩余总个数 = 红包金额平均数
  2. 由于红包是随机金额,我们的红包金额可以在这个平均值左右浮动,总和不变即可
  这样设计,才能真正保证每个人拆开都能领到钱,而且总金额不会超支

    /**
     * 拆红包
     * @param redPacketId 红包id
     * @param uid 用户id
     * @return
     */
    @GetMapping("/getRedPacketMoney")
    public String getRedPacketMoney(int uid, long redPacketId) {
        // 抢到的红包金额
        Integer randomAmount = 0;
        String redPacketName = redPacketId + TOTAL_NUM;
        String totalAmountName = redPacketId + TOTAL_AMOUNT;
        // 预减获取红包剩余数量,decr原子减来防止领取人数超过红包个数
        long decr = redisService.decr(totalAmountName, 1);
        if (decr<0){
            System.out.println(uid+":  抱歉!红包已经抢完了");
            return "抱歉!红包已经抢完了";
        }
        // 下面就开始随机分配金额了,并发下可能领取人数的业务逻辑同时走到了这里,
        // 下面算法最后计算出来的金额就会和总金额有偏差,所以我们可以通过对红包
        // id进行路由,放入同一个队列里面,从而保证顺序消费,
        // 这样金额总和就和总金额不会有偏差

        // 剩余总金额(后面所有逻辑,都由下游业务去队列里面执行)
        Integer totalAmountInt = Integer.parseInt((String) redisService.get(redPacketName));
        // 剩余金额 / 剩余红包个数 * 2 = 最大红包金额
        Integer maxMoney = (int) (totalAmountInt / (decr + 1) * 2);
        Random random = new Random();
        // 红包取值随机数,不超过最大金额(如果是最后一个红包,金额就是剩下的所有钱)
        randomAmount = random.nextInt(maxMoney);
        System.out.println(uid+":  抢到了  "+randomAmount+"   分钱");
        // 红包剩余个数减1,同时剩余金额也要减少
        redisService.decr(redPacketName,randomAmount);  //redis decreby功能
        redisService.set(uid + RECORD + redPacketId,randomAmount.toString());
        // 数据库插入抢红包记录
        updateRacketInDB(uid, redPacketId,randomAmount);
        return randomAmount + "";
    }

    public void updateRacketInDB(int uid, long redPacketId, int amount) {
        // 数据库插入抢红包记录
        RedPacketRecord redPacketRecord = new RedPacketRecord();
        redPacketRecord.setUid(uid);
        redPacketRecord.setRedPacketId(redPacketId);
        redPacketRecord.setAmount(amount);
        redPacketRecord.setCreateTime(new Date());
        redPacketRecordMapper.insertSelective(redPacketRecord);
        // 查询到红包信息
        RedPacketInfoExample example = new RedPacketInfoExample ();
        RedPacketInfoExample.Criteria criteria = example.createCriteria();
        criteria.andRedPacketIdEqualTo(redPacketId);
        RedPacketInfo redPacketInfo = redPacketInfoMapper.selectByExample(example).get(0);
        // 修改红包剩余信息
        redPacketInfo.setRemainingPacket(redPacketInfo.getRemainingPacket()-amount);
        redPacketInfo.setRemainingAmount(redPacketInfo.getRemainingAmount()-1);
        redPacketInfo.setCreateTime(new Date());
        redPacketInfoMapper.updateByPrimaryKey(redPacketInfo);
    }
posted @ 2020-03-19 19:17  吴磊的  阅读(3078)  评论(0编辑  收藏  举报
//生成目录索引列表