1.需求
-
发红包
-
抢红包
-
不加锁保证原子性,支持高并发
-
每人只能抢一次
-
-
记红包
- 记录每人抢了多少
-
拆红包
- 红包算法(二倍均值法)
- 所有人抢到的金额之和等于红包金额,不能超过,也不能少于
- 每个人至少抢到一分钱
- 要保证所有人抢到金额的几率相等
- 红包算法(二倍均值法)
二倍均值法
每次抢到的金额 = 随机取件(0,(剩余红包金额 ÷ 剩余人数N)× 2)
这个公式保证了每次随机金额的平均值是相等的,不会因为抢红包的先后顺序而造成不公平
2. 编码
- RedPackageService
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.stereotype.Service;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* 模拟抢红包
*
* @author 晓风残月Lx
* @date 2023/4/5 16:00
*/
@Service
public class RedPackageService {
@Autowired
private RedisTemplate redisTemplate;
public static final String RED_PACKAGE_KEY = "redpackage:";
public static final String RED_PACKAGE_CONSUME_KEY = "redpackage:consume";
/**
* 拆红包的算法 ---> 二倍均值算法
*
* @param totalMoney
* @param redpackageNumber
* @return
*/
public Integer[] splitRedPackageAlgorithm(int totalMoney, int redpackageNumber) {
Integer[] redPackageNums = new Integer[redpackageNumber];
// 已经被抢夺的红包金额
int useMoney = 0;
for (int i = 0; i < redpackageNumber; i++) {
if (i == redpackageNumber - 1) {
redPackageNums[i] = totalMoney - useMoney;
} else {
// 二倍均值算法,每次拆分后塞进子红包的金额
// 金额 = 随机取件(0,(剩余红包金额 / 未被抢的剩余红包数 N )* 2 )
int avgMoney = ((totalMoney - useMoney) / (redpackageNumber - i)) * 2;
redPackageNums[i] = 1 + new Random().nextInt(avgMoney - 1);
}
useMoney = useMoney + redPackageNums[i];
}
return redPackageNums;
}
/**
* 发红包
*
* @param totalMoney
* @param redPackageNumber
*/
public String sendRedPackage(int totalMoney, int redPackageNumber) {
// 1. 拆红包,将总金额totalMoney拆分成 redpackageNumber 个子红包
Integer[] splitRedPackages = splitRedPackageAlgorithm(totalMoney, redPackageNumber); // 拆分红包算法(二倍均值算法)通过后获得的多个子红包数组
// 2. 发红包并保存进 list 结构 并且设置过期时间
String key = RED_PACKAGE_KEY + IdUtil.simpleUUID();
redisTemplate.opsForList().leftPushAll(key, splitRedPackages);
redisTemplate.expire(key, 1, TimeUnit.DAYS);
// 3.发红包ok,返回前台显示
return key + "\t" + Ints.asList(Arrays.stream(splitRedPackages).mapToInt(Integer::valueOf).toArray());
}
public String robRedPackage(String redPackageKey, String userId) {
// 1.验证某个用户是否抢过红包,不可以多抢
Object redPackage = redisTemplate.opsForHash().get(RED_PACKAGE_CONSUME_KEY + redPackageKey, userId);
// 2.没有抢过红包可以抢,如果抢过返回 -2 表示抢过
if (null == redPackage) {
// 2.1 从 list 结构中出队一个作为抢的红包
Object partRedPackage = redisTemplate.opsForList().leftPop(RED_PACKAGE_KEY + redPackageKey);
if (partRedPackage != null) {
// 2.2 抢到红包后 需要记录到hash结构中 记录每人抢到的红包
redisTemplate.opsForHash().put(RED_PACKAGE_CONSUME_KEY + redPackageKey, userId, partRedPackage);
System.out.println("用户" + userId + "\t 抢到了多少钱的红包:" + partRedPackage);
// TODO 后续异步进mysql或者MQ进一步做统计处理,每一年你发出多少红包,抢到了多少红包
return String.valueOf(partRedPackage);
}
// 2. 抢完了
return "errorCode: -1, 红包抢完了";
} else {
// 3. 抢过了红包
int myRedPackage = (int) redisTemplate.opsForHash().get(RED_PACKAGE_CONSUME_KEY + redPackageKey, userId);
return "errorCode: -2, message:" + userId + "\t 你已经抢过了红包,不能在抢了,你抢的金额是" + myRedPackage;
}
}
}
- RedPackageController
import com.xfcy.service.RedPackageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 模拟抢红包,模拟整数的
* @author 晓风残月Lx
* @date 2023/4/5 15:53
*/
@RestController
public class RedPackageController {
public static final String RED_PACKAGE_KEY = "redpackage:";
public static final String RED_PACKAGE_CONSUME_KEY = "redpackage:consume";
@Autowired
private RedPackageService redPackageService;
@RequestMapping("/send")
public String sendRedPackage(int totalMoney, int redPackageNumber) {
String str = redPackageService.sendRedPackage(totalMoney, redPackageNumber);
return str;
}
@RequestMapping("/rob")
public String robRedPackage(String redPackageKey, String userId) {
String str = redPackageService.robRedPackage(redPackageKey, userId);
return str;
}
}
-
测试
-
http://localhost:7070/send?totalMoney=100&redPackageNumber=5
-
http://localhost:7070/rob?redPackageKey=b82b3408c2b541eab29b15656c5a7747&userId=1
-
3. 批量删除(扩展)
“***” 代表 key前缀
redis-cli keys "***" | xargs redis-cli del