写点什么

微信红包实现原理

作者:Johnny
  • 2022-10-14
    辽宁
  • 本文字数:3407 字

    阅读完需:约 1 分钟

阅读目录


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


回到顶部需求分析  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);// 雪花算法生成唯一 idlong 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);}
复制代码


复制代码

用户头像

Johnny

关注

还未添加个人签名 2021-03-10 加入

还未添加个人简介

评论

发布
暂无评论
微信红包实现原理_Johnny_InfoQ写作社区