前言:
本文主要去分析用Redis实现分布式锁,如何完成一个合理的分布式锁。
Redis使用WATCH命令来代替对数据进行加锁,因为WATCH只会在数据被其他客户端抢先修改的情况下通知执行了这个命令的客户端,而不会阻止其它客户端对数据的修改,即"乐观锁"。
分布式锁是由不同机器上的不同Redis客户端进行获取和释放的。
使用Redis构建锁
Setnx key value
setnx -> Set if not eXists (如果不存在,则SET)的缩写。如果不存在set成功返回int的1,这个key存在了返回0。
setex key seconds value
将value值关联到key,并将key的生存时间设为seconds。
如果key存在,setnx命令将覆写旧值
setex是一个原子性操作,关联值和设置生存时间两个动作会在同一时间内完成。
setnx天生就适合用来实现锁的获取功能,这个命令只会在键不存在的情况下为键设置值,而锁要做的就是将一个随机生成的UUID设置为键的值,并使用这个值来防止锁被其他进程取得。
分布式锁
什么是分布式锁
分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。
分布式锁需要具备哪些条件
- 高性能:加、解锁时高性能。
- 互斥性:在任意一个时刻,只有一个客户端持有锁。
- 无死锁:即便持有锁的客户端崩溃或者其他意外事件,锁仍然可以被获取。
- 容错:只要大部分Redis节点都活着,客户端就可以获取和释放锁
加锁
//...
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
public boolean tryLock(String key, String request) {
String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
if (LOCK_MSG.equals(result)){
return true ;
}else {
return false ;
}
}
使用的是Jedis的
String set(String key, String value, String nxxx, String expx, long time);
API
该命令可以保证 NX EX 的原子性。
一定不要把两个命令(NX EX)分开执行,如果在 NX 之后程序出现问题就有可能产生死锁。
阻塞锁
//一直阻塞
public void lock(String key, String request) throws InterruptedException {
for (;;){
String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
if (LOCK_MSG.equals(result)){
break ;
}
//防止一直消耗 CPU
Thread.sleep(DEFAULT_SLEEP_TIME) ;
}
}
//自定义阻塞时间
public boolean lock(String key, String request,int blockTime) throws InterruptedException {
while (blockTime >= 0){
String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
if (LOCK_MSG.equals(result)){
return true ;
}
blockTime -= DEFAULT_SLEEP_TIME ;
Thread.sleep(DEFAULT_SLEEP_TIME) ;
}
return false ;
}
解锁
解锁也很简单,其实就是把这个 key 删掉就万事大吉了,比如使用 del key 命令。
但现实往往没有那么 easy。
- 如果进程 A 获取了锁设置了超时时间,但是由于执行周期较长导致到了超时时间之后锁就自动释放了。这时进程 B 获取了该锁执行很快就释放锁。这样就会出现进程 B 将进程 A 的锁释放了。
所以最好的方式是在每次解锁时都需要判断锁是否是自己的。
这时就需要结合加锁机制一起实现了。
加锁时需要传递一个参数,将该参数作为这个 key 的 value,这样每次解锁时判断 value 是否相等即可。
所以解锁代码就不能是简单的 del了。
public boolean unlock(String key,String request){
//lua script
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = null ;
if (jedis instanceof Jedis){
result = ((Jedis)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
}else if (jedis instanceof JedisCluster){
result = ((JedisCluster)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
}else {
//throw new RuntimeException("instance is error") ;
return false ;
}
if (UNLOCK_MSG.equals(result)){
return true ;
}else {
return false ;
}
}
这里使用了一个 lua 脚本来判断 value 是否相等,相等才执行 del 命令。
使用 lua 也可以保证这里两个操作的原子性。
因此上文提到的四个基本特性也能满足了:
- 使用 Redis 可以保证性能。
- 阻塞锁与非阻塞锁见上文。
- 利用超时机制解决了死锁。
- Redis 支持集群部署提高了可用性。
总结
- Redis的分布式锁可以确保在分布式的环境下给相关业务加锁,需要保证4个基本原则,不然容易造成死锁,锁失效等各种问题,需要保证加锁解锁操作的原子性。
- 可以使用Redisson来实现Redis的分布式锁。