1、非公平锁
1、底层结构:map
2、实现方式是lua脚本+map
3、实现逻辑:map不存在,创建map并创建key-value,加锁成功,若map存在,判断key是否为请当前线程,是当前线程则value+1,value+1用于支持可重入,如果key不是当前线程,则返回过期时间

2、公平锁
脚本参数说明
-
KEYS1:锁的哈希表(存储持有锁的线程ID及其重入次数)
-
KEYS2:列表,作为 FIFO 等待队列(存储等待线程的ID)
-
KEYS3:有序集合,存储每个等待线程的超时时间戳(score = 超时时间点)
-
ARGV1:锁的租约时间(毫秒)
-
ARGV2:当前线程的唯一标识ID
-
ARGV3:线程的等待时间(毫秒,即每个线程在队列中最多等多久)
-
ARGV4:当前时间戳(毫秒)
Lua
"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 " +
// remove the item from the queue and timeout set
// NOTE we do not alter any other timeout
"redis.call('zrem', KEYS[3], firstThreadId2);" +
"redis.call('lpop', KEYS[2]);" +
"else " +
"break;" +
"end;" +
"end;" +
// check if the lock can be acquired now
"if (redis.call('exists', KEYS[1]) == 0) " +
"and ((redis.call('exists', KEYS[2]) == 0) " +
"or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
// remove this thread from the queue and timeout set
"redis.call('lpop', KEYS[2]);" +
"redis.call('zrem', KEYS[3], ARGV[2]);" +
// decrease timeouts for all waiting in the queue
"local keys = redis.call('zrange', KEYS[3], 0, -1);" +
"for i = 1, #keys, 1 do " +
"redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]);" +
"end;" +
// acquire the lock and set the TTL for the lease
"redis.call('hset', KEYS[1], ARGV[2], 1);" +
"redis.call('pexpire', KEYS[1], ARGV[1]);" +
"return nil;" +
"end;" +
// check if the lock is already held, and this is a re-entry
"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;" +
// the lock cannot be acquired
// check if the thread is already in the queue
"local timeout = redis.call('zscore', KEYS[3], ARGV[2]);" +
"if timeout ~= false then " +
// the real timeout is the timeout of the prior thread
// in the queue, but this is approximately correct, and
// avoids having to traverse the queue
"return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]);" +
"end;" +
// add the thread to the queue at the end, and set its timeout in the timeout set to the timeout of
// the prior thread in the queue (or the timeout of the lock if the queue is empty) plus the
// threadWaitTime
"local lastThreadId = redis.call('lindex', KEYS[2], -1);" +
"local ttl;" +
"if lastThreadId ~= false and lastThreadId ~= ARGV[2] then " +
"ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);" +
"else " +
"ttl = redis.call('pttl', KEYS[1]);" +
"end;" +
"local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);" +
"if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
"redis.call('rpush', KEYS[2], ARGV[2]);" +
"end;" +
"return ttl;"
第1步:清理队列中已超时的等待线程
Lua
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
-
循环检查队列头部 的线程是否已超时(
timeout <= 当前时间)。 -
如果超时,则将其从等待队列(KEYS2)和超时集合(KEYS3)中移除。
-
一旦遇到头部线程未超时,停止清理。
👉 作用:防止已超时的线程长期占据队列位置,影响后续线程获取锁。
第2步:尝试获取锁(锁空闲且队列允许)
Lua
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]);
-- 减少队列中剩余所有等待线程的超时时间
local keys = redis.call('zrange', KEYS[3], 0, -1);
for i = 1, #keys, 1 do
redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]);
end;
-- 获取锁并设置租约时间
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end
条件 :锁不存在(空闲) 且 (等待队列为空 或 队列头正是当前线程)。
-
如果当前线程就是队头,将其从队列和超时集合中移除(表明它即将获得锁)。
-
然后,遍历超时集合中的所有剩余线程,减少它们的超时时间 (
zincrby负值)。这是因为队列中前面的线程已获得锁,后面的线程应相应减少等待时间(公平锁中的时间补偿)。 -
最后,通过
hset和pexpire将锁分配给当前线程,返回nil表示获取成功。
第3步:处理锁重入
Lua
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
-
如果当前线程已经持有该锁(哈希表中存在该线程ID),则重入计数加1,并刷新锁的过期时间。
👉 支持可重入锁,同一线程可多次获取锁而不会死锁。
第4步:当前线程已在等待队列中
Lua
local timeout = redis.call('zscore', KEYS[3], ARGV[2]);
if timeout ~= false then
return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]);
end
-
如果当前线程已经在等待队列中(通过
zscore检查),则直接返回剩余等待时间 。👉 公式:
超时时间戳 - 等待时间 - 当前时间。客户端可根据此值决定继续等待还是放弃。
第5步:将当前线程加入等待队列
Lua
local lastThreadId = redis.call('lindex', KEYS[2], -1);
local ttl;
if lastThreadId ~= false and lastThreadId ~= ARGV[2] then
ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);
else
ttl = redis.call('pttl', KEYS[1]);
end
local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);
if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then
redis.call('rpush', KEYS[2], ARGV[2]);
end
return ttl;
-
先获取队列最后一个线程ID(即队尾)。
-
计算当前线程的 基准等待时间
ttl:-
如果队列非空且最后一个线程不是自己(正常情况),
ttl= 最后一个线程的超时时间戳 - 当前时间。 -
否则(队列空或最后一个线程是自己),
ttl= 锁当前的剩余时间(pttl)。
-
-
然后计算出当前线程的超时时间戳:
ttl + 等待时间 + 当前时间。 -
将当前线程加入超时集合(
zadd)和队列尾部(rpush)。 -
最后返回
ttl,告诉客户端需要等待多久(毫秒)。
总结
| 返回值 | 含义 |
|---|---|
nil |
成功获取锁(首次获取或重入) |
| 正数(毫秒) | 无法获取锁,返回需要等待的时间 |
| 负数(或0) | 等待超时或立即失败(由调用方解释) |
该脚本实现了公平、可重入、带超时的分布式锁,所有操作在 Redis 中原子执行,避免了竞态条件。