Redis NX 实现
利用 NX 的原子性,多个线程并发时,只有一个线程可以设置成功,设置成功即获取了锁。
如果没有获取锁,不会阻塞当前方法,直接跳过任务。
获取锁
1
2
| # set key unique_value NX PX 30000
set product:stock:clothes UUID NX PX 30000
|
- key
- unique_value
- NX
- PX
- 自动失效时间。若出现异常,没有主动释放锁,可以保证超时后,锁可以过期失效(毫秒)
释放锁
释放锁将该 key 删除,在释放锁之前需要校验设置的随机数,相同才表示是该线程加的锁,能释放。
需要采用 LUA 脚本,del 命令没有提供校验值的功能。
redis 执行命令是按照一条指令完成之后,再执行下一条,用 lua 脚本,能保证 redis 执行完这个脚本才执行下一条,所以能保证判断 和 删除是原子性的
1
2
3
4
5
| if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
|
实现
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
| @Slf4j
@RestController
public class RedisLockController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/redisLock")
public String redisLock() {
// 获取分布式锁
Boolean lock = redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Expiration expiration = Expiration.seconds(30);
// NX
RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
// 需要使用 redisTemplate 中的序列化器
byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
return redisConnection.set(redisKey, redisValue, expiration);
});
if (lock) { // 获取到了锁
log.info("获取到了锁");
try {
TimeUnit.SECONDS.sleep(15); // 模拟业务处理
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
String script = "if redis.call(\"get\",KEYS[1])==ARGV[1] then\n" +
"\treturn redis.call(\"del\",KEYS[1])\n" +
"else\n" +
"\treturn 0\n" +
"end";
RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
boolean result = redisTemplate.execute(redisScript, Arrays.asList(key), value);
log.info("释放锁 {}", result);
}
}
log.info("业务完成");
return "success";
}
}
|
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
| @Slf4j
@RestController
public class RedisLock {
private RedisTemplate redisTemplate;
// 锁名称,不同业务可能锁不同
private String key;
private String value;
// 锁过期时间,单位秒
private int expireTime;
public RedisLock(RedisTemplate redisTemplate, String key, int expireTime) {
this.redisTemplate = redisTemplate;
this.key = key;
this.expireTime = expireTime;
// value 可以不暴露出去,每个线程都是不一样的
this.value = UUID.randomUUID().toString();
}
/**
* 获取锁
*/
public boolean getLock() {
// 获取分布式锁
Boolean lock = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Expiration expiration = Expiration.seconds(expireTime);
// NX
RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
// 由于这里需要接受 byte, 不能暴力的使用 string.getBytes()
// 要使用模板里面的 key\value 序列化器来实现
byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
Boolean result = redisConnection.set(redisKey, redisValue, expiration, setOption);
return result;
});
return lock;
}
/**
* 释放锁
*/
public boolean unLock() {
// lua 脚本
String script = "if redis.call(\"get\",KEYS[1])==ARGV[1] then\n" +
"\treturn redis.call(\"del\",KEYS[1])\n" +
"else\n" +
"\treturn 0\n" +
"end";
RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
Boolean result = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(key), value);
return result;
}
}
|
Redisson
依赖配置
1
2
3
4
5
| <dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.7</version>
</dependency>
|
实现
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
| @Slf4j
@RestController
public class RedissonLockController {
@Autowired
private RedissonClient redissonClient;
@GetMapping("/redissonLock")
public String redissonLock() {
log.info("执行方法");
final String key = "redisson";
RLock lock = redissonClient.getLock(key);
// 锁超时时间,如果未获得锁,会阻塞等待获取到锁(-1 表示没有超时时间)
lock.lock(30, TimeUnit.SECONDS);
log.info("获取锁");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.info("释放锁");
lock.unlock();
}
log.info("完成业务");
return "success";
}
}
|