微信红包实现原理
接口开发说明
发红包功能接口开发
- 新增一条红包记录
- 往 mysql 里面添加一条红包记录
- 往 redis 里面添加一条红包数量记录
- 往 redis 里面添加一条红包金额记录
抢红包功能接口开发
- 在抢红包这里并不能保证用户已经能领到这个红包
- 抢红包只是做了一个判断,判断当前是否还有红包
- 有红包则返回可以领
- 没红包则返回不可以领
拆红包功能接口开发
- 拆红包才是用户能领到红包
- 这时候要先减 redis 里面的金额和红包数量 decr decreby
- 减完金额再入库
微信红包设计算法分析
玩法:微信金额是拆的时候实时算出来,不是预先分配的,采用的是纯内存计算,不需要预算空间存储
分配:
- 发100块钱,总共10个红包,那么平均值是10块钱一个,那么发出来的红包的额度在0.01元~20元之间波动
- 当前面4个红包总共被领了30块钱时,剩下70块钱,总共6个红包,那么这7个红包的额度在:0.01~(70➗6✖️2)=23.33之间波动
- 这样算下去,可能会超过最开始的全部金额,因此到了最后面如果不够这么算,那么会采取如下算法:保证剩余用户能拿到最低1分钱即可
存储:数据库会累加已经领取的个数与金额,插入一条领取记录。入账则是后台异步操作
转账:通过财付通往红包所得者账户转账,过程通过是异步操作
数据表设计
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='抢红包记录表,抢一个红包插入一条记录';
代码实现
@RestController public class RedPacketController { @Autowired private RedisService redisService; @Autowired private RedPacketInfoMapper redPacketInfoMapper; @Autowired private RedPacketRecordMapper redPacketRecordMapper; private static final String TOTAL_NUM = "_totalNum"; private static final String TOTAL_AMOUNT = "_totalAmount"; /*** * 发红包 * @param uid * @param totalNum * @return */ @ResponseBody @RequestMapping("/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); long redPacketId = System.currentTimeMillis(); //此时无法保证红包id唯一,最好是用雪花算法进行生成分布式系统唯一键 record.setRedPacketId(redPacketId); redPacketInfoMapper.insert(record); redisService.set(redPacketId + "_totalNum", totalNum + ""); redisService.set(redPacketId + "_totalAmount", totalAmount + ""); return "success"; } /** * 抢红包 * * @param redPacketId * @return */ @ResponseBody @RequestMapping("/getPacket") public Integer getRedPacket(long redPacketId) { String redPacketName = redPacketId + TOTAL_NUM; String num = (String) redisService.get(redPacketName); if (StringUtils.isNotBlank(num)) { return Integer.parseInt(num); } return 0; } /** * 拆红包 * * @param redPacketId * @return */ @ResponseBody @RequestMapping("/getRedPacketMoney") public String getRedPacketMoney(int uid, long redPacketId) { Integer randomAmount = 0; String redPacketName = redPacketId + TOTAL_NUM; String totalAmountName = redPacketId + TOTAL_AMOUNT; String num = (String) redisService.get(redPacketName); if (StringUtils.isBlank(num) || Integer.parseInt(num) == 0) { return "抱歉!红包已经抢完了"; } String totalAmount = (String) redisService.get(totalAmountName); if (StringUtils.isNotBlank(totalAmount)) { Integer totalAmountInt = Integer.parseInt(totalAmount); Integer totalNumInt = Integer.parseInt(num); Integer maxMoney = totalAmountInt / totalNumInt * 2; Random random = new Random(); randomAmount = random.nextInt(maxMoney); } //课堂作业:lua脚本将这两个命令一起请求 redisService.decr(redPacketName, 1); redisService.decr(totalAmountName,randomAmount); //redis decreby功能 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(1111); redPacketRecord.setCreateTime(new Date()); redPacketRecordMapper.insertSelective(redPacketRecord); //这里应该查出RedPacketInfo的数量,将总数量和总金额减去 } }
@Service public class RedisService { @Autowired private RedisTemplate redisTemplate; private static double size = Math.pow(2, 32); /** * 写入缓存 * * @param key * @param offset 位 8Bit=1Byte * @return */ public boolean setBit(String key, long offset, boolean isShow) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.setBit(key, offset, isShow); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 写入缓存 * * @param key * @param offset * @return */ public boolean getBit(String key, long offset) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); result = operations.getBit(key, offset); } catch (Exception e) { e.printStackTrace(); } return result; } /** * 写入缓存 * * @param key * @param value * @return */ public boolean set(final String key, Object value) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.set(key, value); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 写入缓存 * * @param key * @return */ public Object get(final String key) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); return operations.get(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 写入缓存 * * @param key * @param value * @return */ public boolean decr(final String key, int value) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.increment(key,-value); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 写入缓存设置时效时间 * * @param key * @param value * @return */ public boolean set(final String key, Object value, Long expireTime) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.set(key, value); redisTemplate.expire(key, expireTime, TimeUnit.SECONDS); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 批量删除对应的value * * @param keys */ public void remove(final String... keys) { for (String key : keys) { remove(key); } } /** * 删除对应的value * * @param key */ public void remove(final String key) { if (exists(key)) { redisTemplate.delete(key); } } /** * 判断缓存中是否有对应的value * * @param key * @return */ public boolean exists(final String key) { return redisTemplate.hasKey(key); } /** * 读取缓存 * * @param key * @return */ public Object genValue(final String key) { Object result = null; ValueOperations<String, String> operations = redisTemplate.opsForValue(); result = operations.get(key); return result; } /** * 哈希 添加 * * @param key * @param hashKey * @param value */ public void hmSet(String key, Object hashKey, Object value) { HashOperations<String, Object, Object> hash = redisTemplate.opsForHash(); hash.put(key, hashKey, value); } /** * 哈希获取数据 * * @param key * @param hashKey * @return */ public Object hmGet(String key, Object hashKey) { HashOperations<String, Object, Object> hash = redisTemplate.opsForHash(); return hash.get(key, hashKey); } /** * 列表添加 * * @param k * @param v */ public void lPush(String k, Object v) { ListOperations<String, Object> list = redisTemplate.opsForList(); list.rightPush(k, v); } /** * 列表获取 * * @param k * @param l * @param l1 * @return */ public List<Object> lRange(String k, long l, long l1) { ListOperations<String, Object> list = redisTemplate.opsForList(); return list.range(k, l, l1); } /** * 集合添加 * * @param key * @param value */ public void add(String key, Object value) { SetOperations<String, Object> set = redisTemplate.opsForSet(); set.add(key, value); } /** * 集合获取 * * @param key * @return */ public Set<Object> setMembers(String key) { SetOperations<String, Object> set = redisTemplate.opsForSet(); return set.members(key); } /** * 有序集合添加 * * @param key * @param value * @param scoure */ public void zAdd(String key, Object value, double scoure) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); zset.add(key, value, scoure); } /** * 有序集合获取 * * @param key * @param scoure * @param scoure1 * @return */ public Set<Object> rangeByScore(String key, double scoure, double scoure1) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); redisTemplate.opsForValue(); return zset.rangeByScore(key, scoure, scoure1); } //第一次加载的时候将数据加载到redis中 public void saveDataToRedis(String name) { double index = Math.abs(name.hashCode() % size); long indexLong = new Double(index).longValue(); boolean availableUsers = setBit("availableUsers", indexLong, true); } //第一次加载的时候将数据加载到redis中 public boolean getDataToRedis(String name) { double index = Math.abs(name.hashCode() % size); long indexLong = new Double(index).longValue(); return getBit("availableUsers", indexLong); } /** * 有序集合获取排名 * * @param key 集合名称 * @param value 值 */ public Long zRank(String key, Object value) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); return zset.rank(key,value); } /** * 有序集合获取排名 * * @param key */ public Set<ZSetOperations.TypedTuple<Object>> zRankWithScore(String key, long start,long end) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); Set<ZSetOperations.TypedTuple<Object>> ret = zset.rangeWithScores(key,start,end); return ret; } /** * 有序集合添加 * * @param key * @param value */ public Double zSetScore(String key, Object value) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); return zset.score(key,value); } /** * 有序集合添加分数 * * @param key * @param value * @param scoure */ public void incrementScore(String key, Object value, double scoure) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); zset.incrementScore(key, value, scoure); } /** * 有序集合获取排名 * * @param key */ public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithScore(String key, long start,long end) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeByScoreWithScores(key,start,end); return ret; } /** * 有序集合获取排名 * * @param key */ public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithRank(String key, long start, long end) { ZSetOperations<String, Object> zset = redisTemplate.opsForZSet(); Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeWithScores(key, start, end); return ret; } }
抢红包功能扩展设计
- 将红包 ID 的请求放入请求队列中,如果发现超过红包的个数,直接返回
- 类推出 token 令牌和秒杀设计原理
注意点
- 抢到红包不代表能拆成功
- 2014 年的红包一点开就知道金额,分两次操作,先抢到金额,然后再转账。2015 年后的红包的拆和抢是分离的,需要点两次,因此会出现抢到红包了,但点开后告知红包已经被领完的状况。进入到第一个页面不代表抢到,只表示当时红包还有。