分布式锁,在分布式系统里是一个必须面对的核心课题——当多个节点并发访问共享资源时,必须有一套可靠的协调机制来保障数据的一致性。Redis凭借其高性能和原子操作特性,成为实现分布式锁的主流技术方案。本文将从底层原理出发,深入拆解Redis分布式锁的核心逻辑,并详细分析三种常见实现方式的代码、优缺点,以及生产环境中需要特别注意的关键细节。

一、Redis分布式锁核心原理
1.1 核心设计目标与关键特性
一个可靠的分布式锁,必须满足以下几个关键特性:
- 互斥性:同一时刻只允许一个客户端持有锁,否则并发操作共享资源,极易引发数据一致性问题;
- 安全性:锁只能由持有它的客户端主动释放,绝不能被其他客户端误删或非法释放;
- 超时释放:当持有锁的客户端意外宕机或网络中断时,锁必须能够自动过期,避免形成死锁导致系统停滞;
- 原子性:加锁与释放锁的关键操作必须保证原子执行,否则在并发场景下极易出现逻辑漏洞;
- 可重入(可选):同一个客户端在持有锁后,再次请求同一把锁时无需重新获取,能够显著提升易用性与开发效率。
1.2 Redis实现分布式锁的核心基础
Redis主要依赖下面几个核心命令与特性来支撑分布式锁的实现:
| 命令/特性 | 作用说明 |
|---|---|
SET key value NX EX t |
原子执行“不存在则设置(NX)+ 过期时间(EX)”,避免将加锁与设置超时拆分为两步操作,从根本上杜绝并发隐患 |
DEL key |
删除锁(即释放锁),但必须配合校验锁的归属,否则可能误删其他客户端持有的锁 |
| Lua脚本 | 将“校验锁归属 + 释放锁”封装成一个原子操作,完美解决释放锁时可能出现的并发安全问题 |
| Redisson(客户端) | 基于Redis封装了可重入、自动续期、公平锁等高级特性,极大简化了分布式锁的使用复杂度 |
二、三种实现方式详解(逻辑分析+问题探讨)
方式1:基础实现(SetNX + 手动校验释放)
2.1 代码逻辑拆解
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 示例:扣减库存(基础分布式锁实现)
*/
private void order(){
// 1. 生成唯一锁值(用于校验锁归属,防止误删)
String lockValue = UUID.randomUUID().toString();
// 2. 加锁:SETNX + 过期时间(原子操作),30秒自动释放
Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent("product:1001:lock", lockValue, 30, TimeUnit.SECONDS);
try {
// 3. 加锁成功则执行业务逻辑(扣减库存)
if (locked) {
Integer count = (Integer) stringRedisTemplate.opsForHash().get("product:1001",
"number");
if (count > 0) {
stringRedisTemplate.opsForHash().put("product:1001", "number", count - 1);
}
}
} finally {
// 4. 释放锁:先校验锁归属,再删除(非原子操作)
if (lockValue.equals(stringRedisTemplate.opsForValue().get("product:1001:lock"))) {
stringRedisTemplate.delete("product:1001:lock");
}
}
}
2.2 核心逻辑说明
- 加锁:通过
setIfAbsent(底层对应SET NX EX)实现原子加锁,同时设置30秒超时时间,确保客户端宕机后锁也能自动释放,避免死锁风险; - 锁归属校验:使用UUID生成唯一的
lockValue,释放锁前校验该值是否匹配,从而防止误删其他客户端持有的锁; - 释放锁:在finally块中执行释放逻辑,确保业务执行完成(或抛出异常)后锁能够被及时释放。
2.3 存在的核心问题
- 释放锁非原子性:“校验锁归属 + 删除锁”是两步独立操作,如果校验后锁恰好过期,其他客户端已经加上了新锁,当前客户端再执行
delete就会把别人的锁误删掉; - 无重试机制:加锁失败后直接放弃,实际生产场景中需要根据业务需求设置重试逻辑,例如循环重试配合适当休眠;
- 无锁续期:如果业务执行时间超过30秒,锁会自动过期,导致多个客户端同时执行业务,互斥性被彻底破坏;
- Hash操作类型转换风险:
stringRedisTemplate.opsForHash().get()返回的是Object类型,强转为Integer时很可能出现类型异常,因此必须先判空并添加类型校验。
方式2:优化版(Lua脚本保证释放锁原子性)
2.1 代码逻辑拆解
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final String LOCK_KEY = "product:1001:lock";
private static final String STOCK_KEY = "product:1001:number";
private static final long LOCK_TIMEOUT = 30; // 锁超时时间(秒)
private static final long SLEEP_TIME = 100; // 重试间隔(毫秒)
private void order() {
String lockValue = UUID.randomUUID().toString();
try {
// 1. 尝试获取锁(原子加锁)
Boolean locked = tryAcquireLock(lockValue);
if (!locked) {
// 加锁失败可重试或返回失败(示例直接返回,实际可加循环重试)
return;
}
// 2. 执行业务:获取并扣减库存(简化为String结构,避免Hash类型转换问题)
String stockStr = stringRedisTemplate.opsForValue().get(STOCK_KEY);
if (stockStr == null || Integer.parseInt(stockStr) <= 0) {
return;
}
stringRedisTemplate.opsForValue().set(STOCK_KEY, String.valueOf(Integer.parseInt(stockStr) - 1));
} finally {
// 3. 释放锁:Lua脚本封装“校验+删除”,保证原子性
releaseLock(lockValue);
}
}
/**
* 原子加锁:SET NX EX
*/
private Boolean tryAcquireLock(String lockValue) {
return stringRedisTemplate.opsForValue()
.setIfAbsent(LOCK_KEY, lockValue, LOCK_TIMEOUT, TimeUnit.SECONDS);
}
/**
* 原子释放锁:Lua脚本
*/
private void releaseLock(String lockValue) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Arrays.asList(LOCK_KEY),
lockValue
);
}
2.2 核心优化点
- 释放锁原子化:将“校验锁归属(get)+ 删除锁(del)”封装成Lua脚本,Redis会原子执行整个脚本内容,彻底解决方式1中可能出现的“误删锁”问题;
- 简化库存存储:把库存从Hash结构改为String结构,避免了类型转换异常,显著降低了业务复杂度;
- 代码分层清晰:抽离
tryAcquireLock和releaseLock方法,代码复用性和可维护性更好。
2.3 仍存在的问题
- 无锁续期:核心问题依然未解决!如果业务执行时间(例如扣减库存需要40秒)超过了
LOCK_TIMEOUT(30秒),锁会提前过期,导致并发安全问题; - 重试逻辑缺失:示例中加锁失败直接返回,实际场景需要增加“循环重试 + 最大重试次数”机制,避免瞬时并发导致加锁失败;
- 无异常处理:
Integer.parseInt(stockStr)没有做异常捕获,如果库存值不是数字,会直接抛出运行时异常; - 单点风险:依赖单个Redis节点,如果该节点宕机,锁数据就会丢失,可能导致多个客户端同时获得锁。
方式3:生产级实现(Redisson客户端)
Redisson是Redis官方推荐的Ja va客户端,它内置了分布式锁的完整实现,完美解决了手动实现时遇到的诸多痛点。
2.1 代码逻辑拆解
@Resource
private RedissonClient redissonClient;
@Resource
private StringRedisTemplate stringRedisTemplate;
private void order() {
// 1. 获取分布式锁对象(可重入锁)
RLock lock = redissonClient.getLock("product:1001:lock");
try {
// 2. 加锁:最多等待10秒,锁30秒后自动释放;获取锁成功则执行业务
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
try {
// 3. 扣减库存业务逻辑
Integer count = (Integer) stringRedisTemplate.opsForHash().get("product:1001",
"number");
if (count != null && count > 0) {
stringRedisTemplate.opsForHash().put("product:1001", "number", count - 1);
}
} finally {
// 4. 手动释放锁(若业务执行完未超时,主动释放)
lock.unlock();
}
}
} catch (InterruptedException e) {
// 5. 中断异常处理,恢复线程中断状态
Thread.currentThread().interrupt();
}
}
2.2 核心优势(Redisson解决的痛点)
- 自动锁续期(看门狗机制):
- 如果业务执行时间超过了锁超时时间,Redisson会启动后台线程(默认每10秒执行一次)自动将锁的超时时间续期到30秒;
- 只有当客户端正常释放锁或者意外宕机时,续期才会停止,这就彻底解决了“锁提前过期”带来的并发安全问题。
- 可重入性:基于Redis的Hash结构存储锁的持有次数,同一客户端多次调用
tryLock不会导致死锁,极大提升了易用性; - 优雅的重试与等待:
tryLock(waitTime, leaseTime, unit)支持“最大等待时间”,加锁失败时会阻塞等待,直到超时或者成功获取到锁; - 原子性加锁/释放锁:底层封装了Lua脚本,保证加锁、释放锁的原子性,无需开发者手动处理;
- 集群适配:支持Redis主从、哨兵、集群模式,有效解决了单点风险(当然需要正确配置Redisson的集群模式)。
2.3 需注意的细节
- 解锁时机:必须在
finally块中执行unlock(),但最好先判断lock.isHeldByCurrentThread(),避免未持有锁时执行解锁抛出异常; - 异常处理:
tryLock会抛出InterruptedException,需要捕获并恢复线程的中断状态,避免线程状态异常影响后续运行; - Redisson配置:生产环境要正确配置RedissonClient(例如连接池、超时时间、集群节点),否则可能导致锁性能下降甚至失效;
- 锁粒度:避免使用过大的锁粒度(例如“product:lock”),应该细化到具体资源(例如“product:1001:lock”),减少锁竞争,提升系统并发能力。
三、三种实现方式对比与生产建议
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 方式1(基础版) | 代码简单、无额外依赖 | 释放锁非原子、无续期、易误删锁 | 测试环境、低并发非核心业务 |
| 方式2(Lua版) | 释放锁原子化、代码结构清晰 | 无续期、重试逻辑需手动实现、单点风险 | 中小并发、核心逻辑简单场景 |
| 方式3(Redisson) | 自动续期、可重入、集群适配 | 引入Redisson依赖、配置稍复杂 | 生产环境、高并发核心业务 |
生产环境核心建议
- 优先使用Redisson:手动实现分布式锁很容易遗漏边界条件(例如续期、原子性、集群支持),Redisson封装了成熟的解决方案,是生产环境的首选方案;
- 锁超时时间合理设置:结合业务平均执行时间来设置(例如业务平均执行5秒,设置超时30秒),避免过短导致续期频繁,过长增加死锁风险;
- 避免长时间持有锁:分布式锁应该遵循“快进快出”原则,执行业务逻辑时尽量避开耗时操作(例如数据库慢查询、远程调用),必要时可拆分锁粒度;
- 集群模式适配:如果Redis是集群或哨兵模式,Redisson需要配置
RedissonNode或ClusterServersConfig,避免主从切换导致锁丢失; - 兜底方案:分布式锁失效时,需要有兜底逻辑(例如数据库乐观锁),避免数据一致性问题。
四、总结
Redis分布式锁的核心本质可以概括为原子加锁 + 安全释放 + 超时兜底:
- 基础实现(方式1)仅适合在测试环境使用,核心问题在于释放锁非原子、没有续期机制;
- Lua脚本优化版(方式2)解决了释放锁的原子性问题,但续期、重试等逻辑仍需开发者手动处理;
- Redisson(方式3)是生产级方案,通过看门狗机制、可重入性、集群适配等特性,全面解决了手动实现的所有核心痛点。
在生产环境中,除非有特殊的定制需求,否则建议优先基于Redisson来实现分布式锁,这样既能保证可靠性,又能显著降低开发和维护成本。
