前言
在微服务架构中,多个服务实例同时操作共享资源(如库存扣减、订单创建、定时任务防重)时,单机锁(synchronized、ReentrantLock)已无法满足互斥性要求,分布式锁成为解决这一问题的核心组件。Redis 凭借其高性能、低延迟和丰富的数据结构,成为最主流的分布式锁实现方案。
本文将从演进视角出发,系统讲解 Redis 分布式锁的三种核心形态、不同 Redis 架构下的锁表现、与其他分布式锁方案的对比,并给出生产环境的选型决策树和避坑指南。
一、Redis 分布式锁的演进历程
Redis 分布式锁的发展是一个不断解决问题、权衡性能与可靠性的过程,从最基础的 SET NX 命令,到 Redisson 的看门狗机制,再到 Redlock 的多节点共识,每一步都针对前一阶段的痛点进行了优化。
1.1 基础版:SET NX EX(原子命令)
Redis 2.6.12 版本之前,分布式锁的实现存在严重的原子性问题。开发者需要先执行 SETNX 命令加锁,再执行 EXPIRE 命令设置过期时间,这两条命令之间如果服务宕机,锁将永远无法释放,导致死锁。
Redis 2.6.12 版本对 SET 命令进行了增强,支持 NX 和 EX 选项在单条命令中执行,彻底解决了加锁和过期时间设置的原子性问题。
核心命令
java
# 加锁:key不存在时设置,同时设置30秒过期时间
SET lock_key unique_value NX EX 30
# 释放锁:Lua脚本保证"判断+删除"原子性
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
Java 代码实现
java
@Service
public class RedisLockService {
@Resource
private StringRedisTemplate redisTemplate;
private static final String LOCK_KEY = "stock:lock";
private static final long LOCK_EXPIRE = 30; // 30秒过期
private static final String UNLOCK_LUA =
"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
/**
* 加锁:SET NX EX 原子命令 + 唯一值
*/
public boolean lock(String uniqueValue) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, uniqueValue, LOCK_EXPIRE, TimeUnit.SECONDS)
);
}
/**
* 解锁:Lua脚本原子判断+删除(防误删)
*/
public boolean unlock(String uniqueValue) {
Long result = redisTemplate.execute(
new DefaultRedisScript<>(UNLOCK_LUA, Long.class),
Collections.singletonList(LOCK_KEY),
uniqueValue
);
return result != null && result == 1;
}
}
核心问题
- 业务超时导致锁误删:如果业务执行时间超过锁过期时间,锁会自动释放,其他线程获取锁后,原线程执行完会误删新线程的锁。
- 不可重入:同一线程无法多次获取同一把锁,嵌套调用会导致死锁。
- 主从切换丢锁:Redis 主从架构中,主节点宕机后,从节点提升为主,锁数据可能未同步,导致多个客户端同时持有锁。
- 非公平:所有等待锁的客户端同时争抢,可能导致某些客户端永远拿不到锁。
1.2 进阶版:Redisson(生产环境首选)
Redisson 是 Redis 官方推荐的 Java 分布式锁实现,它封装了所有底层细节,提供了自动续期、可重入、公平锁、读写锁等高级特性,彻底解决了原生 SET NX EX 的所有问题。
核心特性:看门狗(WatchDog)机制
看门狗机制是 Redisson 最核心的创新,它解决了业务执行时间不确定导致的锁提前释放问题。
原理 :获取锁成功后,启动一个后台定时任务,每隔lockWatchdogTimeout/3秒(默认 10 秒)自动将锁续期为 30 秒。业务执行完主动释放锁时,会取消续期任务;如果服务宕机(如Full GC等),看门狗也会随之消失,锁 30 秒后自动过期。
源码核心逻辑:
java
// Redisson 看门狗核心逻辑(简化版)
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
entry.setThreadId(threadId);
// 定时任务:每 internalLockLeaseTime / 3 执行一次
Timeout task = commandExecutor.getConnectionManager()
.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// 执行续期命令
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
return;
}
if (res) {
// 续期成功,再次调度
scheduleExpirationRenewal(threadId);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}
// 续期 Lua 脚本
private String renewScript =
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
" redis.call('pexpire', KEYS[1], ARGV[1]); " +
" return 1; " +
"end; " +
"return 0;";
Redisson 支持的锁类型
Redisson 提供了多种锁类型,满足不同业务场景的需求:
1. 可重入锁(RLock)
可重入锁允许同一线程多次获取同一把锁,Redisson 使用 Hash 结构存储锁信息,key 为锁名,field 为 "客户端 UUID: 线程 ID",value 为重入次数。
代码示例:
java
RLock lock = redisson.getLock("order:pay:lock");
lock.lock(); // 第一次获锁
try {
// 业务逻辑
lock.lock(); // 可重入
try {
// 嵌套业务逻辑
} finally {
lock.unlock(); // 重入次数-1
}
} finally {
lock.unlock(); // 重入次数归零,真正释放锁
}
2. 公平锁(RFairLock)
公平锁按照请求顺序获取锁,避免 "插队" 现象。Redisson 基于 Redis 的 ZSet(有序集合)实现等待队列,Score 为请求时间戳,Value 为线程标识。
代码示例:
java
RLock fairLock = redisson.getFairLock("ticket:fair:lock");
fairLock.lock();
try {
// 业务逻辑
} finally {
fairLock.unlock();
}
3. 读写锁(RReadWriteLock)
读写锁实现了 "读读共享、读写互斥、写写互斥" 的特性,适用于读多写少的场景,能显著提高并发性能。
代码示例:
java
RReadWriteLock rwLock = redisson.getReadWriteLock("product:info:lock");
// 读锁:多个线程可同时获取
rwLock.readLock().lock();
try {
// 读取商品信息
} finally {
rwLock.readLock().unlock();
}
// 写锁:互斥获取
rwLock.writeLock().lock();
try {
// 更新商品信息
} finally {
rwLock.writeLock().unlock();
}
4. 联锁(MultiLock)
联锁需要同时获取多个子锁才算加锁成功,只要有一个子锁获取失败,所有已获取的子锁会自动释放。适用于需要同时锁定多个资源的场景。
代码示例:
java
RLock lock1 = redisson.getLock("order:123");
RLock lock2 = redisson.getLock("stock:456");
RLock lock3 = redisson.getLock("user:789");
MultiLock multiLock = new MultiLock(lock1, lock2, lock3);
multiLock.lock();
try {
// 同时操作订单、库存和用户信息
} finally {
multiLock.unlock();
}
1.3 高级版:Redlock 算法(多节点强一致性)
Redisson 单节点锁虽然解决了大部分问题,但在 Redis 主从架构中,主节点宕机后,从节点提升为主,锁数据可能未同步,导致多个客户端同时持有锁。为了解决这个问题,Redis 之父 Antirez 提出了 Redlock 算法。
算法原理
Redlock 算法基于 "多数派共识" 思想,向 N 个独立的 Redis 节点(无主从关系)请求锁,只有获得 ** 多数派(N/2+1)** 节点的锁,且总耗时小于锁过期时间,才算加锁成功。
算法流程
- 获取当前时间戳(毫秒)
- 依次向 N 个独立的 Redis 节点请求锁,使用相同的 key 和随机 value,设置超时时间(远小于锁过期时间)
- 计算获取锁的总耗时
- 判断是否成功:成功节点数 >= N/2+1 且 总耗时 < 锁过期时间
- 成功:锁有效时间 = 原过期时间 - 总耗时;失败:向所有节点释放锁
Redisson Redlock 代码实现
java
public class RedlockDemo {
private static RedissonClient redissonClient1;
private static RedissonClient redissonClient2;
private static RedissonClient redissonClient3;
static {
// 节点1
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.1.100:6379");
redissonClient1 = Redisson.create(config1);
// 节点2
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://192.168.1.101:6379");
redissonClient2 = Redisson.create(config2);
// 节点3
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://192.168.1.102:6379");
redissonClient3 = Redisson.create(config3);
}
public static void redlockDemo() throws InterruptedException {
// 获取多个节点的锁对象
RLock lock1 = redissonClient1.getLock("order:123");
RLock lock2 = redissonClient2.getLock("order:123");
RLock lock3 = redissonClient3.getLock("order:123");
// 创建Redlock(多节点锁)
RLock redlock = redissonClient1.getRedLock(lock1, lock2, lock3);
// 尝试获取锁
boolean locked = redlock.tryLock(5, 30, TimeUnit.SECONDS);
if (locked) {
try {
System.out.println("Redlock获取成功,执行业务...");
// 业务逻辑
} finally {
redlock.unlock();
System.out.println("Redlock释放");
}
} else {
System.out.println("Redlock获取失败");
}
}
}
核心争议
Redlock 算法自提出以来就引发了业界的激烈争论,《Designing Data-Intensive Applications》作者 Martin Kleppmann 对其提出了严厉批评:
- 时钟漂移问题:Redlock 强依赖系统时钟同步,如果某个节点发生时钟漂移,锁可能提前过期,导致多个客户端同时持有锁。
- 网络延迟问题:网络延迟可能导致客户端在锁过期后才收到加锁成功的响应,从而误判自己持有锁。
- GC 停顿问题:Java 应用的 Full GC 可能导致线程停顿数秒,锁过期后其他客户端获取锁,原线程恢复后继续执行,导致数据不一致。
Antirez 反驳称,时钟漂移可以通过 NTP 服务控制在毫秒级,Redlock 在正常的工程环境下是安全的,且没有任何分布式锁方案是绝对完美的。
二、不同 Redis 架构下的分布式锁表现
Redis 分布式锁的可靠性不仅取决于实现方式,还与 Redis 的部署架构密切相关。不同的 Redis 架构在性能、可用性和一致性方面各有优劣。
2.1 单节点 Redis
架构特点:只有一个 Redis 节点,所有读写操作都在该节点上执行。
锁表现:
- 优点:实现简单、性能极高(约 5 万 QPS)、无主从同步延迟问题
- 缺点:存在单点故障,节点宕机后整个分布式锁机制失效
适用场景:测试环境、非核心业务、并发量较低的场景
2.2 主从 / 哨兵模式
架构特点:一主多从,主节点负责写操作,从节点负责读操作;哨兵节点监控主从状态,主节点宕机后自动将从节点提升为主。
锁表现:
- 优点:解决了单点故障问题,可用性较高
- 缺点:主从切换时可能丢失锁数据(主节点写入锁后立即宕机,锁数据未同步到从节点)
优化方案:
- 延迟释放锁:加锁成功后休眠 100ms(确保从节点同步)再返回成功
- 延长锁过期时间:使用 Redisson 看门狗机制,确保主从切换完成前锁不会过期
适用场景:绝大多数生产环境的核心业务,如库存扣减、订单创建等
2.3 Redis Cluster 集群
架构特点:将数据分片存储在多个主节点上,每个主节点有对应的从节点;客户端通过 CRC16 (key)%16384 计算 key 所在的槽位,直接访问对应的主节点。
锁表现:
- 优点:高可用、可扩展,单节点故障仅影响该分片的锁
- 缺点:跨分片锁实现复杂,主从切换仍可能丢失锁数据
优化方案 :分片锁按 crc16 (key)%16384 把锁 key 分片到不同节点,每个节点独立处理锁;单节点故障仅影响该分片的锁,不会导致整个锁机制失效。
适用场景:高并发、大规模应用,如秒杀、直播下单等
三、Redis 分布式锁与其他方案的对比
除了 Redis,ZooKeeper、etcd 和数据库也常被用于实现分布式锁。不同方案在性能、一致性和可靠性方面各有侧重。
3.1 核心特性对比
| 对比维度 | Redis 分布式锁 | ZooKeeper 分布式锁 | etcd 分布式锁 | 数据库分布式锁 |
|---|---|---|---|---|
| 一致性模型 | 最终一致性(AP) | 线性一致性(CP) | 线性一致性(CP) | 强一致性(依赖数据库事务) |
| 核心互斥机制 | SET NX 原子命令 | 临时有序节点全局序号 | 全局递增 Revision+CAS | SELECT ... FOR UPDATE |
| 死锁防护方案 | 过期时间 + 看门狗 | 临时节点(会话断开自动删除) | Lease 租约过期自动删除 | 事务超时 + 定时清理 |
| 可重入性 | 支持(Redisson Hash+Lua) | 支持(Curator ThreadLocal 计数) | 支持(官方 concurrency 包) | 不支持 |
| 公平性 | 默认非公平,需额外实现 | 天然公平 | 天然支持公平锁 | 不支持 |
| 性能 | 极高(约 5 万 QPS) | 中等(约 1 千 QPS) | 中等(约 5 千 QPS) | 低(<1 千 QPS) |
| 实现复杂度 | 低(Redisson 封装后) | 中(Curator 封装后) | 中 | 低 |
| 部署成本 | 低 | 高 | 中 | 低 |
3.2 适用场景分析
- Redis 分布式锁:适用于高并发、对性能极其敏感,且允许极短时间锁失效的场景,如秒杀抢购、限流熔断、防止缓存击穿等。
- ZooKeeper 分布式锁:适用于强一致性、低并发、复杂协调的场景,如分布式任务调度、配置中心、集群管理等。
- etcd 分布式锁:适用于云原生环境,与 Kubernetes 生态集成度高,适合容器化部署的应用。
- 数据库分布式锁:适用于已有数据库且并发量较低的场景,不建议在高并发系统中使用。
四、生产环境选型决策树
基于以上分析,我们可以总结出 Redis 分布式锁的生产环境选型决策树:
lua
开始
|
|-- 业务是否简单,锁丢失影响小?
| |-- 是:使用SET NX EX + 唯一标识 + Lua脚本释放锁
| |-- 否:
| |-- 99%的业务场景:使用Redisson单节点锁 + 看门狗机制
| | |-- 是否需要公平锁?是:使用Redisson公平锁
| | |-- 是否需要读写分离?是:使用Redisson读写锁
| | |-- 是否需要同时锁定多个资源?是:使用Redisson联锁
| |
| |-- 极端安全要求(金融、支付):
| |-- 优先选择ZooKeeper/etcd分布式锁
| |-- 若必须使用Redis:使用Redlock + 业务兜底逻辑
4.1 不同业务场景的具体推荐
| 业务场景 | 推荐方案 | 核心理由 |
|---|---|---|
| 日志采集、统计分析 | SET NX EX | 简单高效,锁丢失影响小 |
| 库存扣减、订单创建 | Redisson 单节点锁 | 自动续期,生产首选,性能与可靠性平衡 |
| 商品信息查询与更新 | Redisson 读写锁 | 读读共享,提高并发性能 |
| 定时任务防重 | Redisson 单节点锁 | 简单可靠,避免任务重复执行 |
| 账户扣款、金融交易 | ZooKeeper/etcd | 强一致性,优于 Redlock |
| 秒杀抢购、直播下单 | Redis Cluster 分片锁 | 高可用、可扩展,支持海量并发 |
五、常见问题与避坑指南
5.1 误删他人持有锁
问题:释放锁时未做身份校验,直接执行 DEL 命令删除键。
解决方案:加锁时存入全局唯一的随机值(如 UUID + 线程 ID)作为 value,释放锁前先验证 value 是否与自身持有一致,一致才释放。关键是用 Lua 脚本保证 "验证 + 删除" 的原子性。
5.2 锁过期提前释放
问题:业务执行时间超过锁过期时间,锁自动释放,其他客户端获取锁。
解决方案:
- 使用 Redisson 看门狗自动续期
- 预估业务执行时间,设置足够长的过期时间
- 优化业务逻辑,减少锁持有时间
5.3 主从切换丢锁
问题:主节点宕机,从节点升主,锁数据丢失。
解决方案:
- 使用 Redlock 多节点锁
- 延迟释放锁,给主从同步留足时间
- 使用 Redis Cluster 分片锁,降低单节点故障的影响
5.4 忘记释放锁
问题:异常退出未释放锁,导致死锁。
解决方案:
- 严格使用 try-finally 保证锁释放
- 设置锁过期时间作为兜底方案
5.5 锁等待风暴
问题:大量客户端同时重试抢锁,导致 Redis CPU 飙升至 100%。
解决方案:使用指数退避 + 随机抖动算法,分散客户端的重试时间。
java
public boolean acquireLockWithBackoff(String lockKey, String uniqueValue, int maxRetries) {
int retries = 0;
long baseDelay = 100; // 基础延迟100ms
while (retries < maxRetries) {
if (lock(uniqueValue)) {
return true;
}
// 指数退避 + 随机抖动
long delay = baseDelay * (1 << retries) + new Random().nextLong(100);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
retries++;
}
return false;
}
六、总结
Redis 分布式锁是微服务架构中最常用的分布式锁方案,但它绝不是简单的 SET NX 命令。从原生命令的原子性,到 Redisson 看门狗的自动续期,再到 Redlock 的多节点共识,本质是性能与可靠性的权衡。
核心结论:
- SET NX EX 是基础,适合低风险场景但坑多
- Redisson 看门狗是生产环境首选,自动续期省心,支持多种锁类型
- Redlock 是极端安全场景的备选,但非万能解决方案,理解其局限比盲目使用更重要
- 99% 的微服务业务,一个 Redisson 单节点锁就足够稳定、高效、简单
- 永远记住:分布式锁不是万能的,业务幂等性设计才是数据安全的最后防线
在实际项目中,我们应该根据业务场景选择最适合的方案,避免过度设计。不要为了炫技引入复杂的 Redlock,务实才是后端开发的核心。