分布式锁
千万级流量以上的项目,基本上都会用redis。
RedLock,redis创始人 比较提出的方案。
我们真的需要锁么?
需要锁的条件:
- 多任务环境下。(进程,线程)
- 任务都对同一共享资源进行写操作。
- 对资源的访问是互斥的。
操作周期:
- 竞争锁。获取锁后才能对资源进行操作。
- 占有锁。操作中。
- 其他竞争者,任务阻塞。
- 占有锁者,释放锁。继续从1开始。
JVM 锁 解决不了分布式环境中的加锁问题。
分布式锁应用场景:服务集群,比如N个订单服务,接受到大量司机的发送的对一个订单的抢单请求。如果是单个服务,可以用jvm锁控制,但是服务集群,jvm 就不行了。因为不在一个jvm中。
分布式锁解决方案
api-driver, eureka 7900 service-order 8004,8005
无锁情况
1 | @Qualifier("grabNoLockService") |
JVM 锁
1 | @Qualifier("grabJvmLockService") |
但是:启动两个service-order8004,8005,则有下面情况
1 | 8005: |
问题:无法解决分布式,集群环境的问题。所以要用分布锁
基于mysql
测试时要恢复数据。tbl_order 中status 为0,tbl_order_lock清空
@Qualifier(“grabMysqlLockService”) 实际用 事件实现。
1 | 8005: |
问题:
1、如果中间出异常了,如何释放锁,用存储过程,还是可以解决。
2、mysql 并发是由限制的。不适合高并发场景。
压测结果:https://help.aliyun.com/document_detail/150351.html?spm=a2c4g.11186623.6.1463.1e732d02nCMBBa
基于Redis
1 | stringRedisTemplate 用法 |
redis:内存存储的数据结构服务器,内存数据库。可用于:数据库,高速缓存,消息队列。采用单线程模型,并发能力强大。10万并发没问题。
分布锁知识:
redis的单进程单线程。
缓存有效期。有效期到,删除数据。
setnx。当key存在,不做任何操作,key不存在,才设置。
《Redis 分布锁》
单节点
加锁
SET orderId driverId NX PX 30000
上面的命令如果执行成功,则客户端成功获取到了锁,接下来就可以访问共享资源了;而如果上面的命令执行失败,则说明获取锁失败。
释放锁
关键,判断是不是自己加的锁。
关注点:
orderId,是我们的key,要锁的目标。
driverId是由我们的司机ID,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。即一个订单被一个司机抢。
NX表示只有当orderId不存在的时候才能SET成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。
PX 30000表示这个锁有一个30秒的自动过期时间。当然,这里30秒只是一个例子,客户端可以选择合适的过期时间。
这个锁必须要设置一个过期时间。否则的话,当一个客户端获取锁成功之后,假如它崩溃了,或者由于发生了网络分区,导致它再也无法和Redis节点通信了,那么它就会一直持有这个锁,而其它客户端永远无法获得锁了。antirez在后面的分析中也特别强调了这一点,而且把这个过期时间称为锁的有效时间(lock validity time)。获得锁的客户端必须在这个时间之内完成对共享资源的访问。
此操作不能分割。
1
2
3SETNX orderId driverId
EXPIRE orderId 30
虽然这两个命令和前面算法描述中的一个SET命令执行效果相同,但却不是原子的。如果客户端在执行完SETNX后崩溃了,那么就没有机会执行EXPIRE了,导致它一直持有这个锁。造成死锁。必须给key设置一个value。value保证每个线程不一样。如果value在每个线程间一样。会发生 误解锁的问题。
1
2
3
4
5
61.客户端1获取锁成功。
2.客户端1在某个操作上阻塞了很长时间。
3.过期时间到了,锁自动释放了。
4.客户端2获取到了对应同一个资源的锁。
5.客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。
之后,客户端2在访问共享资源的时候,就没有锁为它提供保护了。释放锁的操作,得释放自己加的锁。
1 | 1.客户端1获取锁成功。 |
redis故障问题。
如果redis故障了,所有客户端无法获取锁,服务变得不可用。为了提高可用性。我们给redis 配置主从。当master不可用时,系统切换到slave,由于Redis的主从复制(replication)是异步的,这可能导致丧失锁的安全性。
1
2
3
41.客户端1从Master获取了锁。
2.Master宕机了,存储锁的key还没有来得及同步到Slave上。
3.Slave升级为Master。
4.客户端2从新的Master获取到了对应同一个资源的锁。客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。
这个算法中出现的锁的有效时间(lock validity time),设置成多少合适呢?如果设置太短的话,锁就有可能在客户端完成对于共享资源的访问之前过期,从而失去保护;如果设置太长的话,一旦某个持有锁的客户端释放锁失败,那么就会导致所有其它客户端都无法获取锁,从而长时间内无法正常工作。应该设置稍微短一些,如果线程持有锁,开启线程自动延长有效期。
还有一点,如果在过期时间内,程序没有执行完,是不能让key过期的,所以要延时。
1 | 断点打在:rlock.lock(); |
为了解决9.10问题。antirez设计了Redlock算法
Redis的作者antirez给出了一个更好的实现,称为Redlock,算是Redis官方对于实现分布式锁的指导规范。Redlock的算法描述就放在Redis的官网上:
https://redis.io/topics/distlock
RedLock(多master)
debug
1 | 断点达到:rLock.lock() |
目的:对共享资源做互斥访问。
因此antirez提出了新的分布式锁的算法Redlock,它基于N个完全独立的Redis节点(通常情况下N可以设置成5)。
运行Redlock算法的客户端依次执行下面各个步骤,来完成 获取锁 的操作:
- 获取当前时间(毫秒数)。
- 按顺序依次向N个Redis节点执行 获取锁 的操作。这个获取操作跟前面基于单Redis节点的 获取锁 的过程相同,包含value driverId ,也包含过期时间(比如
PX 30000
,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个 获取锁 的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。 - 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
- 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
- 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起 释放锁 的操作(即前面介绍的Redis Lua脚本)。
当然,上面描述的只是 获取锁 的过程,而 释放锁 的过程比较简单:客户端向所有Redis节点发起 释放锁 的操作,不管这些节点当时在获取锁的时候成功与否。
问题:
由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作,因此理论上它的可用性更高。我们前面讨论的单Redis节点的分布式锁在failover的时候锁失效的问题,在Redlock中不存在了,但如果有节点发生崩溃重启,还是会对锁的安全性有影响的。具体的影响程度跟Redis对数据的持久化程度有关。
假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:
- 客户端1成功锁住了A, B, C, 获取锁 成功(但D和E没有锁住)。
- 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。
- 节点C重启后,客户端2锁住了C, D, E, 获取锁 成功。
这样,客户端1和客户端2同时获得了锁(针对同一资源)。
在默认情况下,Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync),因此最坏情况下可能丢失1秒的数据。为了尽可能不丢数据,Redis允许设置成每次修改数据都进行fsync,但这会降低性能。当然,即使执行了fsync也仍然有可能丢失数据(这取决于系统而不是Redis的实现)。所以,上面分析的由于节点重启引发的锁失效问题,总是有可能出现的。为了应对这一问题,antirez又提出了 延迟重启 (delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。
关于Redlock还有一点细节值得拿出来分析一下:在最后 释放锁 的时候,antirez在算法描述中特别强调,客户端应该向所有Redis节点发起 释放锁 的操作。也就是说,即使当时向某个节点获取锁没有成功,在释放锁的时候也不应该漏掉这个节点。这是为什么呢?设想这样一种情况,客户端发给某个Redis节点的 获取锁 的请求成功到达了该Redis节点,这个节点也成功执行了 SET
操作,但是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。因此,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。实际上,这种情况在异步通信模型中是有可能发生的:客户端向服务器通信是正常的,但反方向却是有问题的。