怎么用redis lua脚本实现各分布式锁?Redisson各分布式锁怎么实现的?

发布于:2025-09-03 ⋅ 阅读:(20) ⋅ 点赞:(0)

一、基础可重入锁(RLock)

完整Lua脚本

-- 加锁脚本 
if (redis.call('exists', KEYS[1]) == 0) then 
    redis.call('hset', KEYS[1], ARGV[1], 1); 
    redis.call('pexpire', KEYS[1], ARGV[2]); 
    return nil; 
end; 
if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then 
    redis.call('hincrby', KEYS[1], ARGV[1], 1); 
    redis.call('pexpire', KEYS[1], ARGV[2]); 
    return nil; 
end; 
return redis.call('pttl', KEYS[1]); 
 
-- 释放锁脚本 
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then 
    return nil; 
end; 
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -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;

参数说明

  • KEYS[1]: 锁名称(如myLock
  • KEYS[2]: 解锁消息通道(redisson_lock__channel:{myLock}
  • ARGV[1]: 客户端ID(格式:UUID:threadId
  • ARGV[2]: 锁超时时间(默认30,000ms)

全流程

  1. 加锁
    • 锁不存在时:创建Hash结构(hset),存储客户端ID与重入计数(初始1),设置超时(pexpire
    • 锁已存在且为当前客户端:重入计数+1(hincrby),刷新超时
    • 锁被其他客户端占用:返回剩余生存时间(pttl),触发客户端阻塞重试
  2. 看门狗续期
    • 后台线程每10秒检查锁持有者,若存活则执行pexpire重置为30秒,避免业务未完成锁过期
  3. 释放锁
    • 非持有者操作忽略(hexists校验)
    • 重入计数>0时仅减计数并刷新超时
    • 计数归零时删除锁(del),并通过publish通知等待线程竞争

二、读写锁(RReadWriteLock)

读锁加锁脚本

local mode = redis.call('hget', KEYS[1], 'mode'); 
if (mode == false) then 
    redis.call('hset', KEYS[1], 'mode', 'read'); 
    redis.call('hset', KEYS[1], ARGV[1], 1); 
    redis.call('pexpire', KEYS[1], ARGV[2]); 
    return 1; 
end; 
if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[1]) == 1) then 
    local count = redis.call('hincrby', KEYS[1], ARGV[1], 1); 
    redis.call('pexpire', KEYS[1], ARGV[2]); 
    return count; 
end; 
return redis.call('pttl', KEYS[1]);

写锁加锁脚本

local mode = redis.call('hget', KEYS[1], 'mode'); 
if (mode == false) then 
    redis.call('hset', KEYS[1], 'mode', 'write'); 
    redis.call('hset', KEYS[1], ARGV[1], 1); 
    redis.call('pexpire', KEYS[1], ARGV[2]); 
    return nil; 
end; 
if (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[1]) == 1) then 
    redis.call('hincrby', KEYS[1], ARGV[1], 1); 
    redis.call('pexpire', KEYS[1], ARGV[2]); 
    return nil; 
end; 
return redis.call('pttl', KEYS[1]);

流程特点

  • 读锁共享:无锁或已有读锁时直接叠加计数;存在写锁时仅允许持有该写锁的线程重入(写锁降级)
  • 写锁互斥:需确保无任何读写锁存在(mode字段为write且客户端ID匹配)
  • 性能优势:读操作并发量提升

三、红锁(RedLock)

多节点加锁脚本

-- 与RLock加锁脚本相同(每个节点独立执行)

释放脚本

-- 与RLock释放脚本相同(向所有节点广播)

容错流程

  1. 加锁
    • 向≥5个独立Redis节点发送加锁请求
    • 若多数节点(≥ N/2+1)成功且总耗时 < 锁有效期,视为成功
  2. 释放
    • 向所有节点广播删除命令(即使部分节点未响应)
  3. 争议点
    • 时钟漂移风险:节点时钟不同步可能导致锁有效期计算误差(需依赖单调时钟)
    • 官方建议:奇数独立节点部署(N≥5),容忍少数节点故障

四、公平锁(FairLock)

加锁脚本

-- 清理过期等待线程 
while true do 
    local firstThread = redis.call('lindex', KEYS[2], 0); 
    if firstThread == false then break end; 
    local ttl = redis.call('zscore', KEYS[3], firstThread); 
    if ttl == false or tonumber(ttl) < tonumber(ARGV[4]) then 
        redis.call('zrem', KEYS[3], firstThread); 
        redis.call('lpop', KEYS[2]); 
    else break end; 
end; 
 
-- 检查是否可获取锁 
if (redis.call('exists', KEYS[1]) == 0) 
    and (redis.call('llen', 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; 
else 
    local pos = redis.call('lpos', KEYS[2], ARGV[2]); 
    if pos then 
        return redis.call('zscore', KEYS[3], ARGV[2]) - ARGV[1] - ARGV[4]; 
    else 
        redis.call('rpush', KEYS[2], ARGV[2]); 
        redis.call('zadd', KEYS[3], ARGV[4] + ARGV[1], ARGV[2]); 
        return ARGV[1]; 
    end; 
end;

关键参数

  • KEYS[2]: 线程队列(redisson_lock_queue:{myLock}
  • KEYS[3]: 超时有序集合(redisson_lock_timeout:{myLock}
  • ARGV[4]: 当前时间戳

公平性实现

  1. 排队机制:线程通过rpush进入队列,lpop按序获取锁
  2. 超时清理:循环检查队列头部线程是否超时(zscore判断),避免死等
  3. 性能代价:吞吐量降低20%-30%(队列维护开销)

五、联锁(MultiLock)

加锁流程

-- 复用RLock脚本(每个锁独立执行)

释放流程

-- 复用RLock释放脚本(遍历所有锁执行)

全流程

  1. 原子加锁
    • 遍历所有锁尝试获取(tryLock),超时时间均分(总超时/N)
    • 若失败数 > 容忍阈值(默认0),立即释放已获锁
  2. 联锁释放
    • 无论单锁是否成功,均尝试释放所有锁

六、核心设计总结

  1. 原子性保障
    • 所有操作封装为Lua脚本,确保exists/hset/pexpire等命令原子执行
  2. 防死锁机制
    • 默认超时 + 看门狗续期(后台线程保活)
  3. 误删防护
    • 释放锁时校验客户端ID(UUID+线程ID)
  4. 容错方案
    • 红锁容忍少数节点故障,联锁确保多资源原子更新

完整脚本实现参考Redisson源码:


网站公告

今日签到

点亮在社区的每一天
去签到