Redis分散式锁进化史
近两年来微服务变得越来越热门,越来越多的应用部署在分散式环境中,在分散式环境中,数据一致性是一直以来需要关注并且去解决的问题,分散式锁也就成为了一种广泛使用的技术,常用的分散式实现方式为Redis,Zookeeper,其中基于Redis的分散式锁的使用更加广泛。
但是在工作和网路上看到过各个版本的Redis分散式锁实现,每种实现都有一些不严谨的地方,甚至有可能是错误的实现,包括在代码中,如果不能正确的使用分散式锁,可能造成严重的生产环境故障,本文主要对目前遇到的各种分散式锁以及其缺陷做了一个整理,并对如何选择合适的Redis分散式锁给出建议。
各个版本的Redis分散式锁
V1.0
tryLock(){
SETNX Key 1
EXPIRE Key Seconds
}
release(){
DELETE Key
}
这个版本应该是最简单的版本,也是出现频率很高的一个版本,首先给锁加一个过期时间操作是为了避免应用在服务重启或者异常导致锁无法释放后,不会出现锁一直无法被释放的情况。
这个方案的一个问题在于每次提交一个Redis请求,如果执行完第一条命令后应用异常或者重启,锁将无法过期,一种改善方案就是使用Lua脚本(包含SETNX和EXPIRE两条命令),但是如果Redis仅执行了一条命令后crash或者发生主从切换,依然会出现锁没有过期时间,最终导致无法释放。
另外一个问题在于,很多同学在释放分散式锁的过程中,无论锁是否获取成功,都在finally中释放锁,这样是一个锁的错误使用,这个问题将在后续的V3.0版本中解决。
针对锁无法释放问题的一个解决方案基于GETSET命令来实现
V1.1 基于GETSET
tryLock(){
NewExpireTime=CurrentTimestamp+ExpireSeconds
if(SETNX Key NewExpireTime Seconds){
oldExpireTime = GET(Key)
if( oldExpireTime < CurrentTimestamp){
NewExpireTime=CurrentTimestamp+ExpireSeconds
CurrentExpireTime=GETSET(Key,NewExpireTime)
if(CurrentExpireTime == oldExpireTime){
return 1;
}else{
return 0;
}
}
}
}
release(){
DELETE key
}
思路:
1、SETNX(Key,ExpireTime)获取锁
2、如果获取锁失败,通过GET(Key)返回的时间戳检查锁是否已经过期
3、GETSET(Key,ExpireTime)修改Value为NewExpireTime
4、检查GETSET返回的旧值,如果等于GET返回的值,则认为获取锁成功
注意:这个版本去掉了EXPIRE命令,改为通过Value时间戳值来判断过期
问题:
1、在锁竞争较高的情况下,会出现Value不断被覆盖,但是没有一个Client获取到锁
2、在获取锁的过程中不断的修改原有锁的数据,设想一种场景C1,C2竞争锁,C1获取到了锁,C2锁执行了GETSET操作修改了C1锁的过期时间,如果C1没有正确释放锁,锁的过期时间被延长,其它Client需要等待更久的时间
V2.0 基于SETNX
tryLock(){
SETNX Key 1 Seconds
}
release(){
DELETE Key
}
Redis 2.6.12版本后SETNX增加过期时间参数,这样就解决了两条命令无法保证原子性的问题。但是设想下面一个场景:
1、C1成功获取到了锁,之后C1因为GC进入等待或者未知原因导致任务执行过长,最后在锁失效前C1没有主动释放锁
2、C2在C1的锁超时后获取到锁,并且开始执行,这个时候C1和C2都同时在执行,会因重复执行造成数据不一致等未知情况
3、C1如果先执行完毕,则会释放C2的锁,此时可能导致另外一个C3进程获取到了锁
大致的流程图