一段模拟抢红包的代码

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
public class RedisTest {

/**
* 模拟抢红包逻辑
*
* @param userId
* @param redId
* @return
*/
public Object rob(String userId, String redId) {

// 在处理用户抢红包之前,先判断该用户是否抢过红包
Object object = RedisClient.get(redId + ":" + userId + ":rob");
if (null != object) {
// 说明已经抢过红包了,直接返回
return object;
}

Object total = RedisClient.get(redId + ":total");
int totalNum = Integer.parseInt(total.toString());
// 有红包可抢
if (null != total && totalNum > 0) {
Object value = RedisClient.getRightList(redId);
if (null != value) {
// 设置红包个数 -1
RedisClient.set(redId + ":total", totalNum - 1);

// 将抢到的红包记录异步到数据库中
saveAsync(value);

long time = 60 * 60 * 24;
// 保存到缓存中
RedisClient.set(redId + ":" + userId + ":rob", value, time);
return value;
}
}
return null;
}

/**
* 异步存库
*
* @param object
*/
@Async
public void saveAsync(Object object) {

}

}

以上代码,在单个请求的正常测试下,是没有问题的。但是在实际生产环境中,出现“秒级高并发请求”时,没有控制的多线程会出现抢占资源的情况。从而会出现很多问题。比如数据不一致,或者一个用户抢到了很多个红包。

Redis 底层架构是采用单线程进行设计的,因此它提供的这些操作也是单线程的。操作具有原子性。

原子性 指同一时刻只能有一个线程处理核心业务逻辑。当有其他线程对应的请求过来时,如果前面的线程没有处理完毕,则当前线程将进入等待状态(堵塞),直到前面的线程处理完毕。

优化

通过 Redis 的原子操作 setIfAbsent()方法对该业务逻辑加分布式锁。当多个并发线程同一时刻调用 setIfAbsent()时,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
49
/**
* 使用分布式锁
*
* @param userId
* @param redId
* @return
*/
public Object rob2(String userId, String redId) {
// 在处理用户抢红包之前,先判断该用户是否抢过红包
Object object = RedisClient.get(redId + ":" + userId + ":rob");
if (null != object) {
// 说明已经抢过红包了,直接返回
return object;
}

Object total = RedisClient.get(redId + ":total");
int totalNum = Integer.parseInt(total.toString());
// 有红包可抢
if (null != total && totalNum > 0) {
// 上分布式锁,一个红包每个人只能抢到一次金额,永远要保持一对一
// 构建缓存 key
String lockKey = redId + ":" + userId + ":lock";
// 设定该分布式锁过期时间为 24 小时
long expireTime = 60 * 60 * 24;
boolean lock = RedisClient.setIfAbsent(lockKey, redId, expireTime);

try {
// 表示当前线程已经获取了该分布式锁
if (lock) {
Object value = RedisClient.getRightList(redId);
if (null != value) {
// 设置红包个数 -1
RedisClient.set(redId + ":total", totalNum - 1);

// 将抢到的红包记录异步到数据库中
saveAsync(value);

long time = 60 * 60 * 24;
// 保存到缓存中
RedisClient.set(redId + ":" + userId + "rob", value, time);
return value;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}

RedisClient.setIfAbsent方法

1
2
3
4
5
6
public static boolean setIfAbsent(final String key, Object value, Long expireTime) {
ValueOperations valueOperations = redisTemplate.opsForValue();
Boolean aBoolean = valueOperations.setIfAbsent(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
return aBoolean;
}

此时,在通过 JMeter 每秒 1000 个线程测试,就不会出现一个用户抢到多个红包了。

至此,Redis 分布式锁已经实现。当然,这里只是简单实现了一下。分布式锁可以有很多方式实现,可以再研究其他实现方式。


参考文章:

《分布式中间件技术实战(Java 版)》