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

一、基础可重入锁(RLock)

完整Lua脚本

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)

读锁加锁脚本

lua 复制代码
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]);

写锁加锁脚本

lua 复制代码
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)

多节点加锁脚本

lua 复制代码
-- 与RLock加锁脚本相同(每个节点独立执行)

释放脚本

lua 复制代码
-- 与RLock释放脚本相同(向所有节点广播)

容错流程

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

四、公平锁(FairLock)

加锁脚本

lua 复制代码
-- 清理过期等待线程 
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)

加锁流程

lua 复制代码
-- 复用RLock脚本(每个锁独立执行)

释放流程

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

全流程

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

六、核心设计总结

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

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

相关推荐
zcmodeltech2 小时前
智能矿井沙盘模型多系统协同控制系统设计:基于STM32与Modbus RTU的感知-传输-控制一体化方案
服务器·数据库·分布式·stm32·单片机·嵌入式硬件
江晓鱼未暖3 小时前
十七、Redis 核心原理与架构详解
大数据·数据库·数据仓库·redis·缓存·架构
wWYy.3 小时前
基于Raft分布式Kv存储:RPC调用
分布式·rpc
小罗水3 小时前
第13章 Redis 缓存、幂等锁与任务状态
数据库·redis·缓存
笨蛋不要掉眼泪3 小时前
RabbitMQ消息队列:SpringAMQP
java·分布式·rabbitmq
发量惊人的中年网工4 小时前
SD-WAN如何赋能工业物联网?打造工厂网络远程连接和数据传输新方案
运维·网络·分布式·物联网
段一凡-华北理工大学4 小时前
AI推动工业智能化转型~系列文章05:特征工程:工业 AI 的第一生产力
数据库·人工智能·分布式·搜索引擎·特征工程·高炉智能化
Devin~Y5 小时前
从本地生活电商到 AI RAG:互联网大厂 Java 面试场景完整实战
java·spring boot·redis·elasticsearch·spring cloud·kafka·rag
斯普润布特5 小时前
Kafka KRaft 三节点 ARM64 Docker 部署
分布式·kafka
笨蛋不要掉眼泪6 小时前
RabbitMQ消息队列:交换机机制
java·分布式·rabbitmq