前言
微信红包大家应该不陌生吧,别看小小的一个红包,涉及到技术涵盖很多方面的,比如如图所示,用户发一个红包,会涉及发红包,红包存储,红包拆分,抢红包等流程。本文将详细介绍,一个红包从诞生到过期的整个流程,并且通过代码案例实践讲解,而且重点会分析讲解红包的拆分算法。
微信红包设计流程
依照发红包,红包拆分,抢红包的流程来涉及整个红包流程,采用什么数据结构进行红包设计,由于抢红包,是高并发的,并且响应也要及时,所以采用Redis非关系数据库来设计,是比MySQL好,主要Redis处理每一个命令是单线程,原子操作,无需加锁。
发红包:一个红包会被拆分成多个小红包(金额),比如100块拆分成:20 20 20 30 10,所以可以用redis的list结构来存储
抢红包:需要保证如何保证高并发 多线程 不加锁且保证原子性,所以在redis,每个命令就是单线程原子性天生保证,Ipop出list即可。
记红包:需要保证同一个用户不可以抢夺2次红包,要记录那个红包被那个用户抢了,所以可以用hash结构来存储。
拆红包算法:拆红包算法其实有很多,但是比较合理的可以采用二倍均值算法
代码实现
二倍均值算法实现拆红包
二倍均值,字面也是是红包平均金额的两倍,为了保证随机,取随机区间,最大值为平均金额的两倍,所以最后公式如下:
每次拆分后塞进子红包的余额 = 随机区间(0,(剩余红包金额M / 未被抢的剩余红包个 N) * 2)
具体代码:
代码语言:javascript复制 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 {
// 二倍均值算法,每次拆分后塞进子红包的余额 = 随机区间(0,(剩余红包金额M / 未被抢的剩余红包个 N) * 2)
int avgMoney = ((totalMoney - useMoney) / (redPackageNumber - i) ) * 2;
// new Random().nextInt(avgMoney -1) 是生成 0 到 avgMoney -1(不包括 avgMoney -1),但是不能为0,所以要 1 ,保证第一个红包不为 0
// avgMoney -1 表示最接近二倍均值,而不能等于,这样,保证最后一个红包不为 0,如果不 -1,有可能每次获得随机数都是最大值,那么到最后有可能剩余的红包只能为 0
splitRedPackageNumbers[i] = 1 new Random().nextInt(avgMoney - 1) ;
}
useMoney = useMoney splitRedPackageNumbers[i];
}
return splitRedPackageNumbers;
}
为什么生成每个小红包金额使用如下代码随机生成?
代码语言:javascript复制splitRedPackageNumbers[i] = 1 new Random().nextInt(avgMoney - 1) ;
首先前面加1,原因是new Random().nextInt(avgMoney -1) 是生成 0 到 avgMoney -1(不包括 avgMoney -1),但是不能为0,所以要 1 ,保证第一个红包不为 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 |
可以发现如果随机数最大值,是avgMoney ,会发现,最后一个红包可能为0,所以就随机数控制最大值接近avgMoney -1
avgMoney -1 表示最接近二倍均值,而不能等于,这样,保证最后一个红包不为 0,如果不 -1,有可能每次获得随机数都是最大值,那么到最后有可能剩余的红包只能为 0。
发红包
主要是将拆红包得到的结果,也就是红包总金额totalMoney拆分为redPackageNumber个子红包,保存到list结构里面,并且设置过期时间
代码语言:javascript复制 @RequestMapping(value = "/send")
public String sendRedPackage(int totalMoney, int redPackageNumber){
//1 拆红包,将红包总金额totalMoney拆分为redPackageNumber个子红包
Integer[] splitRedPackages = splitRedPackageAlgorithm(totalMoney,redPackageNumber);
// 2 发红包保存到list结构里面,并且设置过期时间
String key = RED_PACKAGE_KRY IdUtil.simpleUUID();
redisTemplate.opsForList().leftPushAll(key,splitRedPackages);
redisTemplate.expire(key,1, TimeUnit.DAYS);
// 3 发红包成功,返回前台显示
return key "t" Ints.asList(Arrays.stream(splitRedPackages).mapToInt(Integer::valueOf).toArray());
}
调用发红包接口,比如20块钱分成5个红包,查看redis
抢红包
先验证某个用户是否抢过红包,查询redis记录,没有的话,同意用户抢红包,直接获取列表中的其中一个红包,并存红包领取记录,使用hash结构记录红包领取记录
代码语言:javascript复制 /**
* 用户抢红包
* @param redPackageKey 红包
* @param userId 用户id
* @return
*/
@RequestMapping(value = "/rob")
public String robRedPackage(String redPackageKey,String userId){
// 1.验证某个用户是否抢过红包,查询redis记录
Object redPackage = redisTemplate.opsForHash().get(RED_PACKAGE_CONSUME_KRY redPackageKey, userId);
// 2. 用户没有抢过红包
if (null == redPackage){
// 3. 用户抢红包,直接获取列表中的其中一个红包
Object partRedPackage = redisTemplate.opsForList().leftPop(RED_PACKAGE_KRY redPackageKey);
if (partRedPackage != null){
// 4.保存红包领取记录,使用hash结构记录红包领取记录
redisTemplate.opsForHash().put(RED_PACKAGE_CONSUME_KRY redPackageKey,userId,partRedPackage);
System.out.println("用户" userId "t 抢到红包了 " partRedPackage);
return String.valueOf(partRedPackage);
}
return "errorCode : -1 ,红包抢完了";
}
return "errorCode : -2 ," userId " 你已经抢过红包了";
}
调用抢红包接口,返回红包金额
redis也能看到红包记录
总结
本文主要是讲解,红包拆分,发红包,抢红包的流程,并重点介绍了二倍均值法。抢红包的核心思想是将红包金额和数量进行随机分配,以实现公平、随机的抢红包效果。解密过程包括生成红包、抢红包、确认抢红包和查看红包等步骤。然后通过随机生成红包金额,实现了红包的发放和抢红包的功能。
我正在参与2023腾讯技术创作特训营第四期有奖征文,快来和我瓜分大奖!