ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Redis 分布式锁实战,从踩坑到 Lua 脚本最终方案

Redis 分布式锁实战,从踩坑到 Lua 脚本最终方案 一、什么是分布式锁在单体项目中我们可以直接使用synchronized或者reentrantlock实现锁控制多线程并发访问共享资源。 但是分布式环境下服务部署在多个不同服务器jvm 之间相互隔离本地锁失效。 分布式锁就是用来在多个独立服务实例之间实现互斥访问共享资源的锁方案。主流分布式锁实现对比方案实现方式优点缺点Mysql唯一索引 / 悲观锁实现简单性能差容易死锁超时不好处理Redissetnx 过期时间高性能实现简单需要自己处理锁续期、锁误删问题Zookeeper临时有序节点可靠性高性能偏低重连会释放锁运维复杂项目中最常用的就是Redis 分布式锁下面一步步从零实现逐个解决遇到的坑。// 先setnx再expire两条独立命令不具备原子性 boolean lock redisTemplate.opsForValue().setIfAbsent(lock, 1); if(lock){ // 如果这里程序崩溃expire没有执行锁永远不会释放 redisTemplate.expire(lock,30,timeunit.seconds); //执行业务 }❗问题setnx和expire是两条命令中间一旦服务宕机锁永久存在死锁。正确原子命令Redis 单命令保证原子性// set key value nx ex 过期时间一条命令完成加锁设置过期时间 boolean ok redisTemplate.opsForValue() .setIfAbsent(lock, uuid, 30, timeunit.seconds);nxnot exist不存在才写入ex设置过期时间。这里 value 存入 uuid不是随便写死数字为了区分锁是谁加的防止线程误删别人的锁。三、第一个大坑锁被别的线程释放场景还原线程 a 拿到锁过期时间 30s业务执行很慢超过 30 秒redis 自动把锁释放线程 b 成功获取到同一个 lock 锁开始执行业务线程 a 业务终于跑完执行释放锁代码直接 del lock把线程 b 持有的锁删掉了解决方案释放锁前先判断锁的 value 是不是自己的 uuid只有自己的锁才允许删除。// 伪代码获取当前锁的值 string val redisTemplate.opsForValue().get(lock); if(uuid.equals(val)){ // 确认是自己的锁再删除 redisTemplate.delete(lock); }但是这里依然存在新问题get 和 delete 是两条命令非原子操作极端情况判断 value 相等之后准备执行 delete此时锁刚好过期redis 自动释放锁其他线程拿到锁当前线程执行 del删掉别人新锁。getdel 必须保证原子性这时候就需要 Lua 脚本。四、Lua 脚本解决原子性问题Redis 执行 Lua 脚本时整个脚本作为一个原子操作执行不会被其他命令打断完美解决多条命令原子性问题。Lua 脚本释放锁脚本-- 判断key对应value是否等于传入的uuid相等才删除返回1否则返回0 if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1]) else return 0 endjava 代码调用脚本string script if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1]) else return 0 end; redisscriptlong redisscript redisscript.of(script); // keys集合放lock keyargs放当前线程的uuid long result redistemplate.execute(redisscript, lists.newarraylist(lock), uuid);五、第二个大坑锁超时业务还没执行完锁续期问题场景锁过期时间 30s但是业务执行时间超过 30 秒redis 自动释放锁其他线程抢占锁。 方案看门狗watchdog自动续期原理获取锁成功后开启一个后台定时线程每隔一段时间一般是过期时间的 1/3比如 30s 过期每 10s 执行一次判断锁还存在就延长锁的过期时间业务执行完毕关闭定时任务释放锁手写看门狗比较繁琐生产环境一般直接使用 redisson 框架内置看门狗。六、Redisson 框架快速实现分布式锁redisson 对 redis 分布式锁做了封装自动处理原子加锁、lua 释放锁、看门狗自动续期、可重入锁。1. maven 依赖dependency groupidorg.redisson/groupid artifactidredisson-spring-boot-starter/artifactid version3.17.7/version /dependency2. redisson 配置类configuration public class redissonconfig { bean public redissonclient redissonclient(){ config config new config(); // 配置redis地址 config.usesingleserver().setaddress(redis://127.0.0.1:6379); return redisson.create(config); } }3. 业务代码使用autowired private redissonclient redissonclient; public void testlock(){ rlock lock redissonclient.getlock(product:lock); try{ // 尝试获取锁最多等待10秒锁默认30s过期自动看门狗续期 boolean isget lock.trylock(10,30, timeunit.seconds); if(isget){ // 执行业务逻辑 } }catch (exception e){ e.printstacktrace(); }finally { // 判断当前线程持有锁才释放 if(lock.isheldbycurrentthread()){ lock.unlock(); } } }edisson 的 rlock 是可重入锁同一个线程可以多次加锁底层也是 lua 脚本实现计数。七、第三个大坑redis 主从集群下锁失效主从同步问题场景客户端在 redis 主节点成功 set lock拿到锁主节点还没来得及把数据同步给从节点主节点宕机从节点升级成新主节点新主没有 lock 这条数据其他客户端又拿到锁结果两个客户端同时持有锁锁失效解决方案redlock 红锁方案多个独立 redis 节点过半节点加锁成功才算拿到锁业务层面做兜底数据库增加唯一约束防止超卖等严重业务问题红锁代价高生产中很少用大部分业务主从延迟带来的风险可以接受配合数据库兜底。八、总结基础分布式锁加锁和过期时间必须一条原子命令不能分两次执行释放锁不能直接 del必须校验锁归属getdel 要使用 lua 脚本保证原子性业务执行时间超过锁过期时间 → 需要看门狗自动续期推荐 redissonredis 主从切换会存在锁失效风险重要业务必须增加数据库兜底方案分布式锁核心目标保证同一时刻只有一个线程执行共享资源操作
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进