前言
与分布式锁
相对应的是本地锁
,像我们熟悉的synchronized和ReentrantLock都是本地锁,本地锁是作用于JVM内部,单个进程内的操作共享资源互斥
。而现在主流都是分布式和微服务架构,会部署多个服务(多个JVM),为此分布式锁也就应运而生了。
分布式锁主流实现有3种:基于Redis、Zookeeper或Mysql等数据库。
Redis实现分布式锁使用得非常广泛,也是面试的重要考点之一,很多同学都知道这个知识,也大致知道分布式锁的原理,但是具体到细节的掌握上,往往并不完全正确。所以下面就让我们手写Redis分布式锁,以版本迭代的方式,渐进式的解读遇到的问题
和对应的解决方案
,帮你彻底理解Reids分布式锁。
一、v1 初出茅庐
setnx (SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。
两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。
基本语法:setnx key value
例如两个进程同时加锁,只能成功1个:
释放锁,直接使用 DEL 命令删除这个 key 即可:
基于这个思路,使用RedisTemplate我写下了v1版本
的实现代码:
模拟减库存调用如下:
二、v2 小心死锁
1. 业务逻辑异常导致死锁
解决方案:当lock成功后,对执行的业务逻辑加**try finally
**,保证即使业务逻辑异常,也可以unlock。
2. 服务宕机导致死锁
在执行业务逻辑时,虽然我们加了try finally,但假设获得锁的服务宕机,还没有来的及解锁,那么这个锁将一直被占有,其它客户端也将永远拿不到这个锁了。
解决方案:设置过期时间,服务宕机的话key也会在指定时间内自动过期,不会永远占有锁。
我们修改lock方法,对key加上expire
10s:
public boolean lock() {
// 等价于原生命令 setnx key 1
Boolean ok = stringRedisTemplate.opsForValue().setIfAbsent(key, "1");
boolean res = Boolean.TRUE.equals(ok);
if (res) {
// 等价于原生命令 expire key 10
stringRedisTemplate.expire(key, 10, TimeUnit.SECONDS);
}
return res;
}
三、v3 彻底搞定死锁
v2的思路是对的,但是这里还有个小问题,如果setnx成功但expire失败呢? 依然会有死锁的可能,这个问题的根源在于setnx和expire是两条指令而不是原子指令,所以解决原子性问题我们可以采用lua脚本
,详见我写的 Redis使用Lua脚本:保证原子性【项目案例分享】
另外,在Redis2.8版本中,作者加入了set指令的扩展参数ex nx,使得setnx和expire指令可以一起执行:
基本语法: set key value ex secords nx
例如:
我们修改lock方法如下:
代码语言:javascript复制public boolean lock() {
// 等价于原生命令 set key 1 ex 10 nx
Boolean ok = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return Boolean.TRUE.equals(ok);
}
这样就彻底解决了**死锁
**问题