redis实现分布式锁
在计算机系统中,锁作为一种控制并发的机制无处不在。
单机环境下,操作系统能够在进程或线程之间通过本地的锁来控制并发程序的行为。而在如今的大型复杂系统中,通常采用的是分布式架构提供服务。
分布式环境下,基于本地单机的锁无法控制分布式系统中分开部署客户端的并发行为,此时分布式锁就应运而生了。
一个可靠的分布式锁应该具备以下特性:
1.互斥性:作为锁,需要保证任何时刻只能有一个客户端(用户)持有锁
2.可重入: 同一个客户端在获得锁后,可以再次进行加锁
3.高可用:获取锁和释放锁的效率较高,不会出现单点故障
4.自动重试机制:当客户端加锁失败时,能够提供一种机制让客户端自动重试
一、白话分布式
什么是分布
式,用最简单的话来说,就是为了较低单个服务器的压力,将功能分布在不同的机器上面;
就比如:
本来一个程序员可以完成一个项目:需求->设计->编码->测试
但是项目多的时候,一个人也扛不住,这就需要不同的人进行分工合作了
这就是一个简单的分布式协同工作了;
二、分布式锁
首先看一个问题,如果说某个环节被终止或者别侵占,就会发生不可知的事情
这就会出现,设计好的或者设计的半成品会被破坏,导致后面环节出错;
这时候,我们就需要引入分布式锁的概念;
何为分布式锁?
当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
用一个状态值表示锁,对锁的占用和释放通过状态值来标识。
分布式锁的条件:
可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
这把锁要是一把可重入锁(避免死锁)
这把锁最好是一把阻塞锁
这把锁最好是一把公平锁
有高可用的获取锁和释放锁功能
获取锁和释放锁的性能要好
分布式锁的实现:
分布式锁的实现由很多种,文件锁、数据库、redis等等,比较多,在实践中,还是redis做分布式锁性能会高一些;
三、redis实现分布式锁
首先看两个命令:
**setnx:**将 key 的值设为 value,当且仅当 key 不存在。 若给定的 key 已经存在,则 SETNX 不做任何动作。 SETNX 是SET if Not eXists的简写。
127.0.0.1:6379> set lock “unlock”
OK
127.0.0.1:6379> setnx lock “unlock”
(integer) 0
127.0.0.1:6379> setnx lock “lock”
(integer) 0
127.0.0.1:6379>
expire: EXPIRE key seconds
为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除
127.0.0.1:6379> expire lock 10
(integer) 1
127.0.0.1:6379> ttl lock
8
127.0.0.1:6379> get lock
(nil)
基于分布式锁的流程:
这就是一个简单的分布式锁的实现流程,具体代码实现也很简单,就不赘述了;
四、redis实现分布式锁问题
如果出现了这么一个问题:如果setnx是成功的,但是expire设置失败,那么后面如果出现了释放锁失败的问题,那么这个锁永远也不会被得到,业务将被锁死?
解决的办法:使用set的命令,同时设置锁和过期时间
set参数:
set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在时设置value,成功返回OK,失败返回(nil)
XX:key存在时设置value,成功返回OK,失败返回(nil)
实践:
127.0.0.1:6379> set unlock “234” EX 100 NX
(nil)
127.0.0.1:6379>
127.0.0.1:6379> set test “111” EX 100 NX
OK
这样就完美的解决了分布式锁的原子性;
语法
SETNX key value
将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。SETNX是”SETifNot eXists”的简写。
返回值
设置成功,返回 1 。
设置失败,返回 0 。
加锁步骤
SETNX lock.foo <current Unix time + lock timeout + 1>
如果客户端获得锁,SETNX返回1,加锁成功。
如果SETNX返回0,那么该键已经被其他的客户端锁定。
接上一步,SETNX返回0加锁失败,此时,调用GET lock.foo获取时间戳检查该锁是否已经过期
如果没有过期,则休眠一会重试。
如果已经过期,则可以获取该锁。具体的:调用GETSET lock.foo <current Unix timestamp + lock timeout + 1>基于当前时间设置新的过期时间。
注意: 这里设置的时候因为在SETNX与GETSET之间有个窗口期,在这期间锁可能已被其他客户端抢去,所以这里需要判断GETSET的返回值,他的返回值是SET之前旧的时间戳:
若旧的时间戳已过期,则表示加锁成功。
若旧的时间戳还未过期(说明被其他客户端抢去并设置了时间戳),代表加锁失败,需要等待重试。
解锁步骤
解锁相对简单,只需GET lock.foo时间戳,判断是否过期,过期就调用删除DEL lock.foo
参考:
- http://www.coder55.com/article/82006
- https://www.kancloud.cn/idzqj/customer/1996825
- https://www.cnblogs.com/xiaoxiongcanguan/p/10718202.html