最近在开发一个分布式任务调度系统时遇到了一个非常棘手的问题多个服务节点同时处理任务由于缺乏有效的分布式锁机制导致同一个任务被重复执行造成了数据不一致和资源浪费。排查后发现问题的核心在于对分布式锁的理解不够深入尤其是在高并发场景下的选型与实现。本文将围绕分布式锁这一核心技术从概念、选型到实战为你完整拆解一套在 Spring Boot 项目中集成 Redisson 实现高可靠分布式锁的闭环方案。无论你是正在学习分布式系统的学生还是需要在生产环境中解决并发问题的后端开发者都能从本文中找到可直接复用的代码和清晰的避坑指南。1. 背景与核心概念为什么需要分布式锁在单机多线程环境下我们可以使用synchronized关键字或ReentrantLock来保证同一时刻只有一个线程能访问共享资源这是进程内锁或线程锁。然而在微服务或分布式架构中应用被部署在多个独立的 JVM 进程甚至多台物理服务器上。这些进程之间的内存是隔离的单机的锁机制如synchronized完全失效。此时我们需要一个所有服务节点都能共同访问和认可的“协调者”来充当锁的仲裁者这就是分布式锁。分布式锁的核心目标在分布式系统或集群环境中控制对共享资源如数据库某一行、一个文件、一个业务ID的访问确保在同一时间只有一个客户端或线程能执行特定的代码逻辑。常见应用场景防止重复提交用户快速点击提交订单按钮后端需要确保同一订单号只被处理一次。秒杀库存扣减成千上万的请求同时抢购一件商品必须保证库存扣减的原子性不能超卖。定时任务调度在集群中部署了多个相同的定时任务实例需要保证同一时刻只有一个实例执行任务避免重复执行。分布式全局序列号生成生成全局唯一的订单号、流水号需要保证递增序列的原子性。为什么需要深入掌握简单地使用一个setnx命令并不足以应对生产环境的复杂性。你需要考虑锁的可重入性同一个线程可多次获取锁、锁超时防止死锁、锁续期看门狗机制、锁释放的原子性避免误删他人锁以及高可用性锁服务本身不能是单点。接下来我们将从主流实现方案对比开始。2. 环境准备与版本说明在开始编码之前请确保你的开发环境满足以下要求。本文的示例将基于最常用的技术栈进行演示。基础运行环境操作系统Windows 10/11 macOS 或 Linux如 Ubuntu 20.04均可。本文命令以 Linux/macOS 的 Bash 为例Windows 用户可使用 Git Bash 或 WSL。Java 开发套件 (JDK)版本 8 或 11推荐 11。本文使用 OpenJDK 11。java -version # 输出应类似openjdk version 11.0.15 2022-04-19项目管理与构建工具Apache Maven 3.6 或 Gradle 6.8。本文使用 Maven。mvn -v # 输出应包含 Apache Maven 3.8.6集成开发环境 (IDE)IntelliJ IDEA推荐、Eclipse 或 VS Code。核心依赖与中间件Spring Boot2.7.x 版本本文使用 2.7.18。这是当前长期支持版本之一稳定且生态丰富。Redis5.0 版本本文使用 Redis 7.0。Redis 将作为分布式锁的存储和协调中心。你需要确保有一个可访问的 Redis 服务。安装与启动参考使用 Docker 最便捷# 拉取 Redis 7.0 镜像 docker pull redis:7.0 # 运行 Redis 容器暴露 6379 端口 docker run -d --name my-redis -p 6379:6379 redis:7.0 # 测试连接 docker exec -it my-redis redis-cli ping # 应返回 PONGRedisson3.17.x 版本本文使用 3.17.7。Redisson 是一个在 Redis 基础上实现的 Java 驻内存数据网格客户端它提供了丰富易用的分布式对象和服务其中就包括我们需要的分布式锁。示例项目结构我们将创建一个标准的 Spring Boot 项目。初始化后的目录结构大致如下distributed-lock-demo/ ├── pom.xml (Maven 配置文件) ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── demo/ │ │ │ ├── DistributedLockDemoApplication.java (启动类) │ │ │ ├── config/ (配置类目录) │ │ │ ├── service/ (业务服务目录) │ │ │ └── controller/ (Web控制器目录) │ │ └── resources/ │ │ ├── application.yml (或 application.properties) │ │ └── ... │ └── test/ (测试目录) └── ...如果你的具体环境版本与上述不同例如使用 Spring Boot 3.x 或 Gradle不必担心核心配置思路和 API 用法是相通的只需在依赖版本上做相应调整即可。3. 分布式锁方案选型与 Redisson 原理在 Java 领域实现分布式锁主要有以下几种方案各有优劣方案实现方式优点缺点适用场景基于数据库利用数据库的唯一约束或乐观锁版本号。实现简单依赖少。性能差对数据库压力大锁无失效时间容易死锁。并发量极低且不允许引入额外中间件的场景。基于 ZooKeeper创建临时有序节点序号最小的节点获取锁。高可靠性原生支持 Watcher 机制锁释放及时。性能比 Redis 差需要维护 ZooKeeper 集群客户端需处理会话过期。对一致性要求极高且并发量不是首要考量的场景如配置中心、选主。基于 Redis使用SET key value NX PX timeout命令。性能极高实现相对简单。需要自己处理锁续期、原子性释放等问题单节点 Redis 有宕机风险。高并发、对性能要求苛刻且允许极低概率锁失效的场景。基于 Redisson在 Redis 基础上封装提供看门狗、可重入锁等高级特性。功能完善可重入、公平锁、联锁等高可用支持主从、哨兵、集群简单易用。引入额外客户端依赖。生产环境推荐方案适用于绝大多数需要强一致性和高可用分布式锁的场景。为什么选择 Redisson因为它完美解决了原生 Redis 实现分布式锁的诸多痛点可重入锁Reentrant Lock同一个线程可以多次获取同一把锁锁内部有一个计数器获取时1释放时-1计数器为0时才真正释放。这对于递归调用或嵌套方法非常必要。锁续期Watchdog当你的业务逻辑执行时间超过了锁的初始超时时间Redisson 的“看门狗”机制会自动为你续期防止业务未执行完锁就过期导致其他线程误入。默认看门狗检查锁的超时时间是30秒每10秒续期一次。锁释放的原子性Redisson 使用 Lua 脚本在 Redis 端原子性地判断锁标识和释放锁避免了“判断锁归属”和“释放锁”两个操作之间的竞态条件彻底解决了误删他人锁的问题。丰富的锁类型除了基本的可重入锁还提供了公平锁、联锁、红锁、读写锁等满足复杂业务场景。Redisson 分布式锁核心原理简析 当你调用lock.lock()时Redisson 会执行一个 Lua 脚本到 Redis。这个脚本主要做两件事判断key是否存在。如果不存在则用HSET命令设置一个 Hash 结构字段为客户端唯一标识UUID线程ID值为1重入次数并设置一个过期时间。如果key已存在则检查 Hash 中的客户端标识是否与当前请求一致。如果一致则将重入次数1并重新设置过期时间。锁的释放lock.unlock()同样通过 Lua 脚本实现原子操作检查客户端标识匹配则重入次数-1如果减到0则删除key。4. 完整实战在 Spring Boot 中集成 Redisson接下来我们一步步构建一个完整的示例。假设我们有一个“商品库存扣减”的场景。4.1 创建 Spring Boot 项目并添加依赖使用 Spring Initializr 或 IDE 创建一个新的 Spring Boot 项目选择Web和Lombok依赖Lombok 用于简化代码。在pom.xml中手动添加redisson-spring-boot-starter依赖。注意版本匹配Spring Boot 2.7.x 通常对应 Redisson 3.17.x。?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version !-- 使用稳定的 2.7.x 版本 -- relativePath/ /parent groupIdcom.example/groupId artifactIddistributed-lock-demo/artifactId version0.0.1-SNAPSHOT/version namedistributed-lock-demo/name descriptionDemo project for distributed lock with Redisson/description properties java.version11/java.version redisson.version3.17.7/redisson.version !-- 指定 Redisson 版本 -- /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Redisson Starter -- dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version${redisson.version}/version /dependency !-- Lombok -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration excludes exclude groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /exclude /excludes /configuration /plugin /plugins /build /project4.2 配置 Redisson 连接在src/main/resources/application.yml中配置 Redis 连接信息。这里我们连接本地 Docker 启动的 Redis。spring: redis: # Redis 服务器地址如果是远程或云服务请修改此处 host: localhost port: 6379 # 如果有密码配置 password # password: yourpassword database: 0 # 默认使用 0 号数据库 # Redisson 特定配置 (可选大部分情况使用默认即可) redisson: # 单节点模式配置 single-server-config: address: redis://${spring.redis.host}:${spring.redis.port} # 连接池大小 connectionPoolSize: 64 connectionMinimumIdleSize: 24 # 如果 spring.redis.password 设置了这里会自动继承无需重复配置Redisson Spring Boot Starter 会自动读取spring.redis.*的配置并创建RedissonClient实例注入到 Spring 容器中。你也可以通过Configuration类进行更复杂的配置如集群模式、哨兵模式但单机模式上述配置足够。4.3 编写库存服务与锁应用首先我们模拟一个商品库存表。在实际项目中这对应数据库的一张表。这里我们用一个ConcurrentHashMap在内存中模拟。// 文件路径src/main/java/com/example/demo/service/InventoryService.java package com.example.demo.service; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * 模拟库存服务 */ Service Slf4j public class InventoryService { // 模拟数据库中的库存表key: 商品ID, value: 库存数量 private final MapLong, Integer inventoryMap new ConcurrentHashMap(); /** * 初始化一些测试数据 */ PostConstruct public void init() { inventoryMap.put(1001L, 10); // 商品ID 1001初始库存 10 inventoryMap.put(1002L, 5); // 商品ID 1002初始库存 5 log.info(库存初始化完成: {}, inventoryMap); } /** * 查询库存无需加锁 */ public Integer getStock(Long productId) { return inventoryMap.getOrDefault(productId, 0); } /** * 扣减库存 - 不安全版本用于对比 * 存在并发超卖问题 */ public boolean deductStockUnsafe(Long productId, Integer quantity) { Integer currentStock inventoryMap.get(productId); if (currentStock null || currentStock quantity) { log.warn(商品 {} 库存不足当前库存: {} 需要: {}, productId, currentStock, quantity); return false; } // 模拟一个耗时操作放大并发问题 try { Thread.sleep(10); // 10毫秒 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } int newStock currentStock - quantity; inventoryMap.put(productId, newStock); log.info(商品 {} 扣减 {} 成功剩余库存: {}, productId, quantity, newStock); return true; } /** * 扣减库存 - 安全版本使用 Redisson 分布式锁 */ public boolean deductStockWithLock(Long productId, Integer quantity) { // 锁的 key 应该与业务资源强相关这里使用商品ID String lockKey lock:inventory: productId; // 获取锁对象 RLock lock redissonClient.getLock(lockKey); boolean isLocked false; try { // 尝试加锁最多等待5秒锁持有时间10秒看门狗会自动续期 isLocked lock.tryLock(5, 10, java.util.concurrent.TimeUnit.SECONDS); if (!isLocked) { log.error(获取商品 {} 的分布式锁失败可能系统繁忙, productId); return false; } // 成功获取锁执行核心业务逻辑 return doDeductStock(productId, quantity); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(获取锁时被中断, e); return false; } finally { // 必须在 finally 块中释放锁确保锁一定会被释放 if (isLocked lock.isHeldByCurrentThread()) { lock.unlock(); log.debug(商品 {} 的分布式锁已释放, productId); } } } /** * 实际执行库存扣减的逻辑抽离出来方便加锁 */ private boolean doDeductStock(Long productId, Integer quantity) { Integer currentStock inventoryMap.get(productId); if (currentStock null || currentStock quantity) { log.warn(商品 {} 库存不足当前库存: {} 需要: {}, productId, currentStock, quantity); return false; } // 模拟业务处理耗时 try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } int newStock currentStock - quantity; inventoryMap.put(productId, newStock); log.info(【安全】商品 {} 扣减 {} 成功剩余库存: {}, productId, quantity, newStock); return true; } // 需要注入 RedissonClient private final org.redisson.api.RedissonClient redissonClient; public InventoryService(org.redisson.api.RedissonClient redissonClient) { this.redissonClient redissonClient; } }关键代码解释redissonClient.getLock(lockKey)获取一个RLock可重入锁对象。lockKey是锁在 Redis 中的键必须具有业务含义通常格式为lock:业务前缀:资源标识。lock.tryLock(waitTime, leaseTime, unit)这是推荐的使用方式。waitTime获取锁的最大等待时间。如果设置为0则获取不到锁立即返回。这里设置5秒意味着线程会尝试5秒去获取锁。leaseTime锁的持有时间。超过这个时间锁会自动失效。即使你的业务没有执行完。但 Redisson 的看门狗机制默认开启会在业务执行期间自动续期除非你显式指定leaseTime并关闭看门狗。最佳实践通常使用tryLock并设置一个合理的等待时间避免线程无限期阻塞。leaseTime 可以不指定或设为 -1使用看门狗默认机制。lock.unlock()释放锁。务必在finally块中执行以确保异常情况下锁也能被释放。释放前通过lock.isHeldByCurrentThread()检查当前线程是否持有该锁避免误释放。4.4 创建控制器进行测试创建一个简单的 HTTP 接口方便我们通过压测工具如 JMeter、Postman或浏览器进行并发测试。// 文件路径src/main/java/com/example/demo/controller/InventoryController.java package com.example.demo.controller; import com.example.demo.service.InventoryService; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; RestController RequestMapping(/inventory) Slf4j public class InventoryController { private final InventoryService inventoryService; public InventoryController(InventoryService inventoryService) { this.inventoryService inventoryService; } /** * 查询库存 */ GetMapping(/{productId}) public String getStock(PathVariable Long productId) { Integer stock inventoryService.getStock(productId); return String.format(商品 %d 的当前库存为: %d, productId, stock); } /** * 测试不安全扣减 (模拟并发超卖) */ PostMapping(/deduct/unsafe/{productId}) public String deductUnsafe(PathVariable Long productId, RequestParam(defaultValue 1) Integer quantity, RequestParam(defaultValue 50) Integer threadCount) throws InterruptedException { int totalRequests threadCount; CountDownLatch startLatch new CountDownLatch(1); CountDownLatch endLatch new CountDownLatch(totalRequests); ExecutorService executorService Executors.newFixedThreadPool(10); int initialStock inventoryService.getStock(productId); log.info(商品 {} 初始库存: {}, productId, initialStock); for (int i 0; i totalRequests; i) { executorService.submit(() - { try { startLatch.await(); // 等待所有线程就绪 inventoryService.deductStockUnsafe(productId, quantity); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { endLatch.countDown(); } }); } startLatch.countDown(); // 同时放行所有线程 endLatch.await(); // 等待所有线程执行完毕 executorService.shutdown(); int finalStock inventoryService.getStock(productId); log.info(商品 {} 最终库存: {}, productId, finalStock); int sold initialStock - finalStock; int expectedSold Math.min(totalRequests * quantity, initialStock); // 理论最大售出量 return String.format(测试完成。初始库存: %d, 最终库存: %d, 实际售出: %d, 理论最大售出: %d。可能存在超卖。, initialStock, finalStock, sold, expectedSold); } /** * 测试安全扣减 (使用分布式锁) */ PostMapping(/deduct/safe/{productId}) public String deductSafe(PathVariable Long productId, RequestParam(defaultValue 1) Integer quantity, RequestParam(defaultValue 50) Integer threadCount) throws InterruptedException { // 为了公平对比先重置库存实际生产环境不要这样做 // 这里简单重新初始化仅用于演示 // 实际测试前请重启应用或调用重置接口 int totalRequests threadCount; CountDownLatch startLatch new CountDownLatch(1); CountDownLatch endLatch new CountDownLatch(totalRequests); ExecutorService executorService Executors.newFixedThreadPool(10); int initialStock inventoryService.getStock(productId); log.info(商品 {} 初始库存: {}, productId, initialStock); for (int i 0; i totalRequests; i) { executorService.submit(() - { try { startLatch.await(); inventoryService.deductStockWithLock(productId, quantity); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { endLatch.countDown(); } }); } startLatch.countDown(); endLatch.await(); executorService.shutdown(); int finalStock inventoryService.getStock(productId); log.info(商品 {} 最终库存: {}, productId, finalStock); int sold initialStock - finalStock; int expectedSold Math.min(totalRequests * quantity, initialStock); return String.format(测试完成。初始库存: %d, 最终库存: %d, 实际售出: %d, 理论最大售出: %d。库存正确。, initialStock, finalStock, sold, expectedSold); } }4.5 运行与验证启动 Redis确保你的 Redis 服务正在运行例如通过前面提到的 Docker 命令。启动 Spring Boot 应用运行DistributedLockDemoApplication的main方法。测试查询接口打开浏览器或使用curl访问http://localhost:8080/inventory/1001应该看到库存为10。测试不安全扣减模拟超卖使用工具如 Postman并发调用POST http://localhost:8080/inventory/deduct/unsafe/1001?quantity1threadCount20。观察控制台日志你会发现大量的“扣减成功”日志但最终库存很可能为负数。因为20个线程几乎同时读到库存为10都判断为充足然后依次扣减导致超卖。测试安全扣减使用分布式锁重要由于我们是在内存中模拟库存为了再次测试你需要重启应用来重置库存为10。调用POST http://localhost:8080/inventory/deduct/safe/1001?quantity1threadCount20。观察控制台日志。这次日志会显示线程在“获取锁”和“释放锁”并且“扣减成功”的日志是顺序出现的。最终库存一定不会为负最多扣减到0。实际售出数量等于初始库存10证明了分布式锁有效防止了超卖。预期结果对比无锁情况20个请求可能成功扣减20次库存变为-10超卖。有锁情况20个请求只有前10个请求能成功获取锁并扣减库存后10个请求会因库存不足或获取锁超时而失败。库存最终为0符合预期。5. 常见问题与排查思路在实际使用 Redisson 分布式锁时你可能会遇到以下问题问题现象可能原因排查思路与解决方案获取锁一直失败返回 false1. 锁被其他客户端长期占用。2.waitTime设置太短在竞争激烈时来不及获取。3. Redis 连接异常或网络超时。1. 检查持有锁的客户端业务逻辑是否执行时间过长或发生死锁。可以通过 Redis 命令TTL lock_key查看锁剩余时间。2. 适当增加tryLock的waitTime或检查业务设计看是否锁粒度太粗导致竞争激烈。3. 检查 Redis 服务状态、网络连通性以及 Redisson 配置。抛出IllegalMonitorStateException当前线程尝试释放一个它并未持有的锁。通常发生在错误地释放了其他线程的锁或锁已因超时自动释放。务必在finally块中释放锁前使用lock.isHeldByCurrentThread()进行判断。确保释放锁的线程就是获取锁的线程。业务执行时间超过锁过期时间锁提前释放未使用看门狗机制且leaseTime设置过短。业务未执行完锁已过期被其他线程获取导致数据不一致。1.推荐使用lock.tryLock(waitTime, -1, unit)或lock.lock()不指定leaseTime启用看门狗默认开启。2. 如果必须指定leaseTime请确保其值远大于业务最大可能执行时间并考虑在业务代码中实现续期逻辑Redisson 看门狗已封装。Redis 集群模式下主节点宕机可能导致锁丢失在 Redis 异步复制的模式下锁信息可能还未同步到从节点主节点就宕机了此时从节点升级为主锁状态丢失。对于要求绝对强一致性的场景可以考虑使用 Redisson 的红锁RedLock。但请注意红锁性能较低且仍有争议。对于绝大多数业务场景主从哨兵模式已足够可靠需权衡一致性与性能。性能瓶颈高并发下大量线程在竞争同一把锁导致大部分线程处于等待状态系统吞吐量下降。1.细化锁粒度不要用一把大锁锁住整个方法或资源。例如库存扣减锁的 key 精确到商品ID而不是所有商品共用一把锁。2.考虑乐观锁如果冲突概率不高可以使用数据库的乐观锁版本号或 CAS 操作避免互斥等待。3.业务优化检查是否可以通过队列、批量处理等方式减少对锁的竞争。基础排查命令Redis CLI# 连接到 Redis redis-cli # 查看所有以 ‘lock:‘ 开头的键 KEYS lock:* # 查看特定锁的详细信息 (Redisson 锁是 Hash 类型) HGETALL lock:inventory:1001 # 查看锁的剩余生存时间 (TTL) TTL lock:inventory:10016. 最佳实践与工程建议将分布式锁应用到生产环境除了正确使用 API还需要遵循以下工程实践锁的粒度要尽可能细坏实践String lockKey global_inventory_lock;所有商品共用一把锁好实践String lockKey lock:inventory: productId;每个商品ID一把锁细粒度锁能极大减少竞争提升系统并发能力。锁的命名要有业务意义使用统一的命名规范如lock:业务模块:资源标识[:子资源]。例如lock:order:create:${userId}(用户订单创建锁)lock:coupon:grant:${batchId}(优惠券发放批次锁)。清晰的命名便于监控、排查和清理僵尸锁。总是使用 tryLock 并设置超时时间lock()方法会一直阻塞直到获取锁这在网络分区或客户端宕机时可能导致灾难。tryLock(timeout)允许你在指定时间内获取锁超时则快速失败进行降级处理如返回“系统繁忙请重试”。锁必须被释放且由加锁线程释放使用try-finally块确保锁释放。在finally中释放前用isHeldByCurrentThread()判断。避免在try块内使用return语句而跳过unlock。谨慎处理锁的自动续期看门狗默认情况下不指定leaseTime或设置为-1看门狗会工作。如果你显式指定了leaseTime看门狗机制将不会生效。此时你必须确保业务能在leaseTime内完成。对于执行时间不确定的长时间任务如文件处理、复杂计算建议使用默认看门狗。做好降级和熔断分布式锁依赖外部存储Redis它本身可能故障。在tryLock失败或获取锁超时时要有合理的业务降级策略而不是让流程彻底失败。例如可以记录日志、返回排队中状态、或使用本地限流做兜底。监控与告警监控 Redis 的内存、连接数、QPS。监控应用中对特定锁的等待时间、获取失败率。如果某个锁的竞争异常激烈高等待时间、高失败率可能意味着业务热点或锁粒度过粗需要优化。可以记录锁的获取和释放日志Debug级别便于线上问题追踪。在测试环境充分验证模拟网络延迟、Redis 宕机、应用实例重启等异常情况验证锁的行为是否符合预期。进行高并发压测验证锁的性能和正确性。掌握分布式锁的原理和 Redisson 的最佳实践能让你在构建高并发、高可用的分布式系统时更加从容地应对数据一致性的挑战。从理解问题本质开始选择合适的工具再到严谨的编码和全面的测试每一步都至关重要。