一、为什么需要分布式锁?
在单体应用中,我们使用Java的synchronized或ReentrantLock就能解决并发问题。但在微服务架构下,多个实例同时运行,单机的锁机制就失效了。这时就需要分布式锁来保证跨JVM的互斥访问。
分布式锁的核心需求
- 互斥性:同一时刻只能有一个客户端持有锁
- 防止死锁:客户端崩溃后能自动释放锁
- 容错性:Redis节点故障时仍能正常工作
- 可重入性:同一个线程可以多次获取同一个锁
二、Redis实现分布式锁的基础方案
2.1 最简单的实现:SET NX
最早期的实现方式是使用SET命令的NX选项:
SET key value NX PX 30000- NX:只在key不存在时设置
- PX:设置过期时间(毫秒)
这种方式的缺点:
- 无法实现可重入锁
- 无法获取锁的持有人信息
- 无法实现锁的自动续期
2.2 完整实现方案
一个相对完善的Redis分布式锁实现需要包含以下逻辑:
Redis分布式锁基础架构
- 加锁:SET key value NX PX timeout
- 解锁:Lua脚本保证原子性
- 看门狗:定时续期防止业务执行时间超过锁过期时间
三、Redisson分布式锁
Redisson是Redis的Java客户端,它提供了完善的分布式锁实现,解决了原生Redis锁的各种问题。
3.1 核心特性
- 可重入锁:同一个线程可多次获取锁
- 公平锁:按请求顺序获取锁
- 读写锁:允许多个读操作或单个写操作
- 红锁:跨多个Redis节点的分布式锁
- 自动续期:看门狗机制自动延长锁时间
- 锁释放:Lua脚本保证原子性操作
3.2 基本使用
// 获取锁对象 RLock lock = redisson.getLock("myLock"); try { // 加锁(支持等待时间和自动续期) boolean locked = lock.tryLock(100, 30000, TimeUnit.MILLISECONDS); if (locked) { // 执行业务逻辑 doBusiness(); } } finally { // 释放锁 if (lock.isHeldByCurrentThread()) { lock.unlock(); } }四、Redis实现 vs Redisson对比
Redis实现 vs Redisson对比
| 特性 | Redis原生实现 | Redisson |
|---|---|---|
| 实现复杂度 | 高,需要处理多种边界情况 | 低,开箱即用 |
| 可重入锁 | 需要自己实现 | 原生支持 |
| 自动续期 | 需要后台线程续期 | 看门狗自动续期 |
| 公平锁 | 难以实现 | 开箱即用 |
| 读写锁 | 难以实现 | 开箱即用 |
| 红锁 | 需要自己协调 | 开箱即用 |
| 异常处理 | 需要仔细处理 | 完善的异常体系 |
结论:生产环境推荐使用Redisson,它已经处理了各种边界情况和异常场景。
五、Redisson核心原理解析
5.1 加锁机制
Redisson使用Lua脚本保证加锁的原子性:
Redisson加解锁流程
-- 检查key是否存在 if redis.call('exists', KEYS[1]) == 0 then -- 不存在则设置,使用hash结构存储 redis.call('hset', KEYS[1], ARGV[2], 1) redis.call('pexpire', KEYS[1], ARGV[1]) return nil end -- key已存在,检查是否是当前线程 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])锁的数据结构使用Hash类型:
- Key:锁名称
- Field:线程标识(UUID:threadId)
- Value:重入次数
5.2 看门狗机制
看门狗(Watchdog)是Redisson的核心特性,用于解决锁超时问题:
Watchdog机制
- 业务获得锁时,启动后台定时任务
- 每隔lockWatchdogTimeout/3时间检查锁是否还持有
- 如果还持有则重置过期时间
- 业务释放锁时停止看门狗
默认情况下,看门狗每10秒续期一次,锁的过期时间为30秒。
5.3 解锁机制
解锁同样使用Lua脚本保证原子性:
-- 检查锁是否是当前线程持有 if redis.call('hexists', KEYS[1], ARGV[2]) <= 0 then return nil end -- 减少重入次数 local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1) -- 如果重入次数大于0,重置过期时间 if counter > 0 then redis.call('pexpire', KEYS[1], ARGV[1]) return 0 end -- 重入次数为0,删除锁 redis.call('del', KEYS[1]) -- 发布解锁消息 redis.call('publish', KEYS[2], ARGV[3]) return 1六、生产环境实战案例
生产环境实战案例
6.1 库存扣减
@Service public class InventoryService { @Autowired private RedissonClient redisson; public boolean deductStock(String productId, int quantity) { RLock lock = redisson.getLock("stock:" + productId); try { // 等待5秒,锁过期30秒 if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { // 查询库存 int stock = getStock(productId); if (stock >= quantity) { // 扣减库存 updateStock(productId, stock - quantity); return true; } return false; } throw new BusinessException("系统繁忙,请稍后重试"); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }6.2 订单幂等性控制
@Service public class OrderService { @Autowired private RedissonClient redisson; public Order createOrder(OrderRequest request) { String lockKey = "order:create:" + request.getUserId(); RLock lock = redisson.getLock(lockKey); try { if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { // 检查是否已有未完成订单 Order existOrder = getUnfinishedOrder(request.getUserId()); if (existOrder != null) { return existOrder; } // 创建新订单 Order order = buildOrder(request); saveOrder(order); return order; } throw new BusinessException("操作频繁,请稍后重试"); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }6.3 定时任务防止重复执行
@Component public class DataSyncJob { @Autowired private RedissonClient redisson; @Scheduled(cron = "0 */5 * * * *") public void syncData() { String lockKey = "job:data:sync"; RLock lock = redisson.getLock(lockKey); try { // 尝试获取锁,不等待 if (lock.tryLock(0, TimeUnit.SECONDS)) { // 执行数据同步 doSync(); } else { log.info("上次同步任务尚未完成,跳过本次执行"); } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }七、高级特性:读写锁
在读多写少的场景下,使用读写锁可以大幅提升并发性能:
读写锁原理
@Service public class ConfigService { @Autowired private RedissonClient redisson; public String getConfig(String key) { RReadWriteLock rwLock = redisson.getReadWriteLock("config:" + key); RLock readLock = rwLock.readLock(); try { readLock.lock(); // 多个线程可以同时读取 return queryFromDB(key); } finally { readLock.unlock(); } } public void updateConfig(String key, String value) { RReadWriteLock rwLock = redisson.getReadWriteLock("config:" + key); RLock writeLock = rwLock.writeLock(); try { // 写操作独占锁 writeLock.lock(); updateDB(key, value); // 清除缓存 clearCache(key); } finally { writeLock.unlock(); } } }八、最佳实践
8.1 锁的粒度选择
- 粗粒度:锁整个表或模块,简单但并发度低
- 细粒度:锁单个记录,并发度高但实现复杂
推荐:根据业务场景选择合适的粒度,库存扣减用商品维度,订单创建用用户维度。
8.2 锁的过期时间设置
- 不要设置过短,避免业务未执行完就释放
- 不要设置过长,避免故障时长时间阻塞
- Redisson的看门狗会自动续期,但要设置合理的初始时间
推荐:根据业务执行时间的P99值设置,预留50%余量。
8.3 异常处理
- 捕获中断异常,确保锁能被释放
- 使用tryLock而不是lock,避免无限等待
- 检查锁的持有状态,避免误释放
九、常见问题
Q1:Redisson锁会丢失吗?
不会。Redisson使用看门狗机制,即使业务执行时间超过锁的过期时间,也会自动续期,直到业务主动释放锁。
Q2:Redis主从切换会影响锁吗?
会。单节点Redis在主从切换时可能丢失锁。需要使用Redisson的红锁(RedLock)或Redis Cluster。
Q3:如何实现公平锁?
Redisson提供了公平锁实现:
RLock fairLock = redisson.getFairLock("myFairLock"); fairLock.lock();Q4:性能如何?
Redisson的分布式锁性能很好,单机QPS可达5万+,足以应对大多数场景。如有更高性能需求,可以考虑分段锁或本地锁+分布式锁的组合方案。
十、总结
Redisson分布式锁是Java领域最成熟的Redis分布式锁解决方案:
- 使用简单:API友好,学习成本低
- 功能完善:支持多种锁类型和场景
- 稳定可靠:经过大量生产环境验证
- 性能优异:看门狗机制和Lua脚本保证性能
在实际项目中,建议优先使用Redisson,它已经帮你处理了各种复杂的边界情况,让你专注于业务逻辑的实现。