Redisson 分布式锁设计与实战
在分布式环境下,多个服务实例同时操作共享资源(如库存扣减、订单创建、文件处理)时,如何保证数据一致性?这正是分布式锁要解决的核心问题。
本文将深入讲解基于 Redisson 的分布式锁实现原理,包括 Lua 脚本的加锁/解锁逻辑、Watchdog 自动续期机制、可重入锁设计,以及生产环境中的最佳实践与故障排查指南。
1. 分布式锁的核心设计理念
1.1 四大核心特性
互斥性(Mutual Exclusion)
- 在任意时刻,只有一个客户端能持有锁
- 其他客户端必须等待锁释放才能获取
防死锁(Deadlock Prevention)
- 锁必须设置超时时间,避免因程序崩溃导致锁永远无法释放
- 支持自动续期机制,防止业务未完成时锁提前释放
可重入性(Reentrant)
- 同一个线程可以多次获取同一把锁
- 通过重入计数器实现,解决方法嵌套调用场景
高可用性(High Availability)
- 锁服务本身不能成为单点故障
- 支持主从切换、集群部署
1.2 技术选型对比
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Redisson(推荐) | 成熟稳定、API友好、自动续期、可重入 | 依赖Redis | 大部分业务场景 |
| 自研Redis锁 | 轻量级、可定制 | 需要处理各种边界情况,维护成本高 | 学习研究 |
| ZooKeeper | 强一致性、天然支持临时节点 | 性能较低、运维复杂 | 对一致性要求极高的场景 |
| 数据库悲观锁 | 无需额外组件 | 性能差、可能死锁 | 轻量级场景 |
2. 核心技术原理
2.1 Redis 分布式锁实现原理
Redisson 基于 Redis 的 SET 命令和 Lua 脚本实现分布式锁,保证操作的原子性。
加锁脚本
lua
-- 加锁脚本
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;
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]); 工作原理:
- 检查锁是否存在(
exists) - 如果不存在,设置锁并设置过期时间(
hset+pexpire) - 如果锁已存在且是当前线程持有,增加重入计数(
hincrby) - 使用 Hash 结构存储,key 为锁名,field 为线程ID,value 为重入次数
解锁脚本
lua
-- 解锁脚本
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
-- 重入计数 > 0,刷新过期时间
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
-- 重入计数 = 0,删除锁
redis.call('del', KEYS[1]);
return 1;
end; 工作原理:
- 检查锁是否由当前线程持有
- 减少重入计数
- 如果计数 > 0,刷新过期时间
- 如果计数 = 0,删除锁
2.2 Watchdog 自动续期机制
Watchdog 是 Redisson 的核心特性之一,用于防止业务执行时间超过锁的过期时间导致锁提前释放。
工作流程
text
1. 获取锁成功
↓
2. 启动 Watchdog 定时任务
↓
3. 每隔 leaseTime/3 时间检查一次
↓
4. 如果业务还在执行,自动续期
↓
5. 业务执行完成,释放锁,停止 Watchdog
续期时机
- 默认配置:leaseTime = 30秒,每 10秒 续期一次
- 自定义配置:可以通过参数调整
- 自动停止:业务完成或发生异常时自动停止续期
注意:如果指定了
leaseTime参数,Watchdog 将不会启动,需要手动管理锁的过期时间。
2.3 锁的可重入性实现
可重入原理
java
// 外层方法
public void outerMethod() {
distributedLockUtil.executeWithLock("myLock", 3, 10, () -> {
// 第一次获取锁,重入计数 = 1
innerMethod(); // 调用内层方法
return null;
});
}
// 内层方法
public void innerMethod() {
distributedLockUtil.executeWithLock("myLock", 3, 10, () -> {
// 第二次获取锁,重入计数 = 2(可重入)
// 业务逻辑
return null;
});
// 释放一次,重入计数 = 1
}
// 释放一次,重入计数 = 0,锁被完全释放 Redis 存储结构
text
Key: lock:order:123
Field: UUID_ThreadID
Value: 2 (重入次数)
3. 关键代码实现
3.1 Redisson 配置类
java
@Configuration
public class RedissonConfig {
@Value("${spring.data.redis.host:127.0.0.1}")
private String redisHost;
@Value("${spring.data.redis.port:6379}")
private int redisPort;
@Value("${spring.data.redis.password}")
private String redisPassword;
@Value("${spring.data.redis.database:0}")
private int redisDatabase;
@Value("${spring.redisson.connection.minimum-idle-size:5}")
private int minimumIdleSize;
@Value("${spring.redisson.connection.pool-size:10}")
private int poolSize;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// 单节点配置
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort)
.setPassword(!redisPassword.isEmpty() ? redisPassword : null)
.setDatabase(redisDatabase)
.setConnectionMinimumIdleSize(minimumIdleSize)
.setConnectionPoolSize(poolSize);
return Redisson.create(config);
}
} 3.2 分布式锁工具类
java
@Slf4j
@Component
@RequiredArgsConstructor
public class DistributedLockUtil {
private final RedissonClient redissonClient;
/**
* 尝试获取锁并执行操作
*
* @param lockKey 锁的 key
* @param waitTime 等待获取锁的最大时间(秒)
* @param leaseTime 持有锁的最长时间(秒)
* @param supplier 要执行的操作
* @return 操作返回值
*/
public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime, Supplier<T> supplier) {
RLock lock = redissonClient.getLock(lockKey);
try {
boolean acquired = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
if (!acquired) {
throw new RuntimeException("获取锁失败: " + lockKey);
}
log.debug("获取锁成功: {}", lockKey);
return supplier.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁被中断: " + lockKey, e);
} finally {
// 确保锁被正确释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
log.debug("释放锁: {}", lockKey);
}
}
}
/**
* 简化方式:使用默认超时时间
* waitTime = 3秒,leaseTime = 10秒
*/
public <T> T executeWithSimpleLock(String lockKey, Supplier<T> supplier) {
return executeWithLock(lockKey, 3, 10, supplier);
}
/**
* 简化方式:无返回值
*/
public void executeWithSimpleLock(String lockKey, Runnable runnable) {
executeWithLock(lockKey, 3, 10, () -> {
runnable.run();
return null;
});
}
} 3.3 锁 Key 设计规范
命名格式:业务模块:操作类型:业务标识
java
// 订单锁
String lockKey = "order:create:" + orderId;
// 库存锁
String lockKey = "inventory:deduct:" + skuId;
// 用户操作锁
String lockKey = "user:like:" + userId + ":" + targetId; 4. 业务应用案例
4.1 防止重复提交
java
@Service
@RequiredArgsConstructor
public class OrderService {
private final DistributedLockUtil lockUtil;
public Order createOrder(CreateOrderRequest request) {
String lockKey = "order:create:" + request.getUserId();
return lockUtil.executeWithLock(lockKey, 3, 10, () -> {
// 检查是否重复提交
if (orderRepository.existsByRequestId(request.getRequestId())) {
throw new BusinessException("订单已存在,请勿重复提交");
}
// 创建订单
Order order = new Order();
order.setRequestId(request.getRequestId());
order.setUserId(request.getUserId());
// ... 其他业务逻辑
return orderRepository.save(order);
});
}
} 4.2 库存扣减
java
@Service
@RequiredArgsConstructor
public class InventoryService {
private final DistributedLockUtil lockUtil;
public void deductStock(String skuId, int quantity) {
String lockKey = "inventory:deduct:" + skuId;
lockUtil.executeWithLock(lockKey, 5, 30, () -> {
// 1. 查询当前库存
Inventory inventory = inventoryRepository.findBySkuId(skuId);
// 2. 检查库存是否充足
if (inventory.getStock() < quantity) {
throw new BusinessException("库存不足");
}
// 3. 扣减库存
inventory.setStock(inventory.getStock() - quantity);
inventoryRepository.save(inventory);
return null;
});
}
} 5. 最佳实践
5.1 锁粒度设计
| 粒度级别 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| 粗粒度 | lock:order | 实现简单 | 并发度低 |
| 中粒度 | lock:order:userId | 平衡并发与复杂度 | 需要合理设计 |
| 细粒度 | lock:order:orderId | 并发度高 | 锁数量多 |
5.2 超时时间选择
| 业务类型 | 建议配置 | 说明 |
|---|---|---|
| 快速操作 | waitTime=3s, leaseTime=10s | 点赞、收藏等 |
| 一般操作 | waitTime=5s, leaseTime=30s | 订单创建、库存扣减 |
| 复杂操作 | waitTime=10s, leaseTime=60s | 批量处理、文件上传 |
| 长时间操作 | 不指定leaseTime,使用Watchdog | 需要自动续期 |
5.3 异常处理规范
java
public void businessOperation(String businessId) {
String lockKey = "business:operation:" + businessId;
try {
lockUtil.executeWithLock(lockKey, 3, 10, () -> {
// 业务逻辑
return null;
});
} catch (RuntimeException e) {
if (e.getMessage().contains("获取锁失败")) {
// 锁获取失败,给用户友好提示
throw new BusinessException("系统繁忙,请稍后重试");
}
throw e;
}
} 6. 故障排查
6.1 常见问题与解决方案
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 锁获取超时 | 其他线程持有锁时间过长 | 1. 检查 leaseTime 配置 2. 优化业务逻辑执行时间 |
| 锁提前释放 | 业务执行时间超过 leaseTime | 1. 增加 leaseTime 2. 使用 Watchdog 自动续期 |
| 死锁 | 程序异常未释放锁 | 1. 确保 finally 中释放锁 2. 设置合理的过期时间 |
| 锁重入失败 | 不同线程尝试重入 | 检查业务逻辑,确保是同一线程 |
6.2 监控指标
建议监控以下指标:
java
@Component
@RequiredArgsConstructor
public class LockMetrics {
private final MeterRegistry meterRegistry;
public void recordLockAcquired(String lockKey, long waitTime) {
Timer.builder("distributed.lock.acquire.time")
.tag("lock_key", lockKey)
.register(meterRegistry)
.record(waitTime, TimeUnit.MILLISECONDS);
}
public void recordLockFailed(String lockKey) {
Counter.builder("distributed.lock.acquire.failed")
.tag("lock_key", lockKey)
.register(meterRegistry)
.increment();
}
} 7. 总结
Redisson 分布式锁是一个成熟、可靠的解决方案,具备以下核心优势:
- 开箱即用:无需关心底层实现细节,API 简洁易用
- 自动化机制:Watchdog 自动续期、异常自动释放锁
- 可重入支持:支持同一线程多次获取锁
- 高性能:基于 Redis 内存操作,响应速度快(通常 < 5ms)
在使用时,需要注意:
- 合理设计锁粒度,平衡并发度和复杂度
- 根据业务特点选择合适的超时时间
- 确保异常情况下锁能被正确释放
- 建立监控指标,及时发现锁相关问题
分布式锁不是银弹,在高并发场景下,还需要结合消息队列、乐观锁等技术方案,构建完整的并发控制体系。
