二倍均值法生成红包

1
2
3
4
5
6
7
8
9
10
11
12
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient; // 用于分布式锁
// Spring 容器销毁前执行
@PreDestroy
public void shutdownRedisson() {
if (redissonClient != null) {
redissonClient.shutdown();
System.out.println("Redisson 客户端已优雅关闭");
}
}

一、redis 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 初始化红包到Redis
* @param packetId 红包ID
* @param totalPrice 总金额(单位:分)
* @param totalNum 总数量
* @param amounts 预生成的红包金额列表(单位:分)
*/
public boolean initRedPacket(Long packetId, Integer totalPrice, Integer totalNum, List<Integer> amounts) {
try {
// 1. 存储红包基本信息(使用Hash)
Map<String, String> packetInfo = new HashMap<>();
packetInfo.put("totalPrice", String.valueOf(totalPrice));
packetInfo.put("totalNum", String.valueOf(totalNum));
packetInfo.put("remainPrice", String.valueOf(totalPrice));
packetInfo.put("remainCount", String.valueOf(totalNum));
packetInfo.put("status", "1"); // 1-进行中
packetInfo.put("createTime", String.valueOf(System.currentTimeMillis()));

stringRedisTemplate.opsForHash().putAll(RedPacketRedisStructure.Keys.packetInfo(packetId), packetInfo);

// 设置过期时间(24小时)
stringRedisTemplate.expire(RedPacketRedisStructure.Keys.packetInfo(packetId), 24, TimeUnit.HOURS);

// 2. 预生成红包金额列表(使用List,单位:分)
String amountsKey = RedPacketRedisStructure.Keys.packetAmounts(packetId);
for (Integer amount : amounts) {
stringRedisTemplate.opsForList().rightPush(amountsKey, String.valueOf(amount));
}
stringRedisTemplate.expire(amountsKey, 24, TimeUnit.HOURS);

// 3. 初始化剩余数量计数器(使用String)
stringRedisTemplate.opsForValue().set(RedPacketRedisStructure.Keys.packetCount(packetId), String.valueOf(totalNum));
stringRedisTemplate.expire(RedPacketRedisStructure.Keys.packetCount(packetId), 24, TimeUnit.HOURS);

// 4. 初始化已抢用户集合(Set)
String grabbedKey = RedPacketRedisStructure.Keys.packetGrabbed(packetId);
// 添加一个占位符,确保集合存在
stringRedisTemplate.opsForSet().add(grabbedKey, "placeholder");
stringRedisTemplate.opsForSet().remove(grabbedKey, "placeholder");
stringRedisTemplate.expire(grabbedKey, 24, TimeUnit.HOURS);

log.info("红包初始化成功,packetId: {}, totalPrice: {}, totalNum: {}", packetId, totalPrice, totalNum);
return true;
} catch (Exception e) {
log.error("初始化红包失败", e);
return false;
}
}

二、抢红包方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    /**
* 抢红包核心方法(使用分布式锁)
*/
public GrabResult grabRedPacket(Long packetId, Long userId) {
// 1. 获取用户级分布式锁,防止同一用户重复请求
String lockKey = RedPacketRedisStructure.Keys.userLock(packetId, userId);
RLock lock = redissonClient.getLock(lockKey);

try {
// 尝试获取锁,等待100ms,锁持有时间3秒
if (!lock.tryLock(100, 3000, TimeUnit.MILLISECONDS)) {
return GrabResult.fail("系统繁忙,请稍后再试");
}
// 2. 检查是否已抢过
if (hasGrabbed(packetId, userId)) {
return GrabResult.fail("您已经抢过这个红包了");
}
// 3. 减少剩余数量 原子操作
Long remaining = decrementRemainingCount(packetId);
if (remaining == null || remaining < 0) {
return GrabResult.fail("红包已抢完");
}
// 4. 获取红包金额(从预生成列表中获取)
Integer amount = getRedPacketAmount(packetId, remaining);
if (amount == null) {
// 金额获取失败,恢复数量
incrementRemainingCount(packetId);
return GrabResult.fail("红包金额获取失败");
}
// 5. 更新红包剩余金额
boolean updateSuccess = updateRemainingAmount(packetId, amount);
if (!updateSuccess) {
// 更新失败,恢复数量
incrementRemainingCount(packetId);
return GrabResult.fail("红包金额更新失败");
}
// 6. 记录用户已抢
recordUserGrabbed(packetId, userId, amount);
// 7. 保存领取记录
saveGrabRecord(packetId, userId, amount);
// 转换金额单位:分 -> 元 看系统是否需要
// BigDecimal amountYuan = BigDecimal.valueOf(amount)
// .divide(BigDecimal.valueOf(100), 2, RoundingMode.DOWN);
return GrabResult.success(amount, "抢红包成功");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return GrabResult.fail("系统中断");
} catch (Exception e) {
log.error("抢红包异常", e);
return GrabResult.fail("系统异常");
} finally {
// 8. 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}

三、原子操作redis内红包剩余数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 原子操作减少剩余数量
*/
private Long decrementRemainingCount(Long packetId) {
String countKey = RedPacketRedisStructure.Keys.packetCount(packetId);
return stringRedisTemplate.opsForValue().decrement(countKey);
}
/**
* 增加剩余数量(用于回滚)
*/
private Long incrementRemainingCount(Long packetId) {
String countKey = RedPacketRedisStructure.Keys.packetCount(packetId);
return stringRedisTemplate.opsForValue().increment(countKey);
}

从预生成列表中获取红包金额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 从预生成列表中获取红包金额
* @param packetId 红包ID
* @param index 索引位置(从0开始)
*/
private Integer getRedPacketAmount(Long packetId, long index) {
try {
String amountsKey = RedPacketRedisStructure.Keys.packetAmounts(packetId);
String amountStr = stringRedisTemplate.opsForList().index(amountsKey, index);

if (StringUtils.isNotBlank(amountStr)) {
return Integer.parseInt(amountStr);
}
} catch (Exception e) {
log.error("获取红包金额失败", e);
}
return null;
}

四、更新红包剩余金额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 更新红包剩余金额
*/
private boolean updateRemainingAmount(Long packetId, Integer amount) {
try {
String infoKey = RedPacketRedisStructure.Keys.packetInfo(packetId);
// 使用Lua脚本保证原子性
String luaScript =
"local remainPrice = redis.call('HGET', KEYS[1], 'remainPrice') " +
"if remainPrice then " +
" local newAmount = tonumber(remainPrice) - tonumber(ARGV[1]) " +
" if newAmount >= 0 then " +
" redis.call('HSET', KEYS[1], 'remainPrice', tostring(newAmount)) " +
" return 1 " +
" end " +
"end " +
"return 0";
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);

Long result = stringRedisTemplate.execute(
script,
Collections.singletonList(infoKey),
String.valueOf(amount)
);
return result != null && result == 1;
} catch (Exception e) {
log.error("更新剩余金额失败", e);
return false;
}
}

检查用户是否已抢过

1
2
3
4
5
6
7
/**
* 检查用户是否已抢过
*/
private boolean hasGrabbed(Long packetId, Long userId) {
String grabbedKey = RedPacketRedisStructure.Keys.packetGrabbed(packetId);
return Boolean.TRUE.equals(stringRedisTemplate.opsForSet().isMember(grabbedKey, userId.toString()));
}

redis 内记录用户已抢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 记录用户已抢
*/
private void recordUserGrabbed(Long packetId, Long userId, Integer amount) {
try {
String grabbedKey = RedPacketRedisStructure.Keys.packetGrabbed(packetId);
// 1. 添加到已抢集合
stringRedisTemplate.opsForSet().add(grabbedKey, userId.toString());
// 2. 记录用户抢到的金额(Hash结构)
String userAmountKey = grabbedKey + ":amount";
stringRedisTemplate.opsForHash().put(
userAmountKey,
userId.toString(),
String.valueOf(amount)
);
} catch (Exception e) {
log.error("记录用户已抢失败", e);
}
}

保存领取记录到DB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 保存领取记录
*/
private void saveGrabRecord(Long packetId, Long userId, Integer amount) {
try {
String recordsKey = RedPacketRedisStructure.Keys.packetRecords(packetId);
// 使用Hash存储领取记录
String recordId = userId + "_" + System.currentTimeMillis();
Map<String, String> record = new HashMap<>();
record.put("userId", userId.toString());
record.put("amount", String.valueOf(amount));
record.put("grabTime", String.valueOf(System.currentTimeMillis()));
record.put("status", "1"); // 1-成功
stringRedisTemplate.opsForHash().putAll(recordsKey + ":" + recordId, record);
} catch (Exception e) {
log.error("保存领取记录失败", e);
}
}

二倍均值法 预生成红包金额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 预生成红包金额(二倍均值法)
* @param totalPrice 总金额(分)
* @param totalNum 总数量
* @return 金额列表(分)
*/
public List<Integer> generateRedPacketAmounts(int totalPrice, int totalNum) {
List<Integer> amounts = new ArrayList<>();
int remainingAmount = totalPrice;
int remainingCount = totalNum;
Random random = new Random();
for (int i = 0; i < totalNum - 1; i++) {
// 计算最大可分配金额(分)
int avg = remainingAmount / remainingCount;
int max = avg * 2;
// 生成随机金额(最少1分钱)
int amount = random.nextInt(max - 1) + 1;
amounts.add(amount);
remainingAmount -= amount;
remainingCount--;
}
// 最后一个红包
amounts.add(remainingAmount);
// 打乱顺序
Collections.shuffle(amounts);
return amounts;
}

预生成红包金额 均分红包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 预生成红包金额(均分红包)
* @param totalPrice 总金额(分)
* @param totalNum 总数量
* @return 金额列表(分)
*/
public List<Integer> generateRedPacketAverageAmounts(int totalPrice, int totalNum) {
if (totalPrice%totalNum != 0){
return GrabResult.fail("红包金额不允许均分");
}
List<Integer> amounts = new ArrayList<>(totalNum);
// 1. 计算基础金额
int baseAmount = totalPrice / totalNum;
// 2. 生成金额列表
for (int i = 0; i < totalNum; i++) {
amounts.add(baseAmount);
}
return amounts;
}

获取红包统计信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

/**
* 获取红包统计信息
*/
public Map<String, Object> getRedPacketStats(Long packetId) {
Map<String, Object> stats = new HashMap<>();
try {
// 1. 红包基本信息
String infoKey = RedPacketRedisStructure.Keys.packetInfo(packetId);
Map<Object, Object> packetInfo = stringRedisTemplate.opsForHash().entries(infoKey);
stats.put("packetInfo", packetInfo);
// 2. 剩余数量
String countKey = RedPacketRedisStructure.Keys.packetCount(packetId);
String remainCount = stringRedisTemplate.opsForValue().get(countKey);
stats.put("remainCount", remainCount);
// 3. 已抢用户数
String grabbedKey = RedPacketRedisStructure.Keys.packetGrabbed(packetId);
Long grabbedCount = stringRedisTemplate.opsForSet().size(grabbedKey);
stats.put("grabbedCount", grabbedCount);
// 4. 领取记录数
Set<String> recordKeys = stringRedisTemplate.keys(
RedPacketRedisStructure.Keys.packetRecords(packetId) + "*"
);
stats.put("recordCount", recordKeys != null ? recordKeys.size() : 0);
} catch (Exception e) {
log.error("获取红包统计信息失败", e);
}
return stats;
}

清理红包数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 清理红包数据
*/
public boolean cleanupRedPacket(Long packetId) {
try {
// 删除所有相关的key
String pattern = "red_packet:*:" + packetId + "*";
Set<String> keys = stringRedisTemplate.keys(pattern);

if (keys != null && !keys.isEmpty()) {
stringRedisTemplate.delete(keys);
}
log.info("清理红包数据成功,packetId: {}", packetId);
return true;
} catch (Exception e) {
log.error("清理红包数据失败", e);
return false;
}
}