1. 项目背景与核心需求为什么是Redisson在微服务架构和分布式系统成为主流的今天一个看似简单的“库存扣减”操作都可能因为多个服务实例同时执行而引发超卖问题。传统的单机锁如Java的synchronized或ReentrantLock在单体应用时代尚能一战但在分布式环境下它们的作用范围仅限于单个JVM进程对部署在其他服务器上的服务实例无能为力。这时我们需要一个所有服务实例都能“看见”并共同遵守的锁机制这就是分布式锁。实现分布式锁的方案有很多比如基于数据库唯一索引、基于ZooKeeper的临时顺序节点以及基于Redis的SETNX命令。其中基于Redis的方案因其高性能和简单易用而广受欢迎。然而如果你直接使用Redis的SET key value NX PX timeout命令手动实现很快就会遇到一系列棘手的问题锁的续期怎么办锁释放时的原子性如何保证如何实现可重入这些细节处理不当轻则导致锁失效重则引发业务数据错乱。Redisson的出现正是为了解决这些“脏活累活”。它是一个在Redis基础上实现的Java驻内存数据网格客户端将复杂的分布式锁逻辑封装成了简单易用的API。它提供的RLock对象其接口和使用方式几乎与JDK的ReentrantLock一致让开发者能以最小的学习成本获得一个生产级可用的分布式锁实现。它内部帮你处理了锁续期看门狗机制、锁释放的Lua脚本原子性操作、可重入性、公平锁等高级特性。因此在SpringBoot项目中选择Redisson来实现分布式锁是一个兼顾了可靠性、易用性和性能的明智选择。2. 环境搭建与Redisson集成配置在开始编码之前我们需要一个可运行的SpringBoot项目环境并完成Redisson客户端的集成。这里我假设你使用Maven进行依赖管理并有一个基础的SpringBoot 2.x 或 3.x 项目。2.1 引入核心依赖首先在项目的pom.xml文件中添加Redisson的Spring Boot Starter依赖。这个Starter会自动配置Redisson客户端比手动配置要方便得多。dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.0/version !-- 请使用最新稳定版本 -- /dependency为什么选择Starter而不是核心库redisson-spring-boot-starter除了包含核心的redisson依赖还提供了与Spring环境自动集成的能力比如根据application.yml配置自动创建RedissonClientBean。如果你只用核心库就需要自己写一堆Bean配置徒增工作量。2.2 配置文件详解单机、哨兵与集群模式Redisson支持多种Redis部署模式。绝大多数开发和测试环境使用单机模式就已足够。我们在application.yml中进行配置。单机模式配置spring: redis: # 这些是Spring Boot Redis的通用配置Redisson Starter也会读取 host: 127.0.0.1 port: 6379 database: 0 password: yourpassword # 如果没有密码则删除此行或留空 # Redisson专属配置优先级更高更全面 redisson: config: | singleServerConfig: address: redis://${spring.redis.host}:${spring.redis.port} password: ${spring.redis.password} database: ${spring.redis.database} # 连接池配置对性能影响很大 connectionPoolSize: 64 # 最大连接数 connectionMinimumIdleSize: 24 # 最小空闲连接数 idleConnectionTimeout: 10000 # 连接空闲超时单位毫秒 connectTimeout: 10000 # 连接超时 timeout: 3000 # 命令等待超时 retryAttempts: 3 # 命令失败重试次数 retryInterval: 1500 # 命令重试发送时间间隔关键参数解析与调优建议connectionPoolSize和connectionMinimumIdleSize这是影响并发性能的关键。如果你的应用并发量很高且Redis服务器资源充足可以适当调大这两个值。connectionMinimumIdleSize设置一个常驻空闲连接池可以避免突发请求时临时建立连接的开销。生产环境建议根据压测结果调整。idleConnectionTimeout连接空闲多久后释放。设置过短会导致频繁创建连接过长则浪费资源。10秒是一个比较折中的值。timeout执行Redis命令的超时时间。如果你的业务逻辑复杂或网络延迟高可以适当调大避免在Redis响应慢时误判为失败。哨兵与集群模式配置示例如果你的生产环境是高可用的Redis哨兵或集群配置如下redisson: config: | # 哨兵模式 sentinelServersConfig: sentinelAddresses: - redis://sentinel1:26379 - redis://sentinel2:26379 - redis://sentinel3:26379 masterName: mymaster password: yourpassword # 或者集群模式 clusterServersConfig: nodeAddresses: - redis://cluster-node1:6379 - redis://cluster-node2:6379 - redis://cluster-node3:6379 password: yourpassword配置完成后Spring Boot会自动创建一个RedissonClient实例并注入到IoC容器中我们在业务代码中直接Autowired使用即可。3. 分布式锁核心API与基础用法实战Redisson的分布式锁核心接口是RLock它继承了java.util.concurrent.locks.Lock接口所以如果你熟悉ReentrantLock那么上手RLock会非常快。3.1 基础加锁与解锁让我们从一个最基础的场景开始秒杀活动中扣减库存。Service public class SeckillService { Autowired private RedissonClient redissonClient; Autowired private InventoryMapper inventoryMapper; // 假设的库存Mapper public boolean seckillProduct(Long productId) { // 1. 构造锁的Key。这是关键必须保证业务唯一性。 // 格式建议业务前缀:业务标识如 lock:seckill:product:1001 String lockKey lock:seckill:product: productId; RLock lock redissonClient.getLock(lockKey); // 2. 尝试加锁 try { // 尝试获取锁最多等待10秒锁持有时间设置为30秒 boolean isLocked lock.tryLock(10, 30, TimeUnit.SECONDS); if (!isLocked) { // 获取锁失败可能是系统繁忙或死锁直接返回秒杀失败 log.warn(获取分布式锁失败productId: {}, productId); return false; } // 3. 成功获取锁执行核心业务逻辑 log.info(成功获取锁开始处理库存扣减productId: {}, productId); // 查询当前库存 Inventory inventory inventoryMapper.selectById(productId); if (inventory null || inventory.getStock() 0) { return false; } // 扣减库存 inventory.setStock(inventory.getStock() - 1); inventoryMapper.updateById(inventory); // 模拟其他业务操作耗时 Thread.sleep(100); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 log.error(秒杀过程被中断, e); return false; } finally { // 4. 无论如何最终必须释放锁 if (lock.isHeldByCurrentThread()) { // 重要检查是否当前线程还持有锁 lock.unlock(); log.info(锁已释放productId: {}, productId); } } } }代码逐行解析与避坑指南构造锁Key (lockKey)这是分布式锁的“身份证”。必须确保在同一个业务维度下是全局唯一的。通常使用“业务类型:业务ID”的格式。切忌使用固定的Key如global_lock这会导致不同业务间不必要的竞争成为性能瓶颈。tryLock(long waitTime, long leaseTime, TimeUnit unit)这是最常用的加锁方法。waitTime获取锁的最大等待时间。如果设置为0则获取不到锁立即返回false。设置一个合理的等待时间如5-10秒可以避免瞬时高并发下大量请求立即失败起到“排队”和缓冲的作用。leaseTime锁的持有时间。这是Redisson分布式锁最核心的机制之一也是容易踩坑的地方。如果leaseTime设置为-1或使用lock.lock()Redisson会启动一个“看门狗”Watchdog线程在业务执行期间每隔leaseTime / 3的时间默认10秒检查一次如果业务还在执行且锁仍被当前线程持有就自动将锁的过期时间重置为初始值默认30秒。这有效防止了因为业务执行时间过长导致锁自动过期而被其他线程获取的问题。如果leaseTime设置为一个大于0的具体值如30秒看门狗机制将不会启动。锁会在设定的时间后自动过期。这意味着你必须确保你的业务逻辑在leaseTime内一定能执行完毕否则锁会提前释放导致数据不一致。对于执行时间不确定的业务强烈建议使用看门狗模式即不指定leaseTime或设为-1。释放锁 (lock.unlock())必须在finally块中执行确保异常时锁也能被释放避免死锁。同时务必先调用lock.isHeldByCurrentThread()进行检查。因为锁可能因为网络问题、看门狗续期失败或业务超时导致自动过期此时当前线程已不再持有锁如果强行解锁Redisson会抛出IllegalMonitorStateException异常。3.2 可重入锁与公平锁可重入性RLock是可重入锁。这意味着同一个线程可以多次获取同一把锁而不会把自己锁死。这在递归调用或一个方法内需要多次加锁同一资源的场景下非常有用。Redisson内部通过Redis的Hash结构存储锁信息其中包含了线程ID和重入次数。public void reentrantMethod(String key) { RLock lock redissonClient.getLock(key); lock.lock(); try { // 在锁内再次调用需要同一把锁的方法 innerMethod(key); } finally { lock.unlock(); } } private void innerMethod(String key) { RLock lock redissonClient.getLock(key); // 获取的是同一把锁 lock.lock(); // 同一个线程这里会直接增加重入次数不会阻塞 try { // 内部业务逻辑 } finally { lock.unlock(); // 减少重入次数直到为0才会真正释放锁 } }公平锁默认的RLock是非公平锁获取锁的顺序与请求的顺序无关谁抢到是谁的。Redisson也提供了公平锁的实现它保证了等待时间最长的线程优先获得锁。RLock fairLock redissonClient.getFairLock(fairLockKey); fairLock.lock(); try { // 业务逻辑 } finally { fairLock.unlock(); }注意公平锁的实现比非公平锁复杂性能开销也更大因为它需要在Redis中维护一个等待队列。除非业务有严格的先来后到的顺序要求否则建议使用默认的非公平锁以获得更高吞吐。4. 高级特性与生产环境最佳实践掌握了基础用法我们来看看如何让分布式锁在生产环境中更稳健、更高效。4.1 看门狗机制深度剖析与配置前面提到了看门狗这里深入一下。当你调用lock()或tryLock()时不指定leaseTime看门狗就会启动。工作原理加锁成功时在Redis中设置的Key过期时间默认是30秒。后台启动一个定时调度任务看门狗每隔10秒lockWatchdogTimeout / 3检查一次。检查时如果客户端还“活着”持有锁的JVM进程未挂并且锁依然存在则通过Lua脚本将锁的过期时间重新设置为30秒。配置看门狗超时时间 默认的30秒可能不适合所有业务。你可以在Redisson配置中修改它。redisson: config: | singleServerConfig: address: redis://127.0.0.1:6379 lockWatchdogTimeout: 30000 # 单位毫秒默认30000即30秒什么时候应该调整lockWatchdogTimeout调大如果你的业务逻辑平均执行时间很长例如超过20秒为了避免频繁续期带来的网络开销可以适当调大比如设置为60秒。但要注意这也会导致客户端崩溃后锁需要更长时间才能自动释放。调小如果你的业务逻辑通常很短几秒内可以适当调小比如15秒。这样在客户端崩溃时锁能更快被释放减少系统不可用时间。但续期会更频繁。一个重要的坑tryLock(long time, TimeUnit unit)方法这个方法只传一个等待时间不传租期时间。它的租期时间用的就是lockWatchdogTimeout。这意味着如果你用这个方法看门狗机制是生效的。务必确保你的业务逻辑执行时间不会远超过lockWatchdogTimeout。4.2 读写锁ReadWriteLock的应用场景分布式读写锁RReadWriteLock允许多个读锁同时持有但写锁是排他的。这非常适合“读多写少”的场景可以大幅提升系统的并发读取能力。Service public class ProductService { Autowired private RedissonClient redissonClient; Autowired private ProductCacheDao cacheDao; // 模拟缓存访问 // 读操作多个线程可并发执行 public Product getProduct(Long id) { String rwLockKey rwlock:product: id; RReadWriteLock rwLock redissonClient.getReadWriteLock(rwLockKey); RLock readLock rwLock.readLock(); readLock.lock(); try { // 从缓存读取产品信息 Product product cacheDao.getFromCache(id); if (product ! null) { return product; } // 缓存不存在模拟从DB读取这里实际也应加锁但为演示简化 // ... return product; } finally { readLock.unlock(); } } // 写操作独占执行 public void updateProduct(Product product) { String rwLockKey rwlock:product: product.getId(); RReadWriteLock rwLock redissonClient.getReadWriteLock(rwLockKey); RLock writeLock rwLock.writeLock(); writeLock.lock(); try { // 更新数据库 // ... // 清除或更新缓存 cacheDao.evictCache(product.getId()); } finally { writeLock.unlock(); } } }使用要点读写锁的Key同样需要根据业务数据维度设计。写锁会阻塞所有读锁和写锁读锁只会阻塞写锁。要小心“写锁饥饿”问题即一直有读请求导致写请求永远无法获取锁。Redisson的公平锁策略可以在一定程度上缓解这个问题。4.3 联锁MultiLock与红锁RedLock辨析这是分布式锁中高级且容易混淆的概念。联锁MultiLock将多个RLock对象关联成一个锁。只有当你同时获取了所有这些锁时才算加锁成功。这用于需要同时锁定多个独立资源的场景。public void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) { RLock lock1 redissonClient.getLock(lock:account: fromAccountId); RLock lock2 redissonClient.getLock(lock:account: toAccountId); RLock multiLock redissonClient.getMultiLock(lock1, lock2); // 创建联锁 multiLock.lock(); try { // 对两个账户进行转账操作需要同时锁定双方账户 accountService.debit(fromAccountId, amount); accountService.credit(toAccountId, amount); } finally { multiLock.unlock(); } }红锁RedLock这是一个用于提升分布式锁可靠性的算法特别是在Redis集群模式下。它的核心思想是为了在某个主从架构的Redis集群中获得锁客户端需要向超过半数N/2 1的、相互独立的Redis主节点申请锁且每个锁都有相同的过期时间。只有当从大多数节点都成功获取锁时才算真正加锁成功。重要提示Martin Kleppmann《数据密集型应用系统设计》作者曾与Redis作者Antirez就RedLock算法的安全性进行过激烈辩论。目前社区普遍认为在需要强一致性保证的极端场景下如金融交易核心链路RedLock可能仍存在理论上的边界问题。对于绝大多数应用场景使用单Redis实例配合AOF持久化和fsyncalways或Redis哨兵/集群模式并合理设置锁超时时间其可靠性已经足够。盲目追求RedLock会引入极大的复杂性和性能开销。Redisson虽然提供了RedissonRedLock实现但除非你有非常明确的、经过评估的需求否则不建议轻易使用。4.4 生产环境避坑指南与性能调优锁粒度要细锁的Key要精确到具体的数据项如lock:order:123而不是整个表或整个服务如lock:order_service。粗粒度的锁会严重限制并发度。设置合理的超时时间无论是等待时间(waitTime)还是租期时间(leaseTime)。等待时间太短高并发下失败率高太长则系统响应延迟高。租期时间短于业务执行时间会导致锁提前释放太长则客户端故障后锁释放慢。避免在锁内执行耗时操作如远程HTTP调用、复杂的数据库查询、IO操作等。这会导致锁持有时间过长成为系统瓶颈。尽量只把最小必要的、对共享资源有竞争的操作放在锁内。做好降级和熔断分布式锁依赖Redis如果Redis集群不可用锁服务就瘫痪了。在设计业务时要考虑降级方案比如当获取锁失败或超时时是快速失败返回用户“系统繁忙”还是使用一个本地降级策略如本地限流。监控与告警监控Redis的内存、连接数、命令延迟。监控业务中获取锁的成功率、平均等待时间、持有时间。设置告警当锁等待时间过长或失败率飙升时能及时发现问题。测试一定要进行压力测试。模拟高并发场景下分布式锁是否能正确工作会不会出现超卖、死锁虽然Redisson有超时机制但逻辑死锁仍需避免等问题。5. 完整项目示例模拟商品库存秒杀让我们整合以上所有知识构建一个更贴近真实场景的、带有降级策略的秒杀服务示例。1. 项目结构概览src/main/java/com/example/demolock/ ├── DemoLockApplication.java ├── config │ └── RedissonConfig.java (可选用于自定义配置) ├── controller │ └── SeckillController.java ├── service │ └── impl │ └── SeckillServiceImpl.java ├── dao │ ├── InventoryMapper.java (MyBatis Plus示例) │ └── ProductCacheDao.java └── entity └── Inventory.java2. 核心服务实现Service Slf4j public class SeckillServiceImpl implements SeckillService { Autowired private RedissonClient redissonClient; Autowired private InventoryMapper inventoryMapper; Autowired private ProductCacheDao cacheDao; // 引入一个简单的令牌桶作为本地降级限流 private final RateLimiter localLimiter RateLimiter.create(100.0); // 每秒100个令牌 Override Transactional(rollbackFor Exception.class) // 注意锁与事务的先后顺序 public SeckillResult seckillWithLock(Long productId, Long userId) { SeckillResult result new SeckillResult(); result.setProductId(productId); result.setUserId(userId); // 前置检查本地限流降级 if (!localLimiter.tryAcquire()) { result.setSuccess(false); result.setMessage(系统繁忙请稍后再试本地限流); return result; } // 构建分布式锁Key String lockKey lock:seckill:stock: productId; RLock lock redissonClient.getLock(lockKey); boolean isLocked false; try { // 尝试获取锁等待时间5秒使用看门狗机制不指定租期 isLocked lock.tryLock(5, -1, TimeUnit.SECONDS); if (!isLocked) { log.warn(用户{}秒杀商品{}获取分布式锁失败可能并发过高, userId, productId); result.setSuccess(false); result.setMessage(抢购人数过多请重试); return result; } log.info(用户{}成功获取锁开始处理商品{}, userId, productId); // --- 核心业务逻辑开始 --- // 1. 查询并校验库存 (悲观锁select ... for update 这里用分布式锁替代了) Inventory inventory inventoryMapper.selectById(productId); if (inventory null) { result.setSuccess(false); result.setMessage(商品不存在); return result; } if (inventory.getStock() 0) { result.setSuccess(false); result.setMessage(商品已售罄); return result; } // 2. 扣减库存 int updateCount inventoryMapper.decreaseStock(productId, 1); // 使用原子操作 update set stock stock -1 where id? and stock 0 if (updateCount 0) { // 原子操作失败说明库存已不足防御性编程 result.setSuccess(false); result.setMessage(商品库存不足请刷新重试); return result; } // 3. 创建订单模拟 // orderService.createSeckillOrder(...); log.info(用户{}秒杀商品{}成功库存扣减完成, userId, productId); // 4. 更新缓存异步或延迟双删 cacheDao.evictCache(productId); result.setSuccess(true); result.setMessage(秒杀成功); // --- 核心业务逻辑结束 --- } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(秒杀过程被中断productId: {}, userId: {}, productId, userId, e); result.setSuccess(false); result.setMessage(系统异常秒杀中断); } catch (Exception e) { log.error(秒杀过程发生未知异常productId: {}, userId: {}, productId, userId, e); result.setSuccess(false); result.setMessage(系统异常秒杀失败); // 根据异常类型决定是否回滚事务 throw e; } finally { // 安全释放锁 if (isLocked lock.isHeldByCurrentThread()) { try { lock.unlock(); log.debug(用户{}释放商品{}的锁, userId, productId); } catch (IllegalMonitorStateException e) { // 锁可能已自动过期忽略此异常或记录日志 log.warn(释放锁时发生异常可能锁已自动过期productId: {}, productId); } } } return result; } }3. 关键点剖析与进阶思考锁与事务的顺序代码中先加锁再开启事务Transactional。这个顺序很重要。如果先开事务再加锁在锁释放后、事务提交前其他线程可能读到未提交的数据脏读。虽然数据库隔离级别可以缓解但先锁后事务是更清晰的模式。原子化库存扣减即使在锁内更新库存时也使用了decreaseStock这样的原子操作update ... where stock 0。这是第二道防线防止极端情况下如锁逻辑有BUG的超卖。本地限流降级在进入分布式锁竞争前先用Guava的RateLimiter做一层本地限流。这能在Redis出现问题时或瞬时流量极高时保护下游数据库和服务避免雪崩。缓存更新业务成功后要使对应商品的缓存失效。这里可以采用“先删缓存再更新DB”的策略或者更复杂的“延迟双删”来避免缓存一致性问题。这是一个独立的话题但和分布式锁协同工作至关重要。异常处理与锁释放finally块中的释放逻辑是健壮性的保证。捕获IllegalMonitorStateException是因为在高并发或网络波动下锁可能刚好在unlock()调用前因过期而被自动释放。这个例子展示了一个相对完整的、考虑了一定生产环境因素的秒杀场景。实际项目中还需要结合消息队列进行异步下单、使用缓存预热、进行更精细的限流熔断等。分布式锁是保证数据一致性的重要工具但它不是银弹需要融入到整个系统架构中与其他组件配合才能构建出高并发、高可用的服务。
SpringBoot集成Redisson实现分布式锁:从原理到秒杀实战
1. 项目背景与核心需求为什么是Redisson在微服务架构和分布式系统成为主流的今天一个看似简单的“库存扣减”操作都可能因为多个服务实例同时执行而引发超卖问题。传统的单机锁如Java的synchronized或ReentrantLock在单体应用时代尚能一战但在分布式环境下它们的作用范围仅限于单个JVM进程对部署在其他服务器上的服务实例无能为力。这时我们需要一个所有服务实例都能“看见”并共同遵守的锁机制这就是分布式锁。实现分布式锁的方案有很多比如基于数据库唯一索引、基于ZooKeeper的临时顺序节点以及基于Redis的SETNX命令。其中基于Redis的方案因其高性能和简单易用而广受欢迎。然而如果你直接使用Redis的SET key value NX PX timeout命令手动实现很快就会遇到一系列棘手的问题锁的续期怎么办锁释放时的原子性如何保证如何实现可重入这些细节处理不当轻则导致锁失效重则引发业务数据错乱。Redisson的出现正是为了解决这些“脏活累活”。它是一个在Redis基础上实现的Java驻内存数据网格客户端将复杂的分布式锁逻辑封装成了简单易用的API。它提供的RLock对象其接口和使用方式几乎与JDK的ReentrantLock一致让开发者能以最小的学习成本获得一个生产级可用的分布式锁实现。它内部帮你处理了锁续期看门狗机制、锁释放的Lua脚本原子性操作、可重入性、公平锁等高级特性。因此在SpringBoot项目中选择Redisson来实现分布式锁是一个兼顾了可靠性、易用性和性能的明智选择。2. 环境搭建与Redisson集成配置在开始编码之前我们需要一个可运行的SpringBoot项目环境并完成Redisson客户端的集成。这里我假设你使用Maven进行依赖管理并有一个基础的SpringBoot 2.x 或 3.x 项目。2.1 引入核心依赖首先在项目的pom.xml文件中添加Redisson的Spring Boot Starter依赖。这个Starter会自动配置Redisson客户端比手动配置要方便得多。dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.0/version !-- 请使用最新稳定版本 -- /dependency为什么选择Starter而不是核心库redisson-spring-boot-starter除了包含核心的redisson依赖还提供了与Spring环境自动集成的能力比如根据application.yml配置自动创建RedissonClientBean。如果你只用核心库就需要自己写一堆Bean配置徒增工作量。2.2 配置文件详解单机、哨兵与集群模式Redisson支持多种Redis部署模式。绝大多数开发和测试环境使用单机模式就已足够。我们在application.yml中进行配置。单机模式配置spring: redis: # 这些是Spring Boot Redis的通用配置Redisson Starter也会读取 host: 127.0.0.1 port: 6379 database: 0 password: yourpassword # 如果没有密码则删除此行或留空 # Redisson专属配置优先级更高更全面 redisson: config: | singleServerConfig: address: redis://${spring.redis.host}:${spring.redis.port} password: ${spring.redis.password} database: ${spring.redis.database} # 连接池配置对性能影响很大 connectionPoolSize: 64 # 最大连接数 connectionMinimumIdleSize: 24 # 最小空闲连接数 idleConnectionTimeout: 10000 # 连接空闲超时单位毫秒 connectTimeout: 10000 # 连接超时 timeout: 3000 # 命令等待超时 retryAttempts: 3 # 命令失败重试次数 retryInterval: 1500 # 命令重试发送时间间隔关键参数解析与调优建议connectionPoolSize和connectionMinimumIdleSize这是影响并发性能的关键。如果你的应用并发量很高且Redis服务器资源充足可以适当调大这两个值。connectionMinimumIdleSize设置一个常驻空闲连接池可以避免突发请求时临时建立连接的开销。生产环境建议根据压测结果调整。idleConnectionTimeout连接空闲多久后释放。设置过短会导致频繁创建连接过长则浪费资源。10秒是一个比较折中的值。timeout执行Redis命令的超时时间。如果你的业务逻辑复杂或网络延迟高可以适当调大避免在Redis响应慢时误判为失败。哨兵与集群模式配置示例如果你的生产环境是高可用的Redis哨兵或集群配置如下redisson: config: | # 哨兵模式 sentinelServersConfig: sentinelAddresses: - redis://sentinel1:26379 - redis://sentinel2:26379 - redis://sentinel3:26379 masterName: mymaster password: yourpassword # 或者集群模式 clusterServersConfig: nodeAddresses: - redis://cluster-node1:6379 - redis://cluster-node2:6379 - redis://cluster-node3:6379 password: yourpassword配置完成后Spring Boot会自动创建一个RedissonClient实例并注入到IoC容器中我们在业务代码中直接Autowired使用即可。3. 分布式锁核心API与基础用法实战Redisson的分布式锁核心接口是RLock它继承了java.util.concurrent.locks.Lock接口所以如果你熟悉ReentrantLock那么上手RLock会非常快。3.1 基础加锁与解锁让我们从一个最基础的场景开始秒杀活动中扣减库存。Service public class SeckillService { Autowired private RedissonClient redissonClient; Autowired private InventoryMapper inventoryMapper; // 假设的库存Mapper public boolean seckillProduct(Long productId) { // 1. 构造锁的Key。这是关键必须保证业务唯一性。 // 格式建议业务前缀:业务标识如 lock:seckill:product:1001 String lockKey lock:seckill:product: productId; RLock lock redissonClient.getLock(lockKey); // 2. 尝试加锁 try { // 尝试获取锁最多等待10秒锁持有时间设置为30秒 boolean isLocked lock.tryLock(10, 30, TimeUnit.SECONDS); if (!isLocked) { // 获取锁失败可能是系统繁忙或死锁直接返回秒杀失败 log.warn(获取分布式锁失败productId: {}, productId); return false; } // 3. 成功获取锁执行核心业务逻辑 log.info(成功获取锁开始处理库存扣减productId: {}, productId); // 查询当前库存 Inventory inventory inventoryMapper.selectById(productId); if (inventory null || inventory.getStock() 0) { return false; } // 扣减库存 inventory.setStock(inventory.getStock() - 1); inventoryMapper.updateById(inventory); // 模拟其他业务操作耗时 Thread.sleep(100); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 log.error(秒杀过程被中断, e); return false; } finally { // 4. 无论如何最终必须释放锁 if (lock.isHeldByCurrentThread()) { // 重要检查是否当前线程还持有锁 lock.unlock(); log.info(锁已释放productId: {}, productId); } } } }代码逐行解析与避坑指南构造锁Key (lockKey)这是分布式锁的“身份证”。必须确保在同一个业务维度下是全局唯一的。通常使用“业务类型:业务ID”的格式。切忌使用固定的Key如global_lock这会导致不同业务间不必要的竞争成为性能瓶颈。tryLock(long waitTime, long leaseTime, TimeUnit unit)这是最常用的加锁方法。waitTime获取锁的最大等待时间。如果设置为0则获取不到锁立即返回false。设置一个合理的等待时间如5-10秒可以避免瞬时高并发下大量请求立即失败起到“排队”和缓冲的作用。leaseTime锁的持有时间。这是Redisson分布式锁最核心的机制之一也是容易踩坑的地方。如果leaseTime设置为-1或使用lock.lock()Redisson会启动一个“看门狗”Watchdog线程在业务执行期间每隔leaseTime / 3的时间默认10秒检查一次如果业务还在执行且锁仍被当前线程持有就自动将锁的过期时间重置为初始值默认30秒。这有效防止了因为业务执行时间过长导致锁自动过期而被其他线程获取的问题。如果leaseTime设置为一个大于0的具体值如30秒看门狗机制将不会启动。锁会在设定的时间后自动过期。这意味着你必须确保你的业务逻辑在leaseTime内一定能执行完毕否则锁会提前释放导致数据不一致。对于执行时间不确定的业务强烈建议使用看门狗模式即不指定leaseTime或设为-1。释放锁 (lock.unlock())必须在finally块中执行确保异常时锁也能被释放避免死锁。同时务必先调用lock.isHeldByCurrentThread()进行检查。因为锁可能因为网络问题、看门狗续期失败或业务超时导致自动过期此时当前线程已不再持有锁如果强行解锁Redisson会抛出IllegalMonitorStateException异常。3.2 可重入锁与公平锁可重入性RLock是可重入锁。这意味着同一个线程可以多次获取同一把锁而不会把自己锁死。这在递归调用或一个方法内需要多次加锁同一资源的场景下非常有用。Redisson内部通过Redis的Hash结构存储锁信息其中包含了线程ID和重入次数。public void reentrantMethod(String key) { RLock lock redissonClient.getLock(key); lock.lock(); try { // 在锁内再次调用需要同一把锁的方法 innerMethod(key); } finally { lock.unlock(); } } private void innerMethod(String key) { RLock lock redissonClient.getLock(key); // 获取的是同一把锁 lock.lock(); // 同一个线程这里会直接增加重入次数不会阻塞 try { // 内部业务逻辑 } finally { lock.unlock(); // 减少重入次数直到为0才会真正释放锁 } }公平锁默认的RLock是非公平锁获取锁的顺序与请求的顺序无关谁抢到是谁的。Redisson也提供了公平锁的实现它保证了等待时间最长的线程优先获得锁。RLock fairLock redissonClient.getFairLock(fairLockKey); fairLock.lock(); try { // 业务逻辑 } finally { fairLock.unlock(); }注意公平锁的实现比非公平锁复杂性能开销也更大因为它需要在Redis中维护一个等待队列。除非业务有严格的先来后到的顺序要求否则建议使用默认的非公平锁以获得更高吞吐。4. 高级特性与生产环境最佳实践掌握了基础用法我们来看看如何让分布式锁在生产环境中更稳健、更高效。4.1 看门狗机制深度剖析与配置前面提到了看门狗这里深入一下。当你调用lock()或tryLock()时不指定leaseTime看门狗就会启动。工作原理加锁成功时在Redis中设置的Key过期时间默认是30秒。后台启动一个定时调度任务看门狗每隔10秒lockWatchdogTimeout / 3检查一次。检查时如果客户端还“活着”持有锁的JVM进程未挂并且锁依然存在则通过Lua脚本将锁的过期时间重新设置为30秒。配置看门狗超时时间 默认的30秒可能不适合所有业务。你可以在Redisson配置中修改它。redisson: config: | singleServerConfig: address: redis://127.0.0.1:6379 lockWatchdogTimeout: 30000 # 单位毫秒默认30000即30秒什么时候应该调整lockWatchdogTimeout调大如果你的业务逻辑平均执行时间很长例如超过20秒为了避免频繁续期带来的网络开销可以适当调大比如设置为60秒。但要注意这也会导致客户端崩溃后锁需要更长时间才能自动释放。调小如果你的业务逻辑通常很短几秒内可以适当调小比如15秒。这样在客户端崩溃时锁能更快被释放减少系统不可用时间。但续期会更频繁。一个重要的坑tryLock(long time, TimeUnit unit)方法这个方法只传一个等待时间不传租期时间。它的租期时间用的就是lockWatchdogTimeout。这意味着如果你用这个方法看门狗机制是生效的。务必确保你的业务逻辑执行时间不会远超过lockWatchdogTimeout。4.2 读写锁ReadWriteLock的应用场景分布式读写锁RReadWriteLock允许多个读锁同时持有但写锁是排他的。这非常适合“读多写少”的场景可以大幅提升系统的并发读取能力。Service public class ProductService { Autowired private RedissonClient redissonClient; Autowired private ProductCacheDao cacheDao; // 模拟缓存访问 // 读操作多个线程可并发执行 public Product getProduct(Long id) { String rwLockKey rwlock:product: id; RReadWriteLock rwLock redissonClient.getReadWriteLock(rwLockKey); RLock readLock rwLock.readLock(); readLock.lock(); try { // 从缓存读取产品信息 Product product cacheDao.getFromCache(id); if (product ! null) { return product; } // 缓存不存在模拟从DB读取这里实际也应加锁但为演示简化 // ... return product; } finally { readLock.unlock(); } } // 写操作独占执行 public void updateProduct(Product product) { String rwLockKey rwlock:product: product.getId(); RReadWriteLock rwLock redissonClient.getReadWriteLock(rwLockKey); RLock writeLock rwLock.writeLock(); writeLock.lock(); try { // 更新数据库 // ... // 清除或更新缓存 cacheDao.evictCache(product.getId()); } finally { writeLock.unlock(); } } }使用要点读写锁的Key同样需要根据业务数据维度设计。写锁会阻塞所有读锁和写锁读锁只会阻塞写锁。要小心“写锁饥饿”问题即一直有读请求导致写请求永远无法获取锁。Redisson的公平锁策略可以在一定程度上缓解这个问题。4.3 联锁MultiLock与红锁RedLock辨析这是分布式锁中高级且容易混淆的概念。联锁MultiLock将多个RLock对象关联成一个锁。只有当你同时获取了所有这些锁时才算加锁成功。这用于需要同时锁定多个独立资源的场景。public void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) { RLock lock1 redissonClient.getLock(lock:account: fromAccountId); RLock lock2 redissonClient.getLock(lock:account: toAccountId); RLock multiLock redissonClient.getMultiLock(lock1, lock2); // 创建联锁 multiLock.lock(); try { // 对两个账户进行转账操作需要同时锁定双方账户 accountService.debit(fromAccountId, amount); accountService.credit(toAccountId, amount); } finally { multiLock.unlock(); } }红锁RedLock这是一个用于提升分布式锁可靠性的算法特别是在Redis集群模式下。它的核心思想是为了在某个主从架构的Redis集群中获得锁客户端需要向超过半数N/2 1的、相互独立的Redis主节点申请锁且每个锁都有相同的过期时间。只有当从大多数节点都成功获取锁时才算真正加锁成功。重要提示Martin Kleppmann《数据密集型应用系统设计》作者曾与Redis作者Antirez就RedLock算法的安全性进行过激烈辩论。目前社区普遍认为在需要强一致性保证的极端场景下如金融交易核心链路RedLock可能仍存在理论上的边界问题。对于绝大多数应用场景使用单Redis实例配合AOF持久化和fsyncalways或Redis哨兵/集群模式并合理设置锁超时时间其可靠性已经足够。盲目追求RedLock会引入极大的复杂性和性能开销。Redisson虽然提供了RedissonRedLock实现但除非你有非常明确的、经过评估的需求否则不建议轻易使用。4.4 生产环境避坑指南与性能调优锁粒度要细锁的Key要精确到具体的数据项如lock:order:123而不是整个表或整个服务如lock:order_service。粗粒度的锁会严重限制并发度。设置合理的超时时间无论是等待时间(waitTime)还是租期时间(leaseTime)。等待时间太短高并发下失败率高太长则系统响应延迟高。租期时间短于业务执行时间会导致锁提前释放太长则客户端故障后锁释放慢。避免在锁内执行耗时操作如远程HTTP调用、复杂的数据库查询、IO操作等。这会导致锁持有时间过长成为系统瓶颈。尽量只把最小必要的、对共享资源有竞争的操作放在锁内。做好降级和熔断分布式锁依赖Redis如果Redis集群不可用锁服务就瘫痪了。在设计业务时要考虑降级方案比如当获取锁失败或超时时是快速失败返回用户“系统繁忙”还是使用一个本地降级策略如本地限流。监控与告警监控Redis的内存、连接数、命令延迟。监控业务中获取锁的成功率、平均等待时间、持有时间。设置告警当锁等待时间过长或失败率飙升时能及时发现问题。测试一定要进行压力测试。模拟高并发场景下分布式锁是否能正确工作会不会出现超卖、死锁虽然Redisson有超时机制但逻辑死锁仍需避免等问题。5. 完整项目示例模拟商品库存秒杀让我们整合以上所有知识构建一个更贴近真实场景的、带有降级策略的秒杀服务示例。1. 项目结构概览src/main/java/com/example/demolock/ ├── DemoLockApplication.java ├── config │ └── RedissonConfig.java (可选用于自定义配置) ├── controller │ └── SeckillController.java ├── service │ └── impl │ └── SeckillServiceImpl.java ├── dao │ ├── InventoryMapper.java (MyBatis Plus示例) │ └── ProductCacheDao.java └── entity └── Inventory.java2. 核心服务实现Service Slf4j public class SeckillServiceImpl implements SeckillService { Autowired private RedissonClient redissonClient; Autowired private InventoryMapper inventoryMapper; Autowired private ProductCacheDao cacheDao; // 引入一个简单的令牌桶作为本地降级限流 private final RateLimiter localLimiter RateLimiter.create(100.0); // 每秒100个令牌 Override Transactional(rollbackFor Exception.class) // 注意锁与事务的先后顺序 public SeckillResult seckillWithLock(Long productId, Long userId) { SeckillResult result new SeckillResult(); result.setProductId(productId); result.setUserId(userId); // 前置检查本地限流降级 if (!localLimiter.tryAcquire()) { result.setSuccess(false); result.setMessage(系统繁忙请稍后再试本地限流); return result; } // 构建分布式锁Key String lockKey lock:seckill:stock: productId; RLock lock redissonClient.getLock(lockKey); boolean isLocked false; try { // 尝试获取锁等待时间5秒使用看门狗机制不指定租期 isLocked lock.tryLock(5, -1, TimeUnit.SECONDS); if (!isLocked) { log.warn(用户{}秒杀商品{}获取分布式锁失败可能并发过高, userId, productId); result.setSuccess(false); result.setMessage(抢购人数过多请重试); return result; } log.info(用户{}成功获取锁开始处理商品{}, userId, productId); // --- 核心业务逻辑开始 --- // 1. 查询并校验库存 (悲观锁select ... for update 这里用分布式锁替代了) Inventory inventory inventoryMapper.selectById(productId); if (inventory null) { result.setSuccess(false); result.setMessage(商品不存在); return result; } if (inventory.getStock() 0) { result.setSuccess(false); result.setMessage(商品已售罄); return result; } // 2. 扣减库存 int updateCount inventoryMapper.decreaseStock(productId, 1); // 使用原子操作 update set stock stock -1 where id? and stock 0 if (updateCount 0) { // 原子操作失败说明库存已不足防御性编程 result.setSuccess(false); result.setMessage(商品库存不足请刷新重试); return result; } // 3. 创建订单模拟 // orderService.createSeckillOrder(...); log.info(用户{}秒杀商品{}成功库存扣减完成, userId, productId); // 4. 更新缓存异步或延迟双删 cacheDao.evictCache(productId); result.setSuccess(true); result.setMessage(秒杀成功); // --- 核心业务逻辑结束 --- } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(秒杀过程被中断productId: {}, userId: {}, productId, userId, e); result.setSuccess(false); result.setMessage(系统异常秒杀中断); } catch (Exception e) { log.error(秒杀过程发生未知异常productId: {}, userId: {}, productId, userId, e); result.setSuccess(false); result.setMessage(系统异常秒杀失败); // 根据异常类型决定是否回滚事务 throw e; } finally { // 安全释放锁 if (isLocked lock.isHeldByCurrentThread()) { try { lock.unlock(); log.debug(用户{}释放商品{}的锁, userId, productId); } catch (IllegalMonitorStateException e) { // 锁可能已自动过期忽略此异常或记录日志 log.warn(释放锁时发生异常可能锁已自动过期productId: {}, productId); } } } return result; } }3. 关键点剖析与进阶思考锁与事务的顺序代码中先加锁再开启事务Transactional。这个顺序很重要。如果先开事务再加锁在锁释放后、事务提交前其他线程可能读到未提交的数据脏读。虽然数据库隔离级别可以缓解但先锁后事务是更清晰的模式。原子化库存扣减即使在锁内更新库存时也使用了decreaseStock这样的原子操作update ... where stock 0。这是第二道防线防止极端情况下如锁逻辑有BUG的超卖。本地限流降级在进入分布式锁竞争前先用Guava的RateLimiter做一层本地限流。这能在Redis出现问题时或瞬时流量极高时保护下游数据库和服务避免雪崩。缓存更新业务成功后要使对应商品的缓存失效。这里可以采用“先删缓存再更新DB”的策略或者更复杂的“延迟双删”来避免缓存一致性问题。这是一个独立的话题但和分布式锁协同工作至关重要。异常处理与锁释放finally块中的释放逻辑是健壮性的保证。捕获IllegalMonitorStateException是因为在高并发或网络波动下锁可能刚好在unlock()调用前因过期而被自动释放。这个例子展示了一个相对完整的、考虑了一定生产环境因素的秒杀场景。实际项目中还需要结合消息队列进行异步下单、使用缓存预热、进行更精细的限流熔断等。分布式锁是保证数据一致性的重要工具但它不是银弹需要融入到整个系统架构中与其他组件配合才能构建出高并发、高可用的服务。