Redis分布式锁从入门到放弃:Redisson源码解密
文章结构
- 分布式锁基础概念
- 为什么需要分布式锁
- 分布式锁的核心要求(互斥、死锁预防、容错等)
- 常见实现方式对比(数据库、ZooKeeper、Redis)
- Redis分布式锁的演进
- SETNX + EXPIRE 的缺陷(非原子性)
- Lua脚本保证原子性
- Redlock算法的争议
- Redisson分布式锁实战
- 基础用法示例
- 看门狗机制解析
- 可重入锁实现原理
- 源码深度解析
- 加锁流程(tryLockInnerAsync)
- 解锁流程(unlockInnerAsync)
- 锁续期(watchdog)
- 生产环境避坑指南
- 锁等待时间设置
- 主从切换问题与MultiLock
- 锁监控与管理
- Redisson锁的局限性
- 时钟漂移问题
- 高并发场景下性能瓶颈
- 其他锁类型对比(ReadWriteLock、SpinLock)
- 最佳实践与替代方案
- 何时使用Redis分布式锁
- 何时考虑ZooKeeper/etcd
- 基于数据库的分布式锁优化
正文内容
Redis分布式锁从入门到放弃:Redisson源码解密
一、分布式锁:系统架构的生死防线
在微服务架构中,当多个服务实例竞争共享资源时,分布式锁成为保证数据一致性的关键组件。合格的分布式锁必须满足:
- 互斥性:同一时刻只有一个客户端持有锁
- 防死锁:持有锁的客户端崩溃后锁能自动释放
- 容错性:部分节点宕机不影响锁服务
- 高性能:低延迟获取锁
二、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脚本保证SETNX
和EXPIRE
的原子性。
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 看门狗机制
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
- 否则返回锁剩余时间
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
- 计数器>0则更新过期时间
- 计数器=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主从异步复制导致锁丢失:
解决方案: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锁的局限性
- 时钟漂移问题:
- 多节点时钟不同步可能导致锁提前释放
- 高并发性能瓶颈:
场景 吞吐量 (ops/sec) 单Redis节点 35,000 Redis集群 65,000 Zookeeper锁 8,000 etcd锁 12,000 - 公平性问题:
- 默认非公平锁可能导致线程饥饿
七、最佳实践与替代方案
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
- 简单场景用数据库锁