【Redis】分布式锁
Redis分布式锁通过setNX,单线程处理网络请求,不需要考虑并发安全性
所有服务节点设置相同的key,返回为0,则锁获取失败
setnx问题:
1.死锁:
- 持有锁的应用崩溃,无法释放锁,其他应用也不能再获取锁。
- 早期版本在设置锁时不能同时设置超时参数,如果设置锁后还没设置超时就出现宕机,就会死锁问题。通过将设置锁和超时时间改为原子操作解决。
2.锁自动释放:
- 一旦持有锁的应用出现问题,就不会去释放锁。从这个方向思考,可以给锁一个过期时间。这就有可能出现任务未完成但锁已释放。
3.删除其它应用的锁:
- A应用设置的锁到达超时时间被自动释放后,B应用就能设置锁,此时如果A应用完成,就会释放掉B应用的锁。解决这个问题可以在设置锁时,value设置一个uuid,删除锁时判断是否是自己加的锁。
4.锁丢失:
- 在主从集群中,从库未来得及同步时主库宕机。
Redis + Lua实现分布式锁
使用Redisson框架,下面简单说一下原理
3.1加锁的Lua伪代码
可以使用Lua编程语言,编写一段代码,然后在redis中执行,可以认为是原子操作,下面是伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
if (redis.call( 'exists' , keys[1]) == 0) then redis.call( 'hset' , keys[1], argv[2], 1); redis.call( 'pexpire' , keys[1], argv[1]); return nil; end if (redis.call( 'hexists' , keys[1], argv[2]) == 1) then redis.call( 'hincrby' , keys[1], argv[2], 1); redis.call( 'pexpire' , keys[1], argv[1]); return nil; end return redis.call( 'pttl' , keys[1]); |
上面涉及一个redis.call()方法,他的功能根据传入的参数执行redis命令,
以及出现了几个参数,解释如下:
keys[1]:表示的就是锁的名称,比如要对foo这个字符串加锁,那么keys[1]就是foo;
argv就是argument value的简写,表示的是传入的参数列表,是一个数组;
argv[1]:表示锁的生存时间,默认在30秒后释放锁(失效);
argv[2]:表示加锁的客户端ID(可以理解为事务ID)
3.2加锁的代码介绍
分别介绍上面三段代码:
1
2
3
4
5
|
if (redis.call( 'exists' , keys[1]) == 0) then redis.call( 'hset' , keys[1], argv[2], 1); redis.call( 'pexpire' , keys[1], argv[1]); return nil; end |
这段代码,判断key是否存在,如果不存在,则创建一个hash,key为锁的名称,值为客户端id,后面的1表示加锁的次数,后面在判断可重入的时候使用;然后设置key的有效时间;之后就返回锁获取成功。
1
2
3
4
5
|
if (redis.call( 'hexists' , keys[1], argv[2]) == 1) then redis.call( 'hincrby' , keys[1], argv[2], 1); redis.call( 'pexpire' , keys[1], argv[1]); return nil; end |
这段代码主要实现“锁的可重入”,前提是key(锁)已经存在了,那么就判断hash中key对应的客户端是否为当前客户端id,如果是的话,那么就执行hincrby命令,将加锁的次数进行加1,然后重新设置key(锁)的过期时间,之后再返回。
1
|
return redis.call( 'pttl' , keys[1]); |
执行到上面这段代码,表示锁已经存在,且不是当前客户端获取到锁,那么就会执行pttl查看锁的有效期。
3.3watch dog自动延时
如果客户端获取到锁后,超过设置的过期时间,还希望持有锁,那么有watch dog机制,也就是该客户端获取到锁后,就会启动后台线程去判断客户端是否仍旧持有锁,如果是,则会延长过期时间。
3.4释放锁
释放锁,需要注意有锁的重入问题,当锁重入后,有计数器来保存重入的次数(获取锁的次数),每次unlock的时候,将计数减1,当计数为0的时候,表示不再持有该锁,则执行del key[1]命令删除锁。
Redisson分布式锁原理:(Redisson如何实现可重入、可重试、超时续约)
可重入:利用hash结构记录线程id和重入次数
可重试:利用信号量和发布订阅功能实现等待、唤醒,获取锁失败的重试机制
超时续约:利用watchDog,没隔一段时间(releaseTime/3),重置超时时间
Redlock实现
在Redis的分布式环境中,我们假设有N个Redis master。
这些节点完全互相独立,不存在主从复制或者其他集群协调机制。
我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。
现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。
为了取到锁,客户端应该执行以下操作:
获取当前Unix时间,以毫秒为单位。
依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。
当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。
例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。
这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。
如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。
客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。
当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)