前言
前面分析了Redisson可重入锁的原理,主要是通过lua脚本加锁及设置过期时间来保证锁执行的原子性,然后每个线程获取锁会将获取锁的次数+1,释放锁会将当前锁次数-1,如果为0则表示释放锁成功。
可重入原理和JDK中的可重入锁都是一致的。
Redisson公平锁原理
JDK中也有公平锁和非公平锁,所谓公平锁,就是保证客户端获取锁的顺序,跟他们请求获取锁的顺序,是一样的。公平锁需要排队,谁先申请获取这把锁,谁就可以先获取到这把锁,是按照请求的先后顺序来的。
Redisson实现公平锁源码分析
非公平锁使用也很简单:
RLock lock = redisson.getFairLock("anyLock"); lock.lock(); lock.unlock();
核心lua脚本代码:
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { internalLockLeaseTime = unit.toMillis(leaseTime); long currentTime = System.currentTimeMillis(); if (command == RedisCommands.EVAL_LONG) { return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command, // remove stale threads "while true do " + "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" + "if firstThreadId2 == false then " + "break;" + "end; " + "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));" + "if timeout <= tonumber(ARGV[4]) then " + "redis.call('zrem', KEYS[3], firstThreadId2); " + "redis.call('lpop', KEYS[2]); " + "else " + "break;" + "end; " + "end;" + "if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) " + "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " + "redis.call('lpop', KEYS[2]); " + "redis.call('zrem', KEYS[3], ARGV[2]); " + "redis.call('hset', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + "local firstThreadId = redis.call('lindex', KEYS[2], 0); " + "local ttl; " + "if firstThreadId ~= false and firstThreadId ~= ARGV[2] then " + "ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);" + "else " + "ttl = redis.call('pttl', KEYS[1]);" + "end; " + "local timeout = ttl + tonumber(ARGV[3]);" + "if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " + "redis.call('rpush', KEYS[2], ARGV[2]);" + "end; " + "return ttl;", Arrays.<Object>asList(getName(), threadsQueueName, timeoutSetName), internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime, currentTime); } throw new IllegalArgumentException(); }
KEYS/ARGV参数分析
KEYS = Arrays.asList(getName(), threadsQueueName, timeoutSetName)
ARGV = internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime, currentTime
模拟不同线程获取锁步骤
lua脚本源码分析
客户端A thread01 加锁分析
thread01 在10:00:00 执行加锁逻辑,下面开始一点点分析lua脚本执行代码:
"while true do " + "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" + "if firstThreadId2 == false then " + "break;"
lindex redisson_lock_queue:{anyLock} 0,就是从redisson_lock_queue:{anyLock}这个队列中弹出来第一个元素,刚开始,队列是空的,所以什么都获取不到,此时就会直接退出while true死循环
"if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) " + "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " + "redis.call('lpop', KEYS[2]); " + "redis.call('zrem', KEYS[3], ARGV[2]); " + "redis.call('hset', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " +
这段代码判断逻辑的意思是:
那么这个条件整体就可以成立了 anyLock和队列,都是不存在的,所以这个条件肯定会成立。接着执行if中的具体逻辑:
返回一个nil,在外层代码中,就会认为是加锁成功,此时就会开启一个watchdog看门狗定时调度的程序,每隔10秒判断一下,当前这个线程是否还对这个锁key持有着锁,如果是,则刷新锁key的生存时间为30000毫秒 (看门狗的具体流程上一篇文章有讲述)
客户端B thread02 加锁分析
此时thread01 已经获取到了锁,如果thread02 在10:00:10分来执行加锁逻辑,具体的代码逻辑是怎样执行的呢?
"while true do " + "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" + "if firstThreadId2 == false then " + "break;"
进入while true死循环,lindex redisson_lock_queue:{anyLock} 0,获取队列的第一个元素,此时队列还是空的,所以获取到的是false,直接退出while true死循环
"if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) " + "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " + "redis.call('lpop', KEYS[2]); " + "redis.call('zrem', KEYS[3], ARGV[2]); " + "redis.call('hset', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " +
此时anyLock这个锁key已经存在了,说明已经有人加锁了,这个条件首先就肯定不成立了;
接着往下执行,看下另外的逻辑:
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " +
判断一下,此时这个第二个客户端是UUID_02,threadId_02,此时会判断一下,hexists anyLock UUID_02:threadId_02,判断一下在anyLock这个map中,是否存在UUID_02:threadId_02这个key?这个条件也不成立
继续执行后续代码:
"local firstThreadId = redis.call('lindex', KEYS[2], 0); " + "local ttl; " + "if firstThreadId ~= false and firstThreadId ~= ARGV[2] then " + "ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);" + "else " + "ttl = redis.call('pttl', KEYS[1]);" + "end; " + "local timeout = ttl + tonumber(ARGV[3]);" + "if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " + "redis.call('rpush', KEYS[2], ARGV[2]);" + "end; " + "return ttl;",
tonumber()
是lua中自带的函数,tonumber会尝试将它的参数转换为数字。
lindex redisson_lock_queue:{anyLock} 0,从队列中获取第一个元素,此时队列是空的,所以什么都不会有
因为我们是在10:00:10 分请求的,因为anyLock默认过期时间是30s,所以在thread02请求的时候ttl还剩下20s
ttl = pttl anyLock = 20000毫秒,获取anyLock剩余的生存时间,ttl假设这里就被设置为了20000毫秒
timeout = ttl + 当前时间 + 5000毫秒 = 20000毫秒 + 10:00:00 + 5000毫秒 = 10:00:25
接着执行: zadd redisson_lock_timeout:{anyLock} 10:00:25 UUID_02:threadId_02
在set集合中插入一个元素,元素的值是UUID_02:threadId_02,他对应的分数是10:00:25(会用这个时间的long型的一个时间戳来表示这个时间,时间越靠后,时间戳就越大),sorted set,有序set集合,他会自动根据你插入的元素的分数从小到大来进行排序
继续执行: rpush redisson_lock_queue:{anyLock} UUID_02:theadId_02
这个指令就是将UUID_02:threadId_02,插入到队列的头部去
返回的是ttl,也就是anyLock剩余的生存时间,如果拿到的返回值是ttl是一个数字的话,那么此时客户端B而言就会进入一个while true的死循环,每隔一段时间都尝试去进行加锁,重新执行这段lua脚本
简单画图总结如下:
客户端C thread03 加锁分析
此时thread03 在10:00:15来加锁,分析一下执行原理:
"while true do " + "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" + "if firstThreadId2 == false then " + "break;" + "end; " + "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));" + "if timeout <= tonumber(ARGV[4]) then " + "redis.call('zrem', KEYS[3], firstThreadId2); " + "redis.call('lpop', KEYS[2]); " + "else " + "break;" + "end; " + "end;"
while true死循环,lindex redisson_lock_queue:{anyLock} 0,获取队列中的第一个元素,UUID_02:threadId_02,代表的是这个客户端02正在队列里排队
zscore redisson_lock_timeout:{anyLock} UUID_02:threadId_02,从有序集合中获取UUID_02:threadId_02对应的分数,timeout = 10:00:25
判断:timeout <= 10:00:15?,这个条件不成立,退出死循环
"local firstThreadId = redis.call('lindex', KEYS[2], 0); " + "local ttl; " + "if firstThreadId ~= false and firstThreadId ~= ARGV[2] then " + "ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);" + "else " + "ttl = redis.call('pttl', KEYS[1]);" + "end; " + "local timeout = ttl + tonumber(ARGV[3]);" + "if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " + "redis.call('rpush', KEYS[2], ARGV[2]);" + "end; " + "return ttl;",
firstThreadId获取到的是队列中的第一个元素:UUID_02:thread_02
ttl = 10:00:25 - 10:00:15 = 5000毫秒 timeout = 5000毫秒 + 10:00:15 + 5000毫秒 = 10:00:30
将客户端C放入到对列和有序集合中: zadd redisson_lock_timeout:{anyLock} 10:00:30 UUID_03:threadId_03 rpush redisson_lock_queue:{anyLock} UUID_03:theadId_03
最终执行完后 如下图:
Redisson依次加锁逻辑
上面已经知道了,多个线程加锁过程中实际会进行排队,根据加锁的时间来作为获取锁的优先级,如果此时客户端A释放了锁,来看下客户端B、C是如果获取锁的
当客户端A释放锁 ;客户端B请求获取锁
直接看核心逻辑:
+ "if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) " + "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " + "redis.call('lpop', KEYS[2]); " + "redis.call('zrem', KEYS[3], ARGV[2]); " + "redis.call('hset', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " +
if中的判断: exists anyLock 是否不存在,此时客户端A已经释放锁,所以这个条件成立。
然后判断队列不存在,或者队列中第一个元素为空,此时条件不成立,但是后面是or关联的判断,接着判断队列中的第一个元素是否为当前请求的UUID_02:threadId_02, 如果判断成功则开始加锁。
这里就是公平锁依次加锁的核心逻辑。
原文: