Spring Boot 中 Redis 分布式锁的正确使用姿势我后悔没早知道上个月项目里一个定时任务重复执行了两次导致数据被重复处理查了很久才发现是分布式锁失效了。当时我用的是 Redis SETNX 命令自认为写得没问题结果在并发场景下完全不行。这篇文章把踩过的坑和正确的做法分享出来希望你们别再重蹈覆辙。一、为什么你的分布式锁总失效先说说我踩的第一个坑。当时我自信满满地写了这样的代码publicBooleantryLock(StringlockKey,StringrequestId){BooleanresultredisTemplate.opsForValue().setIfAbsent(lockKey,requestId,10,TimeUnit.SECONDS);returnresult;}看起来挺完美对吧设置了 10 秒过期时间防止锁死。但是问题来了问题场景线程 A 获取锁后开始执行任务执行到一半时业务逻辑耗时超过了 10 秒锁自动过期释放。此时线程 B 正好进来也成功获取了锁。这样 A 和 B 同时在执行同一任务数据就被重复处理了。根本原因我没有正确理解 Redis 分布式锁的看门狗机制。锁的过期时间应该由业务执行时间动态决定而不是写死一个固定值。二、Redis 分布式锁的核心原理想用好分布式锁先得搞懂它的核心原理。Redis 分布式锁本质上是利用 SETNXSET if Not eXists命令的原子性// 伪代码获取锁if(redis.setnx(lockKey,requestId)1){// 获取成功设置过期时间redis.expire(lockKey,30);try{// 业务逻辑}finally{// 释放锁if(requestId.equals(redis.get(lockKey))){redis.del(lockKey);}}}但这段代码有三个致命问题问题1SETNX 和 EXPIRE 不是原子操作如果程序在 SETNX 成功后、EXPIRE 执行前崩溃锁就永远不会过期。问题2释放锁时没有验证价值如果线程 A 的锁过期了线程 B 获取了新锁此时线程 A 执行完毕去释放锁会把线程 B 的锁给释放掉。问题3无法自动续期业务执行时间超过锁过期时间锁自动失效其他线程可以进来搞破坏。三、Redisson 框架的正确打开方式说了这么多坑该上正确的解决方案了。Redisson 是 Java 中最成熟的 Redis 客户端之一它帮我们封装好了分布式锁的所有细节。1. 引入依赖dependencygroupIdorg.redisson/groupIdartifactIdredisson-spring-boot-starter/artifactIdversion3.24.3/version/dependency2. 配置 Redisson 客户端ConfigurationpublicclassRedissonConfig{Value(${spring.data.redis.host})privateStringhost;Value(${spring.data.redis.port})privateStringport;BeanpublicRedissonClientredissonClient(){ConfigconfignewConfig();Stringaddressredis://host:port;config.useSingleServer().setAddress(address).setPassword(yourpassword)// 如果有密码.setConnectionPoolSize(64).setConnectionMinimumIdleSize(10);returnRedisson.create(config);}}3. 使用分布式锁ServicepublicclassOrderService{AutowiredprivateRedissonClientredissonClient;publicvoidcreateOrder(Orderorder){StringlockKeyorder:lock:order.getProductId();RLocklockredissonClient.getLock(lockKey);try{// 等待锁超时时间 10 秒自动解锁时间 30 秒// watchDog 会自动续期lock.lock(30,TimeUnit.SECONDS);// 业务逻辑检查库存、创建订单、扣减库存checkStock(order);saveOrder(order);deductStock(order);}catch(Exceptione){log.error(Order creation failed,e);thrownewRuntimeException(创建订单失败,e);}finally{// 释放锁if(lock.isHeldByCurrentThread()){lock.unlock();}}}}这样写有几个好处自动续期Redisson 内置看门狗机制默认每 10 秒检查一次如果业务还在执行自动续期 30 秒原子操作获取锁和设置过期时间是原子执行的可重入同一个线程可以多次获取同一把锁四、常见坑点与最佳实践坑点1锁的 key 设计不合理错误做法所有业务都用同一个 keylock正确做法按业务维度设计 key例如// 订单业务StringlockKeylock:order:orderId;// 库存业务StringlockKeylock:stock:productId;// 分布式环境下用 UUID 线程 ID 确保唯一性StringrequestIdUUID.randomUUID().toString():Thread.currentThread().getId();坑点2释放锁时没有验证 owner错误做法// 直接删除不检查是谁的锁redisTemplate.delete(lockKey);正确做法// Redisson 自动处理了这个逻辑// 释放锁时会验证是否是当前线程持有的锁lock.unlock();如果用原生 Redis要用 Lua 脚本保证原子性Stringscriptif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end;redisTemplate.execute(newDefaultRedisScript(script,Long.class),Collections.singletonList(lockKey),requestId);坑点3没有处理获取锁失败的情况错误做法获取不到锁就直接报错正确做法根据业务场景选择等待策略// 策略1等待并重试booleanacquiredlock.tryLock(10,30,TimeUnit.SECONDS);if(acquired){try{// 业务逻辑}finally{lock.unlock();}}else{// 获取锁失败执行降级逻辑log.warn(Failed to acquire lock, executing fallback);executeFallback();}// 策略2立即返回不阻塞if(lock.tryLock()){try{// 业务逻辑}finally{lock.unlock();}}else{// 直接返回结果或提示用户returnResult.fail(系统繁忙请稍后重试);}坑点4集群环境下锁失效如果 Redis 是主从架构主节点宕机时可能导致锁丢失。建议使用 RedLock 算法或 Redis Cluster// Redisson RedLock 配置RLocklock1redissonClient1.getLock(lockKey);RLocklock2redissonClient2.getLock(lockKey);RLocklock3redissonClient3.getLock(lockKey);RedissonRedLockredLocknewRedissonRedLock(lock1,lock2,lock3);redLock.lock();try{// 业务逻辑}finally{redLock.unlock();}写在最后分布式锁是微服务架构中的必备技能用错了轻则数据重复重则资金损失。我在项目里血的教训总结下来就三点用成熟的框架别自己造轮子Redisson 已经处理了大部分边界情况注意锁的粒度key 设计要足够细避免影响并发性能做好失败降级获取不到锁时要有兜底方案别把所有请求都堵死希望这篇文章能帮你们少踩坑。如果觉得有用点个赞再走相关推荐Redisson 官方文档Redis 分布式锁详解