在开发中,一个进程中多个线程需要竞争某一资源的时候,我们通常会用一把锁来保证只有一个线程获取到资源。如加上synchronize关键字或ReentrantLock锁等操作。但如果是多个进程相互竞争一个资源,如何保证资源只会被一个操作者持有呢?
比如在微服务的架构下,多个应用服务要同时对同一条数据做修改,要确保数据的正确性,那就只能有一个应用修改成功。
Redis实现分布式锁
上一篇文章中在String-字符串类型中可以用作分布式锁,那么具体是如何实现的呢?
首先 Redis 是一个单独的非业务服务,不会受到其他业务服务的限制,所有的业务服务都可以向Redis发送写入命令,且只有一个业务服务可以写入命令成功,那么这个写入命令成功的服务即获得了锁,可以进行后续对资源的操作,其他未写入成功的服务,则进行阻塞处理。
String类型实现
使用setnx(SET if Not Exists)指令实现,即如果 key 不存在,才会设置它的值,否则什么也不做。
假设有两个客户端同时竞争锁,即向Redis写入lock_key,A客户端写入成功则A先获取到锁,客户端B写入失败则B未获取到锁。A客户端在使用完资源后,将redis中lock_key删除,即释放锁。
代码语言:javascript复制# A执行写入
> setnx lock_key true
(integer) 1
# A获得锁,执行A的业务
> del lock_key
(integer) 1
代码语言:javascript复制# B执行写入
> setnx lock_key true
(integer) 0
# B未获取锁,阻塞
但这样是存在一些问题的,试想如果A客户端在获取锁后,出现了故障,导致del语句没有调用,这样lock_key就一直得不到释放,即该资源锁一直存在,那么其他服务也就永远得不到该资源了。那么如何避免呢?
避免死锁
在Redis写入数据时,可以设置数据过期时间,这样即便在服务故障,锁也能自动释放。
代码语言:javascript复制> setnx lock_key true
(integer) 1
> expire lock_key 5
(integer) 1
# 执行业务
> del lock_key
(integer) 1
通过expire指令实现锁过期时间害存在一些问题,试想如果expire出错,没有成功执行,那么也将会造成前面的死锁情况。
因为 setnx 和 expire 是两条指令而不是原子指令,在Redis2.8版本中,可以通过set key velue ex timeout nx
来实现setnx和expire两条指令。
> set lock_key true ex 5 nx
OK
# 执行业务
> del lock_key
(integer) 1
虽然解决了死锁,但这样又引入了新问题,试想A的业务执行过长,超过了锁的过期时间,A锁已经被释放,但此时B重新又获得了锁,A执行完后释放锁,可能就释放了B刚获取到的锁。这就出现了锁过期,释放其他服务锁的问题
避免锁超时问题
每个服务在设置value的时候,带上自己服务的唯一标识,如UUID,或者一些业务上的独特标识。这样在删除key的时候,只删除自己服务之前添加的key就可以了。
但匹配value和删除key是两条指令,不是原子操作,且redis中没有提供set这样的扩展参数,没法直接通过指令实现原子操作。
这里就需要使用Lua脚本处理,Lua脚本可以保证连续多个指令的原子性执行,将这两个操作合并成一个操作,就可以保证其原子性了。
代码语言:javascript复制# delifequals
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end