当前位置: 首页 > news >正文

Redis分布式锁从入门到放弃:Redisson源码解密

文章结构

  1. 分布式锁基础概念
    • 为什么需要分布式锁
    • 分布式锁的核心要求(互斥、死锁预防、容错等)
    • 常见实现方式对比(数据库、ZooKeeper、Redis)
  2. Redis分布式锁的演进
    • SETNX + EXPIRE 的缺陷(非原子性)
    • Lua脚本保证原子性
    • Redlock算法的争议
  3. Redisson分布式锁实战
    • 基础用法示例
    • 看门狗机制解析
    • 可重入锁实现原理
  4. 源码深度解析
    • 加锁流程(tryLockInnerAsync)
    • 解锁流程(unlockInnerAsync)
    • 锁续期(watchdog)
  5. 生产环境避坑指南
    • 锁等待时间设置
    • 主从切换问题与MultiLock
    • 锁监控与管理
  6. Redisson锁的局限性
    • 时钟漂移问题
    • 高并发场景下性能瓶颈
    • 其他锁类型对比(ReadWriteLock、SpinLock)
  7. 最佳实践与替代方案
    • 何时使用Redis分布式锁
    • 何时考虑ZooKeeper/etcd
    • 基于数据库的分布式锁优化

正文内容

Redis分布式锁从入门到放弃:Redisson源码解密

一、分布式锁:系统架构的生死防线

请求资源
同时请求
无锁
有锁
用户1
共享资源
用户2
数据冲突
顺序访问

在微服务架构中,当多个服务实例竞争共享资源时,分布式锁成为保证数据一致性的关键组件。合格的分布式锁必须满足:

  1. 互斥性:同一时刻只有一个客户端持有锁
  2. 防死锁:持有锁的客户端崩溃后锁能自动释放
  3. 容错性:部分节点宕机不影响锁服务
  4. 高性能:低延迟获取锁

二、Redis分布式锁的进化史

2.1 原始方案:SETNX的致命缺陷
// 错误示范(非原子操作)
Boolean result = jedis.setnx("lock_key", "1");
if (result) {jedis.expire("lock_key", 30); // 可能宕机导致死锁
}

这种方案存在原子性问题:如果设置过期时间前进程崩溃,将导致死锁。

2.2 Lua脚本方案(Redis 2.6+)
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 thenreturn redis.call('pexpire', KEYS[1], ARGV[2])
elsereturn 0
end

通过Lua脚本保证SETNXEXPIRE的原子性。

2.3 Redlock算法争议

Redis作者提出的Redlock算法:

sequenceDiagramClient->>+Redis1: SET key random_value NX PX 30000Client->>+Redis2: SET key random_value NX PX 30000Client->>+Redis3: SET key random_value NX PX 30000Note over Client: 获取多数节点锁Client->>-Redis1: DEL keyClient->>-Redis2: DEL keyClient->>-Redis3: DEL key

但分布式系统专家Martin Kleppmann指出其存在时钟漂移问题,引发著名辩论。

三、Redisson分布式锁实战

3.1 基础用法
// 获取锁
RLock lock = redisson.getLock("orderLock");
lock.lock();
try {// 业务逻辑
} finally {lock.unlock();
}
3.2 看门狗机制
加锁
是否设置leaseTime?
不启动看门狗
启动看门狗线程
每10秒续期锁超时时间
3.3 可重入锁实现

Redisson通过计数器实现可重入:

// 伪代码
if (redis.call('exists', KEYS[1]) == 0) thenredis.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) thenredis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return nil;
end;
return redis.call('pttl', KEYS[1]);

四、源码深度解析

4.1 加锁流程(tryLockInnerAsync)
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,"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]);",Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}

核心逻辑

  1. 检查锁是否存在
  2. 不存在则创建哈希结构并初始化计数器
  3. 存在且是当前线程持有则重入计数+1
  4. 否则返回锁剩余时间
4.2 解锁流程(unlockInnerAsync)
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));
}

关键步骤

  1. 检查当前线程是否持有锁
  2. 计数器-1
  3. 计数器>0则更新过期时间
  4. 计数器=0则删除锁并发布解锁消息
4.3 看门狗实现(watchdog)
private void renewExpiration() {Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {public void run(Timeout timeout) {// 续期逻辑RFuture<Boolean> future = renewExpirationAsync(threadId);future.onComplete((res, e) -> {if (e != null) {log.error("Can't update lock", e);return;}if (res) {// 递归调用实现周期性续期renewExpiration();}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}

核心机制

  • leaseTime/3(默认10秒)执行一次续期
  • 通过异步回调实现周期性任务

五、生产环境避坑指南

5.1 锁等待时间设置
// 错误:未设置超时可能导致永久阻塞
lock.lock();
// 正确:设置最长等待时间
boolean res = lock.tryLock(5, 10, TimeUnit.SECONDS);
5.2 主从切换问题

Redis主从异步复制导致锁丢失:

ClientMasterSlaveNewMasterClientB获取锁成功异步复制锁数据返回成功主节点宕机晋升获取相同锁成功 → 互斥失效!ClientMasterSlaveNewMasterClientB

解决方案:MultiLock(多主冗余)

RLock lock1 = redisson1.getLock("lock");
RLock lock2 = redisson2.getLock("lock");
RLock lock3 = redisson3.getLock("lock");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
lock.lock();
try {// 业务逻辑
} finally {lock.unlock();
}
5.3 锁监控

通过Redisson的监控接口:

Config config = new Config();
config.setLockWatchdogTimeout(30000); // 默认30秒
RedissonClient redisson = Redisson.create(config);

六、Redisson锁的局限性

  1. 时钟漂移问题
    • 多节点时钟不同步可能导致锁提前释放
    本地时间快
    获取相同锁
    节点1
    锁提前过期
    节点2
    互斥失效
  2. 高并发性能瓶颈
    场景吞吐量 (ops/sec)
    单Redis节点35,000
    Redis集群65,000
    Zookeeper锁8,000
    etcd锁12,000
  3. 公平性问题
    • 默认非公平锁可能导致线程饥饿

七、最佳实践与替代方案

7.1 何时使用Redis分布式锁
  • 适用
    • 对性能要求高(TPS > 10,000)
    • 允许极低概率的锁失效
  • 不适用
    • 金融交易等强一致性场景
    • 跨资源事务管理
7.2 替代方案对比
方案性能一致性保障复杂性
Redis
ZooKeeper
etcd中高
数据库行锁
7.3 数据库锁优化方案
-- 基于PostgreSQL的阻塞锁
SELECT pg_advisory_xact_lock('order_lock');
-- 业务逻辑
SELECT pg_advisory_xact_unlock('order_lock');

结语:分布式锁的选择之道

黄金法则

  • 追求性能选Redis(接受小概率失效)
  • 追求强一致选ZooKeeper/etcd
  • 简单场景用数据库锁
http://www.lryc.cn/news/583442.html

相关文章:

  • 玛哈特网板矫平机:精密矫平金属开平板的利器
  • 掌握 Winget 安装:从 Microsoft Store 到 winget-install 脚本的完整方案
  • 虚幻引擎5 GAS开发俯视角RPG游戏 #5-8:倾听属性变化
  • 基于Matlab多特征融合的可视化指纹识别系统
  • 141-CEEMDAN-VMD-Transformer-BiLSTM-ABKDE多变量区间预测模型!
  • 让AI绘图更可控!ComfyUI-Cosmos-Predict2基础使用指南
  • Fluent许可配置常见问题
  • Android网络层架构:统一错误处理的问题分析到解决方案与设计实现
  • 编写产品需求文档:黄历日历小程序
  • 暑假读书笔记第五天
  • 自然语言处理中probe探测是什么意思。
  • Oracle 数据库升级踩坑:DBLink ORA-02019 问题解决思路
  • 编写Shell脚本开放端口
  • 函数-3-日期函数
  • 【Linux | 网络】socket编程 - 使用TCP实现服务端向客户端提供简单的服务
  • 记忆管理框架MemOS——在时序推理上较OpenAI提升159%
  • [IMX][UBoot] 13.Linux 内核源码目录分析
  • 脑电分析入门指南:信号处理、特征提取与机器学习
  • 【前端】异步任务风控验证与轮询机制技术方案(通用笔记版)
  • 暑假的挣扎与自我梳理
  • 【计算机三级网络】——IP校园网大题(第二道):路由代码填空
  • 【HarmonyOS6】获取华为用户信息
  • 【Linux】·C++缺陷和思考
  • 【05】MFC入门到精通——MFC 为对话框中的控件添加变量 和 数据交换和检验
  • Laravel 动态生成 PDF:基于 KnpSnappy 实现多公司页眉页脚差异化配置
  • Java零基础笔记08(Java编程核心:面向对象编程高级 {继承、多态})
  • uniapp小程序无感刷新token
  • Docker 高级管理--容器通信技术与数据持久化
  • [论文阅读] 软件工程 | 一篇关于开源许可证管理的深度综述
  • 图像处理中的模板匹配:原理与实现