【Redis八股|第8篇】Redis 分布式锁原理与 Redisson 使用 📅 2026/7/13 15:18:11 前言前面我们学习了 Redis 的数据类型、缓存问题、过期策略、内存淘汰和持久化。这一篇进入 Redis 在项目实战中非常常见的场景分布式锁。在单体项目中如果多个线程同时修改同一份资源可以使用 Java 本地锁比如synchronizedReentrantLock但是在分布式项目中服务可能部署多台机器。比如订单服务实例 A 订单服务实例 B 订单服务实例 C如果多个实例同时处理同一个订单本地锁就不够用了。这时候就需要分布式锁。Redis 是实现分布式锁的常见方案之一。这一篇我们就从为什么需要分布式锁、Redis 如何实现分布式锁、手写锁有哪些问题以及 Redisson 怎么用这几个角度来完整梳理。一、为什么需要分布式锁先看一个库存扣减场景。假设商品库存只有 1 件。用户 A 和用户 B 同时下单。如果没有控制可能发生用户 A 查询库存1 用户 B 查询库存1 用户 A 扣减库存库存变成 0 用户 B 扣减库存库存变成 -1这就出现了超卖。在单体项目中我们可以使用synchronized比如publicsynchronizedvoidcreateOrder(LongproductId){// 查询库存// 扣减库存// 创建订单}但是如果项目部署了多个实例实例 A 有自己的 JVM 实例 B 有自己的 JVMsynchronized只能锁住当前 JVM 内部的线程锁不住其他机器上的线程。所以分布式环境需要一个所有实例都能访问到的锁。Redis 就可以作为这个共享锁中心。二、什么是分布式锁分布式锁可以理解为在分布式系统中让多个服务实例争抢同一把锁只有拿到锁的实例才能执行关键业务。比如多个订单服务实例都访问 Redis实例 A 尝试加锁 lock:product:1001 成功 实例 B 尝试加锁 lock:product:1001 失败 实例 C 尝试加锁 lock:product:1001 失败只有实例 A 可以执行扣减库存。执行完后实例 A 删除锁。然后其他实例才有机会获取锁。三、Redis 实现分布式锁的核心命令Redis 实现分布式锁的核心命令是setlock:product:1001 value nx ex30这里参数含义lock:product:1001锁 keyvalue锁 valuenxkey 不存在时才设置成功ex 3030 秒后自动过期这条命令的意思是只有锁不存在时才能加锁成功并且锁会在 30 秒后自动释放。为什么要用nx因为锁只能被一个线程或实例持有。为什么要设置过期时间防止服务宕机后锁永远不释放。四、手写 Redis 分布式锁示例简单代码publicvoidcreateOrder(LongproductId){StringlockKeylock:product:productId;BooleansuccessstringRedisTemplate.opsForValue().setIfAbsent(lockKey,1,30,TimeUnit.SECONDS);if(!Boolean.TRUE.equals(success)){thrownewRuntimeException(系统繁忙请稍后再试);}try{// 查询库存// 判断库存是否充足// 扣减库存// 创建订单}finally{stringRedisTemplate.delete(lockKey);}}逻辑是先尝试加锁加锁失败说明有其他线程正在处理加锁成功执行核心业务最后释放锁这个版本可以表达基本思路但还不够安全。五、手写锁的问题一误删别人的锁假设锁过期时间是 30 秒。线程 A 获取锁后业务执行了 40 秒。0 秒线程 A 加锁成功锁 30 秒过期 30 秒锁自动过期 31 秒线程 B 加锁成功 40 秒线程 A 执行完 finally删除锁这时线程 A 删除的其实是线程 B 的锁。这就是误删锁问题。六、解决误删锁value 使用唯一标识加锁时value 不要写死成1而应该使用唯一标识。比如StringvalueUUID.randomUUID().toString();加锁BooleansuccessstringRedisTemplate.opsForValue().setIfAbsent(lockKey,value,30,TimeUnit.SECONDS);释放锁时先判断 value 是否是自己的StringredisValuestringRedisTemplate.opsForValue().get(lockKey);if(value.equals(redisValue)){stringRedisTemplate.delete(lockKey);}这样只有锁 value 和自己保存的一致时才删除。七、手写锁的问题二判断和删除不是原子操作上面的释放锁代码还有问题StringredisValuestringRedisTemplate.opsForValue().get(lockKey);if(value.equals(redisValue)){stringRedisTemplate.delete(lockKey);}这里分成两步查询锁 value删除锁这两个操作不是原子的。可能出现线程 A 查询 value发现是自己的 锁刚好过期 线程 B 获取锁 线程 A 执行 delete删除了线程 B 的锁所以释放锁需要保证判断 value 删除 key是一个原子操作。通常可以用 Lua 脚本实现。八、Lua 脚本释放锁Lua 脚本可以让 Redis 原子执行多条命令。释放锁脚本ifredis.call(get,KEYS[1])ARGV[1]thenreturnredis.call(del,KEYS[1])elsereturn0end含义如果锁 key 的 value 等于当前线程的 value 就删除锁 否则不删除Java 中执行 Lua 脚本可以保证判断和删除在 Redis 中原子完成。这样就避免了误删其他线程锁的问题。九、手写锁的问题三锁过期时间不好设置锁必须设置过期时间。但过期时间设置多少合适如果设置太短业务还没执行完锁先过期了会导致并发问题。如果设置太长服务宕机后其他线程要等很久才能重新获取锁体验不好。这就是手写 Redis 锁比较麻烦的地方。需要有一种机制如果业务还没执行完自动给锁续期。这就是 Redisson 的看门狗机制。十、Redisson 是什么Redisson 是一个 Redis 客户端框架它基于 Redis 实现了很多分布式工具。比如分布式锁分布式集合分布式计数器分布式信号量延迟队列我们最常用的就是 Redisson 分布式锁。它帮我们处理了很多手写锁容易出错的问题比如加锁解锁锁续期可重入误删锁等待锁所以实际项目中比起自己手写 Redis 分布式锁更推荐使用 Redisson。十一、Spring Boot 引入 Redisson添加依赖dependencygroupIdorg.redisson/groupIdartifactIdredisson-spring-boot-starter/artifactIdversion3.27.2/version/dependency配置 Redis 地址spring:redis:host:localhostport:6379不同项目版本配置可能略有差异具体以项目 Spring Boot 和 Redisson 版本为准。十二、Redisson 分布式锁使用示例AutowiredprivateRedissonClientredissonClient;publicvoidcreateOrder(LongproductId){RLocklockredissonClient.getLock(lock:product:productId);booleansuccessfalse;try{successlock.tryLock(3,30,TimeUnit.SECONDS);if(!success){thrownewRuntimeException(系统繁忙请稍后再试);}// 查询库存// 判断库存是否充足// 扣减库存// 创建订单}catch(InterruptedExceptione){Thread.currentThread().interrupt();thrownewRuntimeException(获取锁失败);}finally{if(successlock.isHeldByCurrentThread()){lock.unlock();}}}这里tryLock(3,30,TimeUnit.SECONDS)表示最多等待 3 秒获取锁 获取成功后锁 30 秒自动过期释放锁lock.unlock();isHeldByCurrentThread()用来判断当前线程是否持有这把锁避免误解锁。十三、Redisson 看门狗机制Redisson 有一个很重要的机制看门狗。如果加锁时没有手动指定过期时间Redisson 默认会给锁设置一个过期时间比如 30 秒。如果业务还没执行完Redisson 会自动给锁续期。比如lock.lock();Redisson 会自动续期直到业务执行完并调用lock.unlock();这就避免了业务执行时间太长导致锁自动过期的问题。不过如果你使用lock.tryLock(3,30,TimeUnit.SECONDS)手动指定了锁过期时间通常就不会触发自动续期机制。所以实际使用时要理解这点。十四、Redisson 可重入锁Redisson 的RLock是可重入锁。可重入的意思是同一个线程已经持有锁后可以再次获取同一把锁。比如lock.lock();try{methodA();}finally{lock.unlock();}如果methodA()内部又尝试获取同一把锁同一个线程可以再次获取成功。这和 Java 中的ReentrantLock思想类似。十五、分布式锁使用注意事项1. 锁粒度不要太大锁 key 应该尽量精确。比如扣减商品库存lock:product:1001不要直接使用lock:product如果所有商品共用一把锁会导致并发能力很差。2. 锁内逻辑要尽量短加锁后执行的业务越长锁持有时间越长其他请求等待越久。所以锁内只放必要的核心逻辑。3. 一定要释放锁使用try-finally保证释放锁。try{// 业务逻辑}finally{lock.unlock();}4. 注意锁和事务的关系如果业务中有数据库事务要注意锁释放时机。有时候锁释放了但数据库事务还没提交其他线程读到旧数据可能出现问题。这种情况要结合事务提交时机设计。十六、Redis 分布式锁能保证绝对安全吗严格来说Redis 分布式锁并不是所有场景下都绝对安全。比如 Redis 主从切换时可能出现锁还没同步到从节点主节点宕机从节点升级为主节点导致其他客户端又获取到锁。所以在极高一致性要求场景中需要更严谨的方案。但在很多普通业务场景下Redisson 分布式锁已经能满足需求。如果是金融级强一致业务可能要结合数据库锁、事务、唯一索引、消息队列等方案综合处理。十七、面试标准回答如果面试官问Redis 分布式锁怎么实现有什么问题Redisson 做了什么可以这样回答Redis 分布式锁的核心是使用set key value nx ex seconds命令。nx保证只有 key 不存在时才能加锁成功ex设置过期时间防止服务宕机后锁永远不释放。释放锁时不能直接删除 key因为可能误删别人的锁。一般会给锁设置唯一 value释放时判断 value 是否一致一致才删除。判断和删除必须保证原子性通常用 Lua 脚本实现。手写 Redis 锁还会遇到锁过期时间不好设置的问题。如果业务执行时间超过锁过期时间锁可能提前释放。Redisson 提供了更完善的分布式锁实现支持可重入、自动续期、Lua 脚本解锁等能力。它的看门狗机制可以在业务未执行完时自动给锁续期。实际项目中不推荐自己手写复杂分布式锁通常使用 Redisson。十八、常见问题总结1. Redis 分布式锁为什么要设置过期时间防止服务宕机后锁无法释放导致死锁。2. 为什么 value 要唯一为了防止释放锁时误删别人的锁。3. 为什么解锁要用 Lua 脚本因为判断 value 和删除 key 必须是原子操作。4. Redisson 看门狗是什么看门狗会在业务未执行完时自动给锁续期避免锁提前过期。5. Redisson 锁是可重入的吗是的RLock支持可重入。6. 分布式锁可以完全替代数据库事务吗不能。分布式锁解决并发互斥问题事务解决数据一致性提交和回滚问题它们关注点不同。十九、实际开发建议使用 Redis 分布式锁时可以注意下面几点优先使用 Redisson不要轻易手写复杂锁锁 key 要有业务含义锁粒度要尽量小锁内业务逻辑要尽量短一定用try-finally释放锁解锁前确认当前线程持有锁注意锁过期时间和业务执行时间强一致场景不要只依赖 Redis 锁数据库唯一索引、事务、乐观锁也可以配合使用高并发核心链路要压测验证二十、总结这一篇主要学习了 Redis 分布式锁原理和 Redisson 使用。分布式锁是为了解决多服务实例同时操作共享资源的问题。Redis 实现分布式锁的核心命令是set key value nx ex seconds。手写 Redis 锁需要注意误删锁、原子释放锁、锁过期时间、服务宕机等问题。释放锁时需要唯一 value并使用 Lua 脚本保证判断和删除的原子性。实际项目中更推荐使用 Redisson。Redisson 提供了可重入锁、自动续期、线程安全释放锁等能力比手写 Redis 锁更可靠。下一篇我们继续学习 Redis 主从复制、哨兵和集群。