非分布式环境下的可重入排他锁的实现很简单,可以使用的方法很多,如synchronized、ReentrantLock等,但是在分布式环境一下则需要思考一下,一般也可以选型为Redis
、MySQL、Zookeeper等。本文通过Redis实现一个分布式可重入排他锁。
通过Redis的原子特性实现一个分布式环境下的排他锁并不难,默认的 SET
NX
可以很好的帮助我们解决并发下的锁key争抢问题。但是如果持有锁key的线程再一次通过该方式获取该锁则会失败,因为此锁key
已经存在了,即 SET NX
命令无法支持锁的重复获取,需要在代码层做控制,这也带来了实现的复杂性。
SET命令
对SET命令不是很了解的同学可以先看一下使用说明,以下摘自 Redis命令参考
:
从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
EX seconds : 将键的过期时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。
PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。 执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。
NX : 只在键不存在时, 才对键进行设置操作。 执行 SET key value NX 的效果等同于执行 SETNX key value 。
XX : 只在键已经存在时, 才对键进行设置操作。
需要注意一个问题就是上面的选项可以同时生效:
127.0.0.1:6379> set mykey haha ex 10 nx
OK
127.0.0.1:6379> ttl mykey
(integer) 6
127.0.0.1:6379> ttl mykey
(integer) 1
127.0.0.1:6379> ttl mykey
(integer) -2
还可以通过lua来实现
127.0.0.1:6379> eval "return redis.call('set', 'mykey', '2_90041', 'ex', '10', 'nx')" 0
OK
127.0.0.1:6379> ttl mykey
(integer) 6
锁超时
任何一个锁在被一个线程持有之后都最好再设置一个失效时间,防止业务代码由于异常原因,如宕机等,无法正常释放对应的锁key,造成其他线程无法获取到锁。
当然正常情况下,线程执行完业务之后需要及时 unlock
让其他线程获取到锁,提高程序的并发性能。
可重入锁
可重入锁是指一个锁在被一个线程持有后,在该线程未释放锁前的任何时间内,只要再次访问被该锁锁住的函数区都可以再次进入对应的锁区域。
可重入锁有一个可重入度的概念,即每次重新进入一次该锁的锁住的区域都会递增可重入度,每次退出一个该锁锁住的区域都会递减可重入度,最终释放全部锁后,可重入度为0。
当然可重入度可能并不是无限大的,具体要看实现,这里不做深入讨论。
如何用Redis实现分布式环境下可重入锁
所以,我们的基于Redis的分布式可重入锁的实现需要考虑:
排他性
这个Redis的SET NX已经帮我们解决了,具体的锁持有者身份标识问题需要业务解决,下面会说
锁超时性
这个需要实现代码在获取锁key成功之后同时设置一个失效时间,防止锁key可能存在的异常情况下长时间不被释放的问题
可重入性
即获取锁的线程在未释放(锁未过期)之前,仍然可以继续获取该锁,这个需要结合排他性里说到的锁持有者身份标识来解决,即A获取了锁,那么锁里一定要有A的信息,对上了就可以继续让A获取了
需要注意的是前两点需要保证原子性,防止上锁成功和锁失效时间设置之间的gap过长导致失效时间设置时锁已释放
可以通过lua来实现上锁和设置ttl的原子性操作
-当key不存在的时候,设置value和失效时间,返回成功
-当key已经存在的时候,如果oldValue == value,新老值相同时,也会返回成功
-都不符合时返回失败
local function setnx_or_value_equals(key, value, ttl)
local old = redis.call('GET', key);
if old == nil or old == false then
redis.call('SET', key, value, 'px', ttl, 'nx') -- 设置value后设置了px失效时间并且要求
return 1 -- 返回成功
end
if value == old then
return 1 -- 对比新老value一致则认为是相同的线程,返回成功
end
-- 大部分请求只进行一次GET后即返回
return 0 -- 返回失败
end
上述锁实现可以令一个已经通过setnx_or_value_equals()获取了key锁的线程在该key未失效前可以多次获取对应的的key锁。
需要注意这里的key的value要特别进行设计,防止简单的时间戳的数值造成可能的冲突问题,导致其他线程通过value==old来窃取到锁。
一般在分布式环境中value可以参考机器、进程和线程,尽可能的多维度化value的值从而避免冲突,如: HOST_IP + Process_ID + Thread_Id
本文首次发布于ElseF’s Blog, 作者 @stuartlau
,
转载请保留原文链接.
注意:本文来自ElseF's Blog。本站无法对本文内容的真实性、完整性、及时性、原创性提供任何保证,请您自行验证核实并承担相关的风险与后果!
CoLaBug.com遵循[CC BY-SA 4.0]分享并保持客观立场,本站不承担此类作品侵权行为的直接责任及连带责任。您有版权、意见、投诉等问题,请通过[eMail]联系我们处理,如需商业授权请联系原作者/原网站。