赞
踩
目录
应用开发时,如果需要在同进程内的不同线程并发访问某项资源,可以使用各种互斥锁、读写锁;
如果一台主机上的多个进程需要并发访问某项资源,则可以使用进程间同步的原语,例如信号量、管道、共享内存等。
但如果多台主机需要同时访问某项资源,就需要使用一种在全局可见并具有互斥性的锁了。
这种锁就是分布式锁,可以在分布式场景中对资源加锁,避免竞争资源引起的逻辑错误。
Martin Kleppmann 是英国剑桥大学的分布式系统的研究员,之前和 Redis 之父 Antirez 进行过关于 RedLock(红锁,后续有讲到)是否安全的激烈讨论。
Martin 认为一般我们使用分布式锁有两个场景:
效率:使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信。
正确性:加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。
在任意时刻,只有一个客户端持有锁。
分布式锁本质上是一个基于租约(Lease)的租借锁,如果客户端获得锁后自身出现异常,锁能够在一段时间后自动释放,资源不会被锁死。
和本地锁一样支持锁超时,防止死锁。
硬件故障或网络异常等外部问题,以及慢查询、自身缺陷等内部因素都可能导致Redis发生高可用切换,replica提升为新的master。
此时,如果业务对互斥性的要求非常高,锁需要在切换到新的master后保持原状态。
加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁
和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)。
公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。
在Redis中加锁非常简便,直接使用SET命令即可。示例及关键选项说明如下:
SET resource_1 random_value NX EX 5
表 1. 关键选项说明
参数/选项 说明 resource_1 分布式锁的key,只要这个key存在,相应的资源就处于加锁状态,无法被其它客户端访问。 random_value 一个随机字符串,不同客户端设置的值不能相同。 EX设置过期时间,单位为秒。
您也可以使用PX选项设置单位为毫秒的过期时间。
NX 如果需要设置的key在Redis中已存在,则取消设置。示例代码为resource_1这个key设置了5秒的过期时间,如果客户端不释放这个key,5秒后key将过期,锁就会被系统回收,此时其它客户端就能够再次为资源加锁并访问资源了。
解锁一般使用DEL命令,但可能存在下列问题。
- t1时刻,App1设置了分布式锁resource_1,过期时间为3秒。
- App1由于程序慢等原因等待超过了3秒,而resource_1已经在t2时刻被释放。
- t3时刻,App2获得这个分布式锁。
- App1从等待中恢复,在t4时刻运行
DEL resource_1
将App2持有的分布式锁释放了。
从上述过程可以看出,一个客户端设置的锁,必须由自己解开。因此客户端需要先使用GET命令确认锁是不是自己设置的,然后再使用DEL解锁。在Redis中通常需要用Lua脚本来实现自锁自解:
- if redis.call("get",KEYS[1]) == ARGV[1] then
- return redis.call("del",KEYS[1])
- else
- return 0
- end
当客户端发现在锁的租期内无法完成操作时,就需要延长锁的持有时间,进行续租(renew)。同解锁一样,客户端应该只能续租自己持有的锁。在Redis中可使用如下Lua脚本来实现续租:
- if redis.call("get",KEYS[1]) == ARGV[1] then
- return redis.call("expire",KEYS[1], ARGV[2])
- else
- return 0
- end
为了保证 Redis 的可用性,一般采用主从方式部署。主从数据同步有异步和同步两种方式,Redis 将指令记录在本地内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一致的状态,一边向主节点反馈同步情况。
在包含主从模式的集群部署方式中,当主节点挂掉时,从节点会取而代之,但客户端无明显感知。当客户端 A 成功加锁,指令还未同步,此时主节点挂掉,从节点提升为主节点,新的主节点没有锁的数据,当客户端 B 加锁时就会成功。
集群脑裂指因为网络问题,导致 Redis master 节点跟 slave 节点和 sentinel 集群处于不同的网络分区,因为 sentinel 集群无法感知到 master 的存在,所以将 slave 节点提升为 master 节点,此时存在两个不同的 master 节点。Redis Cluster 集群部署方式同理。
当不同的客户端连接不同的 master 节点时,两个客户端可以同时拥有同一把锁。如下:
Redis的主从同步(replication)是异步进行的,如果向master发送请求修改了数据后master突然出现异常,发生高可用切换,缓冲区的数据可能无法同步到新的master(原replica)上,导致数据不一致。如果丢失的数据跟分布式锁有关,则会导致锁的机制出现问题,从而引起业务异常。下文介绍三种保障一致性的方法。
红锁是Redis作者提出的一致性解决方案。红锁的本质是一个概率问题:如果一个主从架构的Redis在高可用切换期间丢失锁的概率是k%
,那么相互独立的N个Redis同时丢失锁的概率是多少?如果用红锁来实现分布式锁,那么丢锁的概率是(k%)^N
。鉴于Redis极高的稳定性,此时的概率已经完全能满足产品的需求。
说明 红锁的实现并非这样严格,一般保证M(1<M=<N)
个同时锁上即可,但通常仍旧可以满足需求。
Redis的WAIT命令会阻塞当前客户端,直到这条命令之前的所有写入命令都成功从master同步到指定数量的replica,命令中可以设置单位为毫秒的等待超时时间。在云Redis版中使用WAIT命令提高分布式锁一致性的示例如下:
- SET resource_1 random_value NX EX 5
- WAIT 1 5000
使用以上代码,客户端在加锁后会等待数据成功同步到replica才继续进行其它操作,最大等待时间为5000毫秒。执行WAIT命令后如果返回结果是1则表示同步成功,无需担心数据不一致。相比红锁,这种实现方法极大地降低了成本。
需要注意的是:
参考链接:
https://redis.io/topics/distlock
https://help.aliyun.com/document_detail/146758.html
https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。