微信红包实现原理
阅读目录
需求分析数据库表设计编码实现 我们平时在用微信的时候,经常会用到‘抢红包’的功能。那么这样一个需求给我们的话,具体又应该怎么实现呢?
回到顶部需求分析 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 进行路由,放入同一个队列里面,从而保证顺序消费,// 这样金额总和就和总金额不会有偏差
复制代码
评论