在分布式系统中,协调多个服务或实例对共享资源的访问是一个核心挑战。简单使用Redis的SETNX命令实现的分布式锁,虽然基础,但在实际生产环境中往往面临几个关键问题。
一、基于SETNX的分布式锁存在哪些问题?
- 不可重入:同一个线程无法多次获取同一把锁。这在使用锁封装的方法相互调用时,极易引发死锁。
- 不可重试:获取锁仅尝试一次,失败便立即返回。在并发激烈的场景下,合理的重试机制是提升成功率的关键。
- 超时释放隐患:为避免死锁设置锁超时时间,若业务执行耗时超过锁有效期,锁会提前释放,存在安全风险。
- 主从一致性问题:在Redis主从架构下,锁信息异步同步到从节点。若在主节点写入锁后、同步完成前主节点宕机,锁信息可能丢失,导致多个客户端同时持有锁。
下图清晰对比了这几种锁机制的特性:

二、Redisson:Java分布式锁的强大框架
Redisson 是一个在Redis基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了基础的Redis操作,更封装了丰富的分布式对象和分布式服务,其中分布式锁是其最核心的功能之一。
Redisson的分布式锁功能完善,提供了多种锁和同步器以满足不同场景需求,具体列表如下:

三、快速集成与使用Redisson分布式锁
3.1 引入依赖
首先,在您的 pom.xml 中添加Redisson依赖。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
3.2 配置Redisson客户端
通过一个配置类来创建 RedissonClient Bean,这是操作所有Redisson功能的基础。
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// 使用单节点模式,根据实际情况可配置集群、哨兵等模式
config.useSingleServer()
.setAddress("redis://192.168.150.101:6379")
.setPassword("123321");
return Redisson.create(config);
}
}
3.3 基础锁使用示例
以下是一个在测试环境中的基本使用示例,展示了获取锁、执行业务和释放锁的完整流程。
@Resource
private RedissonClient redissonClient;
@Test
void testRedisson() throws Exception {
// 1. 获取锁对象(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
// 2. 尝试获取锁(支持等待重试和自动释放时间)
// 参数:最大等待时间、锁持有时间、时间单位
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
// 3. 判断是否获取成功
if (isLock) {
try {
System.out.println("成功获取锁,执行业务逻辑...");
// 你的业务代码放在这里
} finally {
// 4. 无论如何,最终在finally块中释放锁
lock.unlock();
}
}
}
3.4 业务场景实战:秒杀订单
在真实的业务场景,例如秒杀下单中,我们使用Redisson锁来防止用户重复下单,其高并发控制逻辑如下:
@Service
public class VoucherOrderServiceImpl implements IVoucherOrderService {
@Resource
private RedissonClient redissonClient;
@Override
public Result seckillVoucher(Long voucherId) {
// ... 优惠券查询、时间校验、库存判断等前置逻辑 ...
Long userId = UserHolder.getUser().getId();
// 创建分布式锁对象,锁键以业务前缀+用户ID构成,实现细粒度锁
RLock lock = redissonClient.getLock("lock:order:" + userId);
// 尝试获取锁(此处为立即获取,不等待)
boolean isLock = lock.tryLock();
if (!isLock) {
// 获取锁失败,代表该用户正在下单或重复请求
return Result.fail("不允许重复下单!");
}
try {
// 获取代理对象以确保事务生效
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
// 释放锁
lock.unlock();
}
}
}
四、Redisson可重入锁的实现原理
Redisson的可重入锁设计精巧,它利用Redis的Hash结构来存储锁状态,完美解决了线程重入问题。其加锁的核心逻辑通过一段Lua脚本来保证原子性执行。
加锁Lua脚本解析:
- KEYS[1]:锁的键名。
- ARGV[1]:锁的生存时间。
- ARGV[2]:锁的客户端标识,通常为
客户端ID:线程ID,用作Hash结构中的field。
-- 锁不存在时,加锁并设置过期时间
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 锁已存在,且是当前线程持有,则重入次数+1
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]);
流程解读:
- 判断锁(大Key)是否存在。不存在则直接加锁,设置线程标识(小Key)和重入次数为1。
- 锁已存在,则检查是否由当前线程持有(即小Key是否存在)。如果是,则将重入次数加1(
hincrby)。
- 以上都不是,说明锁被其他线程持有,返回锁的剩余有效期(
pttl)。
下图展示了Redis中存储的锁数据结构:

五、锁重试与WatchDog看门狗机制
Redisson不仅支持在 tryLock 时指定等待时间进行重试,其 lock() 方法还内置了强大的 WatchDog(看门狗) 机制,解决了锁超时释放而业务未执行完的安全隐患。
核心流程:
- 当使用
lock() 无参方法时,默认加锁时间为30秒。
- 加锁成功后,Redisson会启动一个后台定时任务(看门狗线程),每隔 锁有效期 / 3(即10秒)检查一次。
- 如果业务仍在执行(即客户端仍存活),看门狗会通过Lua脚本重置锁的过期时间,恢复到30秒。
- 此过程不断递归,直到业务完成并主动释放锁。如果客户端宕机,看门狗线程随之停止,锁最终会因超时而自动释放,避免了死锁。
这种机制确保了只要业务线程健康运行,锁就不会被意外释放,同时结合了 [数据库](https://yunpan.plus/f/23-1) 等组件的可靠性设计思路,为长事务提供了保障。
六、MultiLock联锁解决主从一致性危机
在Redis主从或哨兵模式下,主节点写入锁后,若在数据同步到从节点前发生故障切换,新的主节点可能不包含该锁信息,导致锁失效。这就是分布式锁的 主从一致性 或 脑裂 问题。

Redisson提供的 MultiLock(联锁) 正是为解决此问题而生。其思想是将锁同时写入多个独立的Redis主节点(非主从关系),所有节点都加锁成功才算成功。
工作原理:
- 客户端需要向N个独立的Redis实例尝试加锁。
- 总加锁超时时间 =
锁数量 * 1500ms。例如3个锁,则总等待时间为4500ms。
- 在总时间内,必须成功获取所有锁。只要有一个实例加锁失败,则会在剩余时间内不断重试。
- 一旦失败或最终超时,已获取的锁会被全部释放。
下图展示了Redisson MultiLock的工作流程:

通过MultiLock,牺牲了一定的写入性能(需要写多个节点),换取了极高的锁可用性和数据一致性,适用于对可靠性要求极高的金融、交易等场景。