说说 Redisson 分布式锁的原理?

Sherwin.Wei Lv7

说说 Redisson 分布式锁的原理?

回答重点

Redisson 是基于 Redis 实现的分布式锁,实际上是使用 Redis 的原子操作来确保多线程、多进程或多节点系统中,只有一个线程能获得锁,避免并发操作导致的数据不一致问题。

1)锁的获取

  • Redisson 使用 Lua 脚本,利用 exists + hexists + hincrby 命令来保证只有一个线程能成功设置键(表示获得锁)。
  • 同时,Redisson 会通过 pexpire 命令为锁设置过期时间,防止因宕机等原因导致锁无法释放(即死锁问题)。

2)锁的续期

  • 为了防止锁在持有过程中过期导致其他线程抢占锁,Redisson 实现了锁自动续期的功能。持有锁的线程会定期续期,即更新锁的过期时间,确保任务没有完成时锁不会失效。

3)锁的释放

  • 锁释放时,Redisson 也是通过 Lua 脚本保证释放操作的原子性。利用 hexists + del 确保只有持有锁的线程才能释放锁,防止误释放锁的情况。
  • Lua 脚本同时利用 publish 命令,广播唤醒其它等待的线程。

4)可重入锁

  • Redisson 支持可重入锁,持有锁的线程可以多次获取同一把锁而不会被阻塞。具体是利用 Redis 中的哈希结构,哈希中的 key 为线程 ID,如果重入则 value +1,如果释放则 value -1,减到 0 说明锁被释放了,则 del 锁。

扩展知识

Redisson 分布式锁 lua 相关源码解析

加锁的代码:

1
2
3
4
5
6
7
8
9
10
11
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return commandExecutor.syncedEval(getRawName(), LongCodec.INSTANCE, command,
"if ((redis.call('exists', KEYS[1]) == 0) " +
"or (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; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}

hincrby: 如果哈希表的 key 不存在,会创建新的哈希表并执行 hincrby 命令

if + or 的逻辑可能不好理解,我把上述的 lua 脚本拆开来看(以下脚本的效果同上):

1
2
3
4
5
6
7
8
9
10
11
12
13
if (redis.call('exists', KEYS[1]) == 0) then 
redis.call('hincrby', 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;

return redis.call('pttl', KEYS[1]);

上述的 lua 脚本含义如下

1)若锁不存在,则新增锁,并设置锁重入计数为 1 且设置锁过期时间

1
2
3
4
5
if (redis.call('exists', KEYS[1]) == 0) then 
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;

2)若锁存在,且唯一标识(线程id相关)也匹配,则当前加锁请求为锁的重入请求,哈希的重入计数+1,并再次设置锁过期时间

1
2
3
4
5
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;

3)若锁存在,但唯一标识不匹配,则说明锁被其他线程占用,当前线程无法解锁,直接返回锁剩余过期时间(pttl)

1
return redis.call('pttl', KEYS[1]);

上述脚本中,几个参数的含义如下:

1
2
3
KEYS[1] = Collections.singletonList(getRawName())
ARGV[1] = unit.toMillis(leaseTime)
ARGV[2] = getLockName(threadId)

释放锁的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

上述的 lua 脚本含义如下

1)若锁不存在,直接返回不需要解锁

1
2
3
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then 
return nil;
end;

2)若锁存在,且唯一标识(线程id相关)也匹配,计数 - 1,如果此时计数还大于 0 ,再次设置锁过期时间

1
2
3
4
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;

3)若计数小于等于 0 ,则删除 key ,且通过广播通知其它等待锁的线程,此时释放锁了

1
2
3
4
else
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;

上述脚本中,几个参数的含义如下:

Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId)

1
2
3
4
5
KEYS[1] = getRawName()
KEYS[2] = getChannelName()
ARGV[1] = LockPubSub.UNLOCK_MESSAGE
ARGV[2] = internalLockLeaseTime
ARGV[3] = getLockName(threadId)

Redisson 的锁类型

  • 公平锁:与可重入锁类似,公平锁确保多个线程按请求锁的顺序获得锁。
  • 读写锁:支持读写分离。多个线程可以同时获得读锁,而写锁是独占的。
  • 信号量与可数锁:允许多个线程同时持有锁,适用于资源的限流和控制。

Redis分布式锁

关于上述的 SETNX、Lua 脚本释放锁等,看参考下面这个面试题

锁过期和网络分区补充

  • 锁过期问题:在 Redis 中,通过 SETNX 获取的锁通常带有过期时间。如果业务逻辑执行时间超过锁的过期时间,可能会出现锁被其他线程重新获取的问题。Redisson 通过锁的续期机制解决了这个问题。
  • 网络分区问题:Redis 是基于主从复制的,在主从切换或发生网络分区时,锁可能出现不一致的情况(如主节点锁还存在,副节点却因为主从切换失去了锁)。为解决这一问题,业界提出了 Redlock 算法,Redisson 也可以使用此算法确保锁的高可用性。
Comments