Redis案例-微信抢红包
1、业务描述
微信红包,一个人能发红包,红包的分发规则,红包能被几个人抢,超过24小时没有人领取自动退回原账户,红包详情页面有每个人的抢红包记录,包括金额大小和时间,当前已经抢了几个人剩余红包个数等信息。
2、需求分析
- 各种节假日,发红包+抢红包,不用说,100%高并发业务要求,不能用mysql来做;
- 一个总的大红包,会有可能拆分成多个小红包,总金额=分金额1+分金额2+分金额3…分金额N;
- 每个人只能抢一次,你需要有记录,比如100块钱,被拆分成10个红包发出去,总计有10个红包,抢一个少一个,总数显示(10/6)直到完,需要记录那些人抢到了红包,重复抢作弊不可以;
- 有可能还需要你计时,完整抢完,从发出到全部over,耗时多少;
- 红包过期,或者群主人品差,没人抢红包,原封不动退回;
- 红包过期,剩余金额可能需要回退到发红包主账户下;
由于是高并发不能用mysql来做,只能用redis,那需要redis的什么数据类型?
3、架构设计
-
拆分算法
红包其实就是金额,拆分算法,100块钱,分成10个小红包,金额有可能小概率相同,可能有两个红包都是2.58,如何拆分随机金额设定每个红包里塞多少钱是个问题。
-
次数限制
每个人只能抢一次,不能重复。
-
原子性
每抢走一个红包就减少一个(类似库存减少),那就需要保证库存的原子性,不能加锁实现
关键点
-
发红包
-
抢红包
抢,不加锁且原子性,还能支持高并发,每人一次且有抢红包记录
-
记红包
记录每个人抢了多少
-
拆红包算法
- 所有人抢到的金额之和等于红包总金额,不能超过,也不能少于。
- 每个人至少抢到一分钱。
- 要保证所有人抢到的金额的几率相等。
抢红包业务通用算法,一般业内的红包通用算法
-
二倍均值法
剩余红包金额为M,剩余人数为N,那么有以下公式
每次抢到的金额 = 随机区间 (0到 (剩余红包金额M ➗剩余人数N)X2)
这个公式,保证了每次随机金额的平均值是相等的,不会因为抢红包的先后顺序而造成不公平。
举个例子:
假设有10人,红包总额为100元
- 第一次:100➗10 X2 =20,所以第一个人的随机范围是(0,20),平均可以抢到10元。假设的一个人随机抢到了10元,那么剩余金额就是90元;
- 第二次:90➗9 X2 =20,所以第一个人的随机范围是(0,20),平均可以抢到10元。假设的一个人随机抢到了10元,那么剩余金额就是80元;
- 第三次:80➗10 X2 =20,所以第一个人的随机范围是(0,20),平均可以抢到10元。假设的一个人随机抢到了10元,以此类推…
分析一下RedPackageController
发红包 | 抢红包 | 记红包 | 拆红包算法 |
---|---|---|---|
设置K V 一个红包多个金额值使用list | 如何保证高并发+多线程+不加锁且保证原子性 在redis中,每个命令就是 单线程天生的原子性 使用数据结构list,lpop出数据 | 盘点+汇总防止作弊,同一个用户不可以抢2次红包使用redis中的hset,它本来就不允许相同 | 业内常用二倍均值法 |
4、编码实现
说明:此处的controller是基于前面学习的redis一系列的案例基础上添加的。
RedPackageController.java
package com.zm.controller;
import cn.hutool.core.util.IdUtil;
import com.google.common.primitives.Ints;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@RestController
public class RedPackageController {
public static final String RED_PACKAGE_KET = "redpackage:";
public static final String RED_PACKAGE_CONSUME_KET = "redpackage:consume";
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/send")
//totalMoney红包总金额,redPackageNumber红包个数
public String sendRedPackage(int totalMoney,int redPackageNumber){
//1、拆红包,将总金额totalMoney拆分成redPackageNumber个子红包
Integer[] splitRedPacks = splitRedPackageAlgorithm(totalMoney, redPackageNumber);//通过拆分红包算法分好的多个子红包数组
//2、拆完红包开始发红包,发红包保存进list结构里面
String key = RED_PACKAGE_KET + IdUtil.simpleUUID();
redisTemplate.opsForList().leftPushAll(key,splitRedPacks);
//还需要设置24小时过期
redisTemplate.expire(key, 1,TimeUnit.DAYS);
//3、将发红包信息返回前台显示
return key + "\t" + Ints.asList(Arrays.stream(splitRedPacks).mapToInt(Integer::valueOf).toArray());
}
//抢红包
@RequestMapping("/rob")
public String robRedPackages(String redPackageKey,String userID){
//1、验证某个用户是否抢过红包,不可以多抢
Object redPackage =redisTemplate.opsForHash().get(RED_PACKAGE_CONSUME_KET+redPackageKey,userID);
if (redPackage == null){//如果为空就是没抢过
//2、从红包里(list)里出队一个作为客户抢的红包,抢一个红包
Object partRedPackage = redisTemplate.opsForList().leftPop(RED_PACKAGE_KET + redPackageKey);
if (partRedPackage != null){
//抢到红包后需要做记录,进入hash结构,表示谁抢了多少钱红包
redisTemplate.opsForHash().put(RED_PACKAGE_CONSUME_KET+redPackageKey,userID,partRedPackage);
System.out.println("用户:"+userID+"\t 抢到了:"+partRedPackage+"元");
//后续可以有的操作,异步进mysql或者MQ进一步做统计,做个汇总
return "恭喜你抢到了:"+String.valueOf(partRedPackage)+"元";
}
//抢完了
return "手慢无,你来晚了,红包没了";
}
//3、某个用户抢过了,不可以作弊抢多次
return userID+"你已经抢过了,不能重复抢!";
}
//拆分红包算法----二倍均值算法
private Integer[] splitRedPackageAlgorithm(int totalMoney,int redPackageNumber){
Integer[] redPackageNumbers = new Integer[redPackageNumber];//拆开好的红包数组,当前是空的
int userMoney = 0;//已经被抢的红包金额
for (int i = 0; i < redPackageNumber; i++) {
//先判断当前循环次数是不是最后一个,如果是最后一个就不需要使用二倍均值算法了
if (i == redPackageNumber -1){
redPackageNumbers[i] = totalMoney - userMoney;
}else {
//再使用二倍均值算法
//拆分金额 = 随机区间内(0,(剩余红包金额M ➗ 剩余红包个数N) * 2)
int avgMoney = ((totalMoney - userMoney) / (redPackageNumber - i)) * 2;
redPackageNumbers[i] = 1 + new Random().nextInt(avgMoney -1);
}
userMoney = userMoney + redPackageNumbers[i];
}
return redPackageNumbers;
}
}
测试
启动程序之后先塞红包:localhost:7777/send?totalMoney=100&redPackageNumber=5
到redis查看红包拆分情况
然后抢红包:localhost:7777/rob?redPackageKey=e0798e46601944d6b9bae951f0baf4c3&userID=1
到redis中查看记录情况
没有错 1号用户抢了50元,接下来还用1号抢试试
提示出来了,不能重复抢
接下来全抢完
然后再看redis中还有没有了
只剩下记录了,红包已经没有了,所以红包的key也没有了。这个时候再来一个6号抢红包,就会发现没有了。