Redis7之抢红包案例
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 {@Autowiredprivate 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";@Autowiredprivate 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