前言
微信红包作为大家耳熟能详的一种互动方式,其背后的技术支持包含多个方面。从用户发出红包到红包被抢完,涉及到的流程包括发红包、红包存储、红包拆分以及抢红包等。本文将详细介绍这一系列流程,并通过代码案例来实践讲解,特别重点分析红包的拆分算法。
微信红包设计流程
整个红包流程按照发红包、红包拆分、抢红包的顺序来设计。在数据结构的选择上,考虑到抢红包的高并发特性和即时响应要求,采用Redis非关系数据库进行设计是优于MySQL的。Redis的每个命令都是单线程执行,保证原子性操作,无需额外的锁机制。
1. 发红包
一个红包通常会被拆分成多个小红包,例如100元可以拆分为20元、20元、20元、30元和10元。这种情况下,可以使用Redis的list结构来存储这些拆分后的小红包。
2. 抢红包
在高并发环境下,如何保证多线程抢红包时不加锁且保持原子性是关键。Redis的特性使得每个命令都是单线程且原子性的,因此使用LPOP
命令即可实现。
3. 记红包
为了确保同一个用户不能抢夺同一个红包两次,需要记录哪些红包被哪些用户抢过。这里可以使用Redis的hash结构来进行存储。
拆红包算法
在拆红包算法中,较为合理的是采用“二倍均值算法”。该算法的核心思想是每次拆分红包时,取一个随机区间,其最大值为剩余红包金额的两倍与未被抢的剩余红包个数的乘积。这样可以保证拆分的随机性和公平性。
代码实现
二倍均值算法的具体代码实现如下:
private Integer[] splitRedPackageAlgorithm(int totalMoney, int redPackageNumber){
Integer[] splitRedPackageNumbers = new Integer[redPackageNumber];
int useMoney = 0;
for (int i = 0; i < redPackageNumber; i++) {
if(i == redPackageNumber - 1){
splitRedPackageNumbers[i] = totalMoney - useMoney;
}else {
int avgMoney = ((totalMoney - useMoney) / (redPackageNumber - i)) \* 2;
splitRedPackageNumbers[i] = 1 + new Random().nextInt(avgMoney - 1);
}
useMoney += splitRedPackageNumbers[i];
}
return splitRedPackageNumbers;
}
为什么生成每个小红包金额使用如下代码随机生成?
splitRedPackageNumbers[i] = 1 + new Random().nextInt(avgMoney - 1);
首先,前面加1的原因是new Random().nextInt(avgMoney -1)
会生成0到avgMoney - 1
之间的随机数,但不能为0。为了保证第一个红包不为0元,所以需要+1。其次,avgMoney - 1
是为了控制随机数的最大值接近于avgMoney
但不等于它。这样可以确保最后一个红包也不会为0元。如果不这样做,有可能每次随机数都取最大值,导致最后剩余的红包金额只能为0。
后面avgMoney -1,如果不-1,结果是怎样呢?接下来模拟不-1的情况下,假设100块分5个红包,每次随机数都取最大值,那么有如下情况:
红包 | useMoney | avgMoney | 每次随机数最大 | 本次红包金额 |
---|---|---|---|---|
1 | 0 | 20 * 2 | new Random().nextInt(avgMoney) == 39 | 40 |
2 | 40 | 15*2 | new Random().nextInt(avgMoney) == 29 | 30 |
3 | 70 | 10*2 | new Random().nextInt(avgMoney) == 19 | 20 |
4 | 90 | 5*2 | new Random().nextInt(avgMoney) == 9 | 10 |
5 | 0 | | | 0 |
发红包
发红包的主要步骤是将拆分后的小红包保存到Redis的list结构中,并设置过期时间。具体代码如下:
@RequestMapping(value = "/send")
public String sendRedPackage(int totalMoney, int redPackageNumber){
Integer[] splitRedPackages = splitRedPackageAlgorithm(totalMoney,redPackageNumber);
String key = RED\_PACKAGE\_KRY + IdUtil.simpleUUID();
redisTemplate.opsForList().leftPushAll(key,splitRedPackages);
redisTemplate.expire(key,1, TimeUnit.DAYS);
return key+"\t" + Ints.asList(Arrays.stream(splitRedPackages).mapToInt(Integer::valueOf).toArray());
}
调用发红包接口,比如20块钱分成5个红包,查看redis。
抢红包
抢红包的逻辑是先验证用户是否已经抢过该红包,如果没有则允许其抢红包并记录到Redis中。具体代码如下:
@RequestMapping(value = "/rob")
public String robRedPackage(String redPackageKey,String userId){
Object redPackage = redisTemplate.opsForHash().get(RED\_PACKAGE\_CONSUME\_KRY + redPackageKey, userId);
if (redPackage == null){
Object partRedPackage = redisTemplate.opsForList().leftPop(RED\_PACKAGE\_KRY + redPackageKey);
if (partRedPackage != null){
redisTemplate.opsForHash().put(RED\_PACKAGE\_CONSUME\_KRY+redPackageKey,userId,partRedPackage);
System.out.println("用户"+userId+" 抢到红包了 " + partRedPackage);
return String.valueOf(partRedPackage);
}
return "errorCode : -1 ,红包抢完了";
}
return "errorCode : -2 ," + userId + " 你已经抢过红包了";
}
调用抢红包接口后,用户可以返回抢到的红包金额,并且在Redis中也能查看到相应的红包记录。
总结
本文主要介绍了微信红包的拆分、发放和抢红包的流程,并重点讲解了二倍均值法在拆红包算法中的应用。通过随机生成红包金额的方式,实现了公平且随机的抢红包效果。整个过程中涉及到了Redis数据库的高效利用以及高并发环境下的数据处理问题。