背景
为什么要做分布式锁?
Java开发就逃不过多线程问题,而对于单个实例,我们可以使用synchronized锁作为基本的线程锁,解决多线程问题,但对于实际项目中集群部署,分布式系统(不同的客户端)要访问共享资源,就会发生并发问题,比如不同的账号允许操作同一订单时。
分布式锁应具备的条件
- 互斥性:在任意一个时刻,只有一个客户端持有锁。
- 无死锁:即便持有锁的客户端崩溃或者其他意外事件,锁仍然可以被获取。
- 可重入:锁的拥有者可再次请求不会阻塞
基于缓存实现分布式锁
由于Redis是一个单独的中间件,不同客户端可以往同一个Redis或者集群中加锁,这样就能保证加锁的地方或者是资源是相同的。并且由于Redis也是单线程的,同时也支持lua脚本,可以保证并发安全的问题,所以可以很简单的实现分布式锁的功能。
首先redis是一个缓存工具,其底层是lua脚本实现对数据的缓存,我们需要实现分布式锁,就是使用其SET命令的机制。
在redis的SET命令中,包含 [NX | XX] 、以及基本的SET,三种模式:
- 仅SET–当key存在时更新velue,当key不存在时新增
NX
– 仅当key尚不存在时才设置该密钥。XX
– 仅当key已存在时才设置该密钥。
因此,利用SETNX的特性,便可实现分布式锁:
如何实现分布式锁
了解了基本的redis实现分布式锁原理,即可开始简单编写一个分布式锁
public class debugRedisLock {
private RedisTemplate<String, Object> redisTemplate;
@PostMapping("/create")
public void createOrder(@RequestParam String id) {
boolean result = Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(id, "value"));
if (!result) {
throw new RuntimeException();
}
try {
service.createOrder(id);
} finally {
redisTemplate.delete(id);
}
}
}
为了保证“无死锁”的特性,我们还需加上超时时间
public class debugRedisLock {
private RedisTemplate<String, Object> redisTemplate;
@PostMapping("/create")
public void createOrder(@RequestParam String id) {
boolean result = Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(id, "value", 10, TimeUnit.SECONDS));
if (!result) {
throw new RuntimeException();
}
try {
service.createOrder(id);
} finally {
redisTemplate.delete(id);
}
}
}
响应式Redis怎么用分布式锁
理解了简单的redis分布式锁的原理和实现,于是我自信满满的开始了踩坑
在实际项目中,可以见到各式各样不同的redis template,例如:RedisTemplate<T, T>、StringRedisTemplate、ReactiveRedisTemplate<T,T>,为避免过量踩坑,又不能确定其他template是否可以在现有环境工作,我们只能使用项目内自身使用的redis,然后不同的redis们,也总有自己的坑,在这里我将使用ReactiveRedisTemplate躺一次浑水。
起初我使用常规的RedisTemplate写法
public class debugRedisLock {
private ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
@PostMapping("/create")
public Mono<Object> createOrder(@RequestParam String id) {
boolean result = Mono.just(Boolean.TRUE).equals(reactiveRedisTemplate.opsForValue().setIfAbsent(id, "value", Duration.ofSeconds(10)));
if (!result) {
throw new RuntimeException();
}
try {
return service.createOrder(id);
} finally {
reactiveRedisTemplate.delete(id);
}
}
}
但是锁确实生效了,是永远的生效了。。。。。。。
Mono.equals
的底层实现是对包装起来的抽象类直接对比:
public boolean equals(Object obj) {
return (this == obj);
}
显然即便是两个都为true的Boolean,也是不会相等的。
第二次,总结他人经验,响应式编程思路:
public class debugRedisLock {
private ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
@PostMapping("/create")
public Mono<Object> createOrder(@RequestParam String id) {
return reactiveRedisTemplate.opsForValue().setIfAbsent(id, "1", Duration.ofSeconds(10))
.flatMap(result -> {
boolean lock = TRUE.equals(result);
if (!lock) {
throw new RuntimeException();
}
try {
return service.createOrder(id);
} finally {
reactiveRedisTemplate.delete(id);
}
});
}
}
Mono中的值只能通过block的方式才能完全取出,但是这是一个要响应出去的接口,不能阻塞去取setIfAbsent的值,那么我们可以用faltMap,类似Stream的方式去对返回值做操作。
虽然分布式锁成功了,但是总有些不对劲,我期望返回一个异常,但实际接口返回了Mono.empty()
,虽然也限制了业务程序,但是这个返回总有点鸽子用脖子飞起来的感觉。
flatMap中逻辑简单,可以确认不会出问题,那问题只会出在响应式的Redis setIfAbsent方法有问题。
显然这个方法是有返回的,但是内部注释却没有提到:
不过它提供了官方文档链接,从文档中我们才了解到:
代码的终极是lua脚本,而这个命令却早已弃用,取而代之的是
新命令的返回也做出了解释:
那么当我们使用的时候,第二次进入接口获取锁,它并不会返回false,而是nil,而对于Mono,setIfAbsent方法返回了Mono.empty()
,就不会执行后续的flatMap。
对此官方也对这一问题,做出了回复:
不过,Mono响应式编程为我们提供了好的解决方案,Mono.defaultIfEmpty()
,当Mono中没有任何数据返回时,可以提供一个默认值返回:
public class debugRedisLock {
private ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
@PostMapping("/create")
public Mono<Object> createOrder(@RequestParam String id) {
return reactiveRedisTemplate.opsForValue().setIfAbsent(id, "1", Duration.ofSeconds(10))
.defaultIfEmpty(FALSE)
.flatMap(result -> {
boolean lock = TRUE.equals(result);
if (!lock) {
throw new RuntimeException();
}
try {
return service.createOrder(id);
} finally {
reactiveRedisTemplate.delete(id);
}
});
}
}
至此,一个简单的分布式锁实现了。
参考文档:
https://redis.io/commands/set/
https://github.com/spring-projects/spring-data-redis/issues/1355