[WenJi项目实战]拒绝死锁与误删从手写 Redis 锁到 Redisson 看门狗的演进之路前言在文迹项目的用户发布博客的场景中为了防止同一用户频繁刷接口赚取积分我们通常会使用 Redis 分布式锁进行限流。本文将结合实际业务拆解手写 Redis 锁的核心原理分析其在极端场景下的隐患并探讨如何通过 Lua 脚本和 Redisson 进行架构升级。 当前分布式锁的实现原理核心代码// ① 加锁SETNX TTL 原子操作StringlockKeylock:blog:userId;// 按用户粒度加锁StringlockValueUUID.randomUUID().toString();// 唯一标识BooleanacquiredstringRedisTemplate.opsForValue().setIfAbsent(lockKey,lockValue,10,TimeUnit.SECONDS);if(Boolean.FALSE.equals(acquired)){thrownewBusinessException(429,操作太频繁);}// ② 释放锁GET 比较 DELETE防误删StringcurrentValuestringRedisTemplate.opsForValue().get(lockKey);if(lockValue.equals(currentValue)){stringRedisTemplate.delete(lockKey);}原理拆解三个关键设计点设计点实现方式解决什么问题原子加锁setIfAbsent(key, value, ttl, unit)对应 RedisSET key value NX EX 10NX 保证互斥EX 防止死锁唯一标识UUID 作为 value防止误删其他线程持有的锁安全解锁GET → 比较 UUID → DELETE解决锁过期后误删新锁的经典问题 为什么需要 UUID假设线程 A 获取锁后业务执行超时导致锁自动过期。此时线程 B 获取了锁如果线程 A 随后直接执行delete就会把线程 B 的锁删掉。通过 UUID 校验可以确保线程 A 只能删除属于自己的锁。锁粒度设计目前按用户粒度加锁lock:blog:{userId}用户 Aid1:lock:blog:1← 只锁用户 A 自己用户 Bid2:lock:blog:2← 不影响用户 B同一用户并发 100 次请求仅 1 次成功其余返回 429不同用户之间完全隔离。现存隐患分析虽然手写锁能解决大部分并发问题但在架构层面仍存在以下缺陷锁过期时间写死业务执行时间如果超过 10s锁会自动释放后续请求进入导致并发安全问题。缺乏续期机制没有类似 Redisson 的“看门狗”机制锁一旦过期只能自动释放。解锁非原子性释放锁分为GET和DELETE两步。在极端时序下GET 之后、DELETE 之前锁恰好过期依然有极小概率发生误删。 架构升级方案方案 1引入 Lua 脚本保证解锁绝对原子性当前最快捷的修改方式项目也修改成这个方法将GET和DELETE合并为 Redis 端的原子操作彻底消除误删风险。1. 编写lua/unlock.luaifredis.call(GET,KEYS[1])ARGV[1]thenreturnredis.call(DEL,KEYS[1])elsereturn0end2. 配置 Spring Bean启动时加载一次ConfigurationpublicclassRedisLuaConfig{BeanpublicDefaultRedisScriptLongunlockScript(){DefaultRedisScriptLongscriptnewDefaultRedisScript();script.setLocation(newClassPathResource(lua/unlock.lua));script.setResultType(Long.class);returnscript;}}3. 业务层调用LongresultstringRedisTemplate.execute(unlockScript,Collections.singletonList(lockKey),lockValue);方案 2引入 Redisson 看门狗终极方案如果希望彻底解决锁过期和续期问题引入 Redisson 是最佳选择。1. 添加依赖dependencygroupIdorg.redisson/groupIdartifactIdredisson-spring-boot-starter/artifactIdversion3.27.0/version/dependency2. 业务代码重构AutowiredprivateRedissonClientredissonClient;publicvoidpublishBlog(TravelBlogblog){RLocklockredissonClient.getLock(lock:blog:blog.getUserId());// tryLock(等待时间, 锁持有时间, 时间单位)// 锁持有时间传 -1 则启用看门狗自动续期if(!lock.tryLock(0,-1,TimeUnit.SECONDS)){thrownewBusinessException(429,操作太频繁);}try{// 业务逻辑... 锁会自动续期默认每 10 秒续一次}finally{// 必须在 finally 中释放且 Redisson 内部已实现原子化释放lock.unlock();}} 总结轻量级场景如果业务执行极快毫秒级手写SETNXLua 脚本解锁已经足够无需引入额外依赖。复杂/耗时场景如果业务逻辑复杂、执行时间不可控强烈建议直接上Redisson利用看门狗机制保障分布式锁的绝对安全。
[WenJi项目实战]拒绝死锁与误删:从手写 Redis 锁到 Redisson 看门狗的演进之路
[WenJi项目实战]拒绝死锁与误删从手写 Redis 锁到 Redisson 看门狗的演进之路前言在文迹项目的用户发布博客的场景中为了防止同一用户频繁刷接口赚取积分我们通常会使用 Redis 分布式锁进行限流。本文将结合实际业务拆解手写 Redis 锁的核心原理分析其在极端场景下的隐患并探讨如何通过 Lua 脚本和 Redisson 进行架构升级。 当前分布式锁的实现原理核心代码// ① 加锁SETNX TTL 原子操作StringlockKeylock:blog:userId;// 按用户粒度加锁StringlockValueUUID.randomUUID().toString();// 唯一标识BooleanacquiredstringRedisTemplate.opsForValue().setIfAbsent(lockKey,lockValue,10,TimeUnit.SECONDS);if(Boolean.FALSE.equals(acquired)){thrownewBusinessException(429,操作太频繁);}// ② 释放锁GET 比较 DELETE防误删StringcurrentValuestringRedisTemplate.opsForValue().get(lockKey);if(lockValue.equals(currentValue)){stringRedisTemplate.delete(lockKey);}原理拆解三个关键设计点设计点实现方式解决什么问题原子加锁setIfAbsent(key, value, ttl, unit)对应 RedisSET key value NX EX 10NX 保证互斥EX 防止死锁唯一标识UUID 作为 value防止误删其他线程持有的锁安全解锁GET → 比较 UUID → DELETE解决锁过期后误删新锁的经典问题 为什么需要 UUID假设线程 A 获取锁后业务执行超时导致锁自动过期。此时线程 B 获取了锁如果线程 A 随后直接执行delete就会把线程 B 的锁删掉。通过 UUID 校验可以确保线程 A 只能删除属于自己的锁。锁粒度设计目前按用户粒度加锁lock:blog:{userId}用户 Aid1:lock:blog:1← 只锁用户 A 自己用户 Bid2:lock:blog:2← 不影响用户 B同一用户并发 100 次请求仅 1 次成功其余返回 429不同用户之间完全隔离。现存隐患分析虽然手写锁能解决大部分并发问题但在架构层面仍存在以下缺陷锁过期时间写死业务执行时间如果超过 10s锁会自动释放后续请求进入导致并发安全问题。缺乏续期机制没有类似 Redisson 的“看门狗”机制锁一旦过期只能自动释放。解锁非原子性释放锁分为GET和DELETE两步。在极端时序下GET 之后、DELETE 之前锁恰好过期依然有极小概率发生误删。 架构升级方案方案 1引入 Lua 脚本保证解锁绝对原子性当前最快捷的修改方式项目也修改成这个方法将GET和DELETE合并为 Redis 端的原子操作彻底消除误删风险。1. 编写lua/unlock.luaifredis.call(GET,KEYS[1])ARGV[1]thenreturnredis.call(DEL,KEYS[1])elsereturn0end2. 配置 Spring Bean启动时加载一次ConfigurationpublicclassRedisLuaConfig{BeanpublicDefaultRedisScriptLongunlockScript(){DefaultRedisScriptLongscriptnewDefaultRedisScript();script.setLocation(newClassPathResource(lua/unlock.lua));script.setResultType(Long.class);returnscript;}}3. 业务层调用LongresultstringRedisTemplate.execute(unlockScript,Collections.singletonList(lockKey),lockValue);方案 2引入 Redisson 看门狗终极方案如果希望彻底解决锁过期和续期问题引入 Redisson 是最佳选择。1. 添加依赖dependencygroupIdorg.redisson/groupIdartifactIdredisson-spring-boot-starter/artifactIdversion3.27.0/version/dependency2. 业务代码重构AutowiredprivateRedissonClientredissonClient;publicvoidpublishBlog(TravelBlogblog){RLocklockredissonClient.getLock(lock:blog:blog.getUserId());// tryLock(等待时间, 锁持有时间, 时间单位)// 锁持有时间传 -1 则启用看门狗自动续期if(!lock.tryLock(0,-1,TimeUnit.SECONDS)){thrownewBusinessException(429,操作太频繁);}try{// 业务逻辑... 锁会自动续期默认每 10 秒续一次}finally{// 必须在 finally 中释放且 Redisson 内部已实现原子化释放lock.unlock();}} 总结轻量级场景如果业务执行极快毫秒级手写SETNXLua 脚本解锁已经足够无需引入额外依赖。复杂/耗时场景如果业务逻辑复杂、执行时间不可控强烈建议直接上Redisson利用看门狗机制保障分布式锁的绝对安全。