需求场景
各种节假日,发红包+抢红包
实现思路
- 红包拆分。100个红包拆分成10个,保证了每次随机金额的平均值是相等的,不会因为抢红包的先后顺序而造成不公平。使用二倍均值法。
100*2/(10)=20
,在[0-20]之间设置红包金额;如果抽中一个,金额是10,90*2/(10-1)=20
,金额大小还在[0-20] - 发红包的时候,生成红包金额列表和红包的唯一主键。
- 抢红包的时候,判断当前人是否抽过红包,判断红包金额列表还有数据,如果数据都是ok的,用hash结构存储用户+金额的关系。
Java实现
@RestController
public class RedPackageController {
public static final String RED_PACKAGE_KEY = "redpackage:";
public static final String RED_PACKAGE_CONSUME_KEY = "redpackage:consume:";
private final String ID_KEY = "id:generator:redpacket";
@Autowired
private RedisTemplate redisTemplate;
/**
* 发红包
* http://localhost:8080/send?totalMoney=100&redPackageNumber=10
*
* @param totalMoney
* @param redPackageNumber
* @return
*/
@RequestMapping("/send")
public String sendRedPackage(int totalMoney, int redPackageNumber) {
//1 拆红包,总金额拆分成多少个红包,每个小红包里面包多少钱
Integer[] splitRedPackages = splitRedPackage(totalMoney, redPackageNumber);
//2 红包的全局ID
String key = incrementId().toString();
//3 采用list存储红包并设置过期时间,红包主有且仅有一个,不用加锁控制
redisTemplate.opsForList().leftPushAll(RED_PACKAGE_KEY + key, splitRedPackages);
redisTemplate.expire(RED_PACKAGE_KEY + key, 1, TimeUnit.DAYS);
return key;
}
/**
* 抢红包
* http://localhost:8080/rob?redPackageKey=1&userId=111
*
* @param redPackageKey
* @param userId
* @return
*/
@RequestMapping("/rob")
public String rodRedPackage(String redPackageKey, String userId) {
//1 验证某个用户是否抢过红包
Object redPackage = redisTemplate.opsForHash().get(RED_PACKAGE_CONSUME_KEY + redPackageKey, userId);
//2 没有抢过就开抢,否则返回-2表示抢过
if (redPackage == null) {
// 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或者RabbitMQ进一步处理
return String.valueOf(partRedPackage);
}
//抢完
return "errorCode:-1,红包抢完了";
}
//3 某个用户抢过了,不可以作弊重新抢
return "errorCode:-2, message: " + "\t" + userId + " 用户你已经抢过红包了";
}
public Long incrementId() {
return this.redisTemplate.opsForValue().increment(ID_KEY);
}
private Integer[] splitRedPackage(int totalMoney, int redPackageNumber) {
//已经被抢的钱
int useMoney = 0;
// 每次抢到的钱
Integer[] redPackageNumbers = new Integer[redPackageNumber];
Random random = new Random();
for (int i = 0; i < redPackageNumber; i++) {
if (i == redPackageNumber - 1) {
redPackageNumbers[i] = totalMoney - useMoney;
} else {
//每次抢到的金额 = 随机区间 (0, (剩余红包金额M ÷ 剩余人数N ) X 2)
int avgMoney = ((totalMoney - useMoney) / (redPackageNumber - i)) * 2;
//System.out.println(avgMoney);
redPackageNumbers[i] = 1 + random.nextInt(avgMoney - 1);
}
useMoney = useMoney + redPackageNumbers[i];
}
return redPackageNumbers;
}
}