Redis分布式锁的实现(Redission)
- 其他
- 2025-09-20 23:18:01

写在前面
本人在学习Redis过程中学习到分布式锁时太多困惑和疑难杂点 需要总结梳理思路 以下思路都是最简单最基本的思路 主要用到Redission工具类 会涉及到看门狗机制等 本文内容部分引自Javaguide,小林coding等热门八股 用于个人学习用途
分布式锁介绍对于单机多线程来说 通常用ReetrantLock类 synchronized关键字这类JDK自带的本地锁来控制一个JVM进程内多线程对本地共享资源的访问
图解引自JavaGuide:
线程访问共享资源是 互斥 的!也就是同一时刻只有单线程可获取本地锁访问共享资源
在分布式系统下 不同服务或者客户端通常运行在多个独立的JVM进程上 若多个JVM进程共享同份资源 使用本地锁就没办法实现资源的互斥访问了 为了解决这种问题 分布式锁 诞生了
举例子:下单服务一共部署 3 份 都对外提供服务 用户下订单之前后台要查库存 为了防止超卖 就需要加锁来实现对检查库存操作的同步访问 由于订单服务位于不同JVM进程中 本地锁在这种情况下就无法正常工作 因此要用到分布式锁 才能解决多线程不在同个JVM进程下也能获取同把锁 劲儿实现共享资源的互斥访问
图解引自JavaGuide:
可以看出来两者区别就在于 分布式锁就是联系起不同的JVM进程并保证所有JVM都可以对共享资源进行同步操作 基本的要求还是大体一致的 同样 独立进程里的线程访问共享资源是互斥的 某一时刻只有单线程可获取到分布式锁访问共享资源
分布式锁最基本要满足:
1.互斥:任何一个时刻 锁只能被一个线程持有(保证原子操作以免造成数据不一致问题)
2.高可用:锁的服务是高可用的 即使客户端释放锁的代码逻辑出现问题 锁最后还是会被释放 不影响其他线程中进程对共享资源的访问(一定可以释放锁 无论是异常还是出错 锁的效率很高)
3.可重入:一个节点获取了锁以后还可以再次获取锁(多次使用客户端一个功能 增删改)
实现分布式锁:Redis 或 ZooKeeper实现分布式锁 以Redis为例
how to 实现?首先 从分布式锁的定义出发 无论是本地锁还是分布式锁 都有共性--“互斥”
在Redis里 setnx命令可以实现互斥 setnx(set if Not eXist)对应Java里的setIfAbsent方法 如果key不存在 才会设置key的值 若key存在就什么都不做
> SETNX lockKey uniqueValue (integer) 1 > SETNX lockKey uniqueValue (integer) 0释放锁 用del删除key就行
> DEL lockKey (integer) 1但是这里有很多可能 在自学过程中 想到过若在当前线程已经获取锁的情况下
如果使用多个命令(如 setnx 和 expire设置过期时间),可能会出现:
1.成功获取锁 但是设置过期时间失败 导致锁无法正常释放
2.释放锁时 误删其他线程的锁(如锁已过期,但当前线程误删了新线程的锁)
为了解决特殊情况 需要保证数据前后操作一致性 也就是原子性 最常见的就是用lua脚本通过key对应的value(唯一值)来判断
lua脚本基于c语言 保证了解锁操作的原子性 因为Redis在执行lua脚本时可以用原子性的方式执行 从而保证锁释放操作的原子性
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end图解引自JavaGuide:
这样做有两个好处 用value判断是否为对应线程的锁 是则释放 否则error 防止误删其他锁
同时 用lua脚本保持原子操作 确保锁释放不会被其他线程打断
技术往往都是双刃剑 同样这种锁也有一些问题 要是程序遇到问题 比如说释放锁的逻辑突然挂掉可能导致锁无法正常释放 进而造成共享资源无法被其他进程或者线程访问 所以 为了避免这种问题 需要给锁设置一个过期时间
分布式锁设置过期时间为了避免这种因外部原因或者突然断电异常 可以给这个key(这个分布式锁)设置一个过期时间
127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX OKlockKey:加锁的锁名 uniqueValue 能够唯一标识锁的随机字符串(可以用UUID)
NX:只有当lockKey对应的key值不存在时才能set成功
EX:过期时间设置(秒为单位) EX 3 表示这个锁有三秒的自动过期时间 与EX对应的是PX(毫秒)两个都是过期时间设置
一定需要保证设置指定key的值和过期时间是原子操作!!!!!!否则锁无法被释放
但是我怎么知道要设置多长过期时间 我写完这个文章我难道要给自己设置个过期时间 到九点前我写完了 那我网页还挂着为啥不关机 九点之前没写完 我电脑直接关机了我怎么写
对于这种分布式锁也是一样的道理 一般正常的操作都是毫秒级别 过期时间小于线程对共享资源的操作时间 就回出现锁提前过期的问题 进而导致分布式锁直接失效 过期时间设置过长又会浪费性能
要是能自动设置就好了!
如何实现优雅的续期....?
总会有人想到的 这里就引入Redis里现成的方案 Redission
Redission自动续期机制Redission就可以做到自动续期机制 其底层实现上 使用了看门狗机制(Watch Dog)
在共享资源的线程操作还未完成的情况下 看门狗会一直延长过期时间(默认情况下存活30s
每10s更新一次过期时间)进而保证锁不会因为超时而被释放
多好的看门狗 多希望自己学习也是这
图解引自JavaGuide:
看门狗名字的由来于 getLockWatchdogTimeout() 方法,此方法返回看门狗给锁续期的过期时间(默认30s)
//默认 30秒,支持修改 private long lockWatchdogTimeout = 30 * 1000; public Config setLockWatchdogTimeout(long lockWatchdogTimeout) { this.lockWatchdogTimeout = lockWatchdogTimeout; return this; } public long getLockWatchdogTimeout() { return lockWatchdogTimeout; }方法内用lua脚本保证操作原子性 renewExpiration方法包含主要逻辑
private void renewExpiration() { //...... Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { //...... // 异步续期,基于 Lua 脚本 CompletionStage<Boolean> future = renewExpirationAsync(threadId); future.whenComplete((res, e) -> { if (e != null) { // 无法续期 log.error("Can't update lock " + getRawName() + " expiration", e); EXPIRATION_RENEWAL_MAP.remove(getEntryName()); return; } if (res) { // 递归调用实现续期 renewExpiration(); } else { // 取消续期 cancelExpirationRenewal(null); } }); } // 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用 }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); ee.setTimeout(task); }JavaGuide的源码分析很到位 我之前做过看门狗机制的笔记 比较凌乱 基本上是这样的流程
有ttl和time(ttl 锁有效时间 time获取锁的等待时间) 需要确保业务正常运行释放锁 不能因为阻塞异常释放锁 引出看门狗机制
这里的ee对象封装了当前线程id与当前定时的任务(该定时任务里用lua脚本对redis有效期定时更新 在看门狗机制30s的情况下 每30/3 = 10秒后会重启任务 通过内部递归重复调用方法执行)
Watch Dog 通过调用 renewExpirationAsync() 方法实现锁的异步续期:
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, // 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认) "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return 1; " + "end; " + "return 0;", Collections.singletonList(getRawName()), internalLockLeaseTime, getLockName(threadId)); }这里的renewExpirationAsync方法实现了续期 用lua脚本主要是保证操作原子性
JavaGuide里用Redission分布式 可重入锁RLock来说明如何使用Redission实现分布式锁
// 1.获取指定的分布式锁对象 RLock lock = redisson.getLock("lock"); // 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制 lock.lock(); // 3.执行业务 ... // 4.释放锁 lock.unlock(); // 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制 lock.lock(10, TimeUnit.SECONDS);注意 不设置锁过期时间时 才会出发看门狗自动续期机制
如何实现可重入锁?什么是可重入锁?
单线程可以多次获取同一把锁 例如一个线程在执行一个带锁的方法 在方法中又调用了另一个同样需要相同锁的方法 则改成可以直接执行调用的方法 即可重入 相同线程多次进方法内的锁
Java中 synchronized(JVM内置监视器锁)与ReentrantLock(Lock实现类锁)都是可重入锁
不可重入的分布式锁基本可以满足绝大部分场景 特殊情况下还是要用可重入分布式锁
可重入分布式锁实现核心思路是 线程在获取锁时判断是否为自己的锁 如果是 就不用重新获取
所以 可以给每个锁关联一个可重入计数器 和 一个 占有他的线程(给线程上标识)当可重入计数器
大于0时 则锁被占有 需要判断占有该所的线程和请求获取锁的线程是否一致(是否有相同标识)
实际上 总有人替你负重前行 Redission内部内置了多种类型的锁比如可重入锁(ReentrantLock)自旋锁(SpinLock) 公平锁(Fair Lock)多重锁(MultiLock)红锁(RedLock)读写锁(ReadWriteLock)
要是都用一个Redis服务 Redis爆炸了怎么办?所以一般都是集群服务 所以..
集群模式下如何实现分布式锁?如何保证分布式锁可靠性?Redis集群数据同步到每个节点 会是异步还是同步? 一定是异步操作 同步太慢 那如果Redis主节点获取到锁后 在未能同步到其他节点时主服务宕机了.. 此时 新的Redis主节点还是能获取锁 变成新老大 多个应用服务还是可以同时获取锁
图解引自JavaGuide:
针对这种突然崩溃的情况 还是有人替我负重前行 用RedLock(红锁)来解决
RedLock算法思想 是 让客户端向Redis集群中多个独立的Redis实例依次请求申请加锁 若客户端能和多半数实例成功完成加锁操作 那么就宏观上认为:客户端成功获得分布式锁 否则加锁失败
即使部分Redis节点出现问题 只要保证Redis集群中有多半数Redis节点可用 分布式锁服务就ok
RedLock是直接操作Redis节点的 并不是通过Redis集群操作的 这样才可以避免Redis集群主从切换导致锁丢失问题
贴上锁优化时的问题
哨兵会让从节点变为主节点 但是原锁就会失效 可以在新主节点进新锁 会引发线程安全问题
以及从开始到现在我遇到的分布式锁的优缺点和优化过程 今天又看到RedLock
基本上到这里也是个人项目的内容
面试拷打:如何在项目中实现分布式锁?采用了什么方法?Redission实现分布式锁..后续后面补上
参考资料[1]Redisson: github /redisson/redisson
[2]redisson-3.17.6: github /redisson/redisson/releases/tag/redisson-3.17.6
[3]Redlock 算法: redis.io/topics/distlock
[4]How to do distributed locking - Martin Kleppmann - 2016: martin.kleppmann /2016/02/08/how-to-do-distributed-locking.html
[5]JavaGuide:原文链接:支付宝一面:如何基于Redis实现分布式锁?
Redis分布式锁的实现(Redission)由讯客互联其他栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“Redis分布式锁的实现(Redission)”