① 使用redis实现的分布式锁原理是什么
一、写在前面
现在面试,一般都会聊聊分布式系统这块的东西。通常面试官都会从服务框架(Spring Cloud、Dubbo)聊起,一路聊到分布式事务、分布式锁、ZooKeeper等知识。
所以咱们这篇文章就来聊聊分布式锁这块知识,具体的来看看Redis分布式锁的实现原理。
说实话,如果在公司里落地生产环境用分布式锁的时候,一定是会用开源类库的,比如Redis分布式锁,一般就是用Redisson框架就好了,非常的简便易用。
大家如果有兴趣,可以去看看Redisson的官网,看看如何在项目中引入Redisson的依赖,然后基于Redis实现分布式锁的加锁与释放锁。
下面给大家看一段简单的使用代码片段,先直观的感受一下:
大家看到了吧,那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数
(5)释放锁机制
如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。
其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。
如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:
“del myLock”命令,从redis里删除这个key。
然后呢,另外的客户端2就可以尝试完成加锁了。
这就是所谓的分布式锁的开源Redisson框架的实现机制。
一般我们在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。
(6)上述Redis分布式锁的缺点
其实上面那种方案最大的问题,就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。
但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。
接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。
此时就会导致多个客户端对一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。
所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。
② 分布式锁实现及常见问题
对于平台系统,由于系统是集群模式,每个节点都是无状态模式,提供统一的功能,所以无法使用JVM的锁,需要使用分布式锁保证所有机器只有一个在执行
由于分布式锁一般是通过其他机器或进程(redis,zk,mysql等)来记录状态所以需要有的特性
Redis为AP模式,可能存在获取到多把锁,比如在主从切换时,Zk为CP架构,不会存在获取到多把锁,因为在创建节点时会同步给半数节点。redis分布式锁相对更快
设置锁的超时时间,宕机后只会影响一定的时间
获取锁时判断设置的值是否和当前线程的信息一样
使用watchDog进行续租
极端情况下,节点1获取到了锁,发送修改了值的请求,请求已经发送出去,但是还没有发送到资源服务器,此时节点1断开了ZK连接,节点2拿到了锁,也发送了修改值的命令,资源服务器先收到节点2的请求,之后又收到了节点1的请求就会出错
③ 分布式锁是什么
什么是分布式锁?实现分布式锁的三种方式
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。那具体什么是分布式锁,分布式锁应用在哪些业务场景、如何来实现分布式锁呢?
一 为什么要使用分布式锁
我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的锁进行处理,并且可以完美的运行,毫无Bug!
注意这是单机应用,后来业务发展,需要做集群,一个应用需要部署到几台机器上然后做负载均衡,大致如下图:
六、基于ZooKeeper的实现方式
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。
七、总结
上面的三种实现方式,没有在所有场合都是完美的,所以,应根据不同的应用场景选择最适合的实现方式。
在分布式环境中,对资源进行上锁有时候是很重要的,比如抢购某一资源,这时候使用分布式锁就可以很好地控制资源。
当然,在具体使用中,还需要考虑很多因素,比如超时时间的选取,获取锁时间的选取对并发量都有很大的影响,上述实现的分布式锁也只是一种简单的实现,主要是一种思想。
④ Redisson实现分布式锁原理
如图所示啊,石杉大佬画的redisson分布式锁原理。
大概总结下,保证我们的key落到一个集群里,并且加锁操作是基于lua脚本的原子性操作,对于锁延迟由watch dog控制。
具体可以看 https://www.cnblogs.com/AnXinliang/p/10019389.html
如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。
接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。
此时就会 导致多个客户端对一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。
所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。
如果主动结构redis架构模式下,我们想保证完全一致,必须重写加锁的逻辑了, 保证必须mater和slave同时加锁成功,我们整个加锁才是成功的 。
上面的2是对于单个主从结构我们可以这样干,如果假设我们有多个相对独立的master,无slave呢?我们在其中一个master上加了🔐,然后它挂了岂不是我们的数据会在剩下的结点会重新加锁成功?
redis引入了 红锁 的概念:用Redis中的多个master实例,来获取锁,只有 大多数 实例获取到了锁,才算是获取成功 。
具体的红锁算法分为以下五步:
以上步骤来自redis 分布式锁的解释,如下
”相同的key和随机值(随机值用于唯一关系客户端和key)在N个节点上请求锁“
这里的随机数是什么?
这对于 避免删除由另一个客户端创建的锁 很重要。例如,客户端可能会获取锁,执行某些操作时被阻塞的时间超过锁的有效时间(密钥将过期的时间),然后移除已被其他客户端获取的锁。使用 just DEL 是不安全的,因为客户端可能会删除另一个客户端的锁。使用上面的脚本,每个锁都用一个随机字符串“签名”,所以只有当它仍然是客户端试图移除它时设置的锁才会被移除。
这个随机字符串应该是什么?我们假设它是 20 个字节 /dev/urandom ,但您可以找到更便宜的方法使其对您的任务足够独特。例如,一个安全的选择是用 RC4 播种 /dev/urandom ,并从中生成一个伪随机流。一个更简单的解决方案是使用具有微秒精度的 UNIX 时间戳,将 时间戳与客户端 ID 连接起来。它并不安全,但对于大多数环境来说可能就足够了。
假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:
为了应对这一问题,提出了 延迟重启 (delayed restarts)的概念。
就是,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。
这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。
客户端1在获得锁之后发生了很长时间的GC pause,在此期间,它获得的锁过期了,而客户端2获得了锁。
当客户端1从GC pause中恢复过来的时候,它不知道自己持有的锁已经过期了,它依然向共享资源( 比如 一个存储服务)发起了写数据请求,
而这时锁实际上被客户端2持有,因此两个客户端的写请求就有可能冲突(锁的互斥作用失效了)。
如何解决这个问题呢?引入了 fencing token 的概念:
首先:RedLock根据随机字符串来作为单次锁服务的token,这就意味着对于资源而言,无法根据锁token来区分client持有的锁所获取的先后顺序。
fencing token可以理解成采用全局递增的序列替代随机字符串,即 有序token ,作为锁token来使用
流程:
假设有5个Redis节点A, B, C, D, E。
这个问题用Redis实现分布式锁暂时无解。而生产环境这种情况是存在的。
时钟跳跃是可以避免的,取决于基础设施和运维; 时钟跳跃是可以避免的,取决于基础设施和运维;
redis是保持的AP而非CP,如果要追求强一致性可以使用zookeeper分布式锁,但是zookeeper也不是完全没问题,在出现网络颜值,客户端与服务端失联情况的时候也依然可能会出现分布式的问题。
⑤ 分布式锁的三种实现方式
分布式锁三种实现方式:
1、基于数据库实现分布式锁;
2、基于缓存(Redis等)实现分布式锁;
3、基于Zookeeper实现分布式锁。从性能角度(从高到低)来看:“缓存方式>Zookeeper方式>=数据库方式”。
⑥ 分布式锁的三种实现方式
分布式锁的三种实现方式分别是:基于数据库实现分布式锁、基于缓存(Redis等)实现分布式锁、基于Zookeeper实现分布式锁。
一、基于数据库实现分布式锁
1、悲观锁
利用select … where … for update 排他锁。
注意:其他附加功能与实现一基本一致,这里需要注意的是“where name=lock ”,name字段必须要走索引,否则会锁表。有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题。
2、乐观锁
所谓乐观锁与前边最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到,抢购和秒杀就是用了这种实现以防止超卖,通过增加递增的版本号字段实现乐观锁。
二、基于缓存(Redis等)实现分布式锁
1、使用命令介绍:
(1)SETNX
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
(2)expire
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
(3)delete
delete key:删除key
在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。
2、实现思想:
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
三、基于Zookeeper实现分布式锁
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。
基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock。
(2)线程A想获取锁就在mylock目录下创建临时顺序节点。
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁。
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点。
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
(6)数据库锁分布式锁扩展阅读;
一、数据库分布式锁实现的缺点:
1、db操作性能较差,并且有锁表的风险。
2、非阻塞操作失败后,需要轮询,占用cpu资源。
3、长时间不commit或者长时间轮询,可能会占用较多连接资源。
二、Redis(缓存)分布式锁实现的缺点:
1、锁删除失败,过期时间不好控制。
2、非阻塞,操作失败后,需要轮询,占用cpu资源。
三、ZK分布式锁实现的缺点:
性能不如redis实现,主要原因是写操作(获取锁释放锁)都需要在Leader上执行,然后同步到follower。
⑦ 分布式锁原理
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。原理就是,当我们要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。
就是要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。
在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。
当在分布式模型下,数据可能只有一份,此时需要利用锁的技术控制某一时刻修改数据的进程数。与单机模式下的锁不同,分布式锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。
分布式情况下之所以问题变得复杂,主要就是需要考虑到网络的延时和不可靠。分布式锁还是可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存如Redis、Memcache。至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。
⑧ 什么是分布式锁实现方式有哪些
什么是分布式锁?实现分布式锁的三种方式在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。那具体什么是分布式锁,分布式锁应用在哪些业务场景、如何来实现分布式锁呢?
一 为什么要使用分布式锁
我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的锁进行处理,并且可以完美的运行,毫无Bug!
注意这是单机应用,后来业务发展,需要做集群,一个应用需要部署到几台机器上然后做负载均衡,大致如下图:
上图可以看到,变量A存在三个服务器内存中(这个变量A主要体现是在一个类中的一个成员变量,是一个有状态的对象),如果不加任何控制的话,变量A同时都会在分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的!即使不是同时发过来,三个请求分别操作三个不同内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的!
如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题!
为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用并发处理相关的功能进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的应用并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
二、分布式锁应该具备哪些条件
在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
三、分布式锁的三种实现方式
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition
tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。
⑨ 分布式锁
与分布式锁对应的是【单机锁】,我们在写多线程程序时,避免同时操作一个共享变量而产生数据问题,通常会使用一把锁来实现【互斥】,其使用范围是在【同一个进程中】。(同一个进程内存是共享的,以争抢同一段内存,来判断是否抢到锁)。
如果是多个进程,如何互斥呢。就要引入【分布式锁】来解决这个问题。想要实现分布式锁,必须借助一个外部系统,所有进程都去这个系统上去【申请加锁】。
而这个外部系统,必须要实现【互斥】的能力,即两个请求同时进来,只会给一个进程返回成功,另一个返回失败(或等待)。
这个外部系统,可以是 MySQL,也可以是 Redis 或 Zookeeper。但为了追求更好的性能,我们通常会选择使用 Redis 或 Zookeeper 来做。
依赖mysql的行锁 select for update。
一个特例,唯一索引。唯一索引是找到了就直接停止遍历,非唯一索引还会向后遍历一行。移步第八个case。
现在的索引:
分析:
单条命中只会加行锁,不加间隙锁。所以RC/RR是一样的。
事务1对id=10这条记录加行锁。所以 场景1会锁等待,场景2不会锁等待 。
分析:
RC隔离级别:
事务1未命中,不会加任何锁。所以 场景1,场景2,场景3都不会锁等待 。
RR隔离级别:
事务1未命中,会加间隙锁。因为主键查询,只会对主键加锁。
在10和18加间隙锁。
间隙锁和查询不冲突。 场景1不会锁等待 。
间隙锁和插入冲突。 场景2和场景3会锁等待 。
分析
单条命中只会加行锁,不加间隙锁。所以RC/RR是一样的。
事务1对二级索引和主键索引加行锁。 事务1和事务2都会发生锁等待 。
分析
RC隔离级别:
事务1未命中,不会加任何锁。所以 场景1,场景2,场景3都不会锁等待 。
RR隔离级别:
事务1对二级索引N0007到正无穷上间隙锁,主键索引不上锁。 场景1会锁等待,场景2不会锁等待 。
分析:
RC隔离级别:
只会加行锁。 场景1场景2会锁等待 。 场景3不会发生锁等待 。
RR隔离级别:
会加行锁和间隙锁。 场景1场景2场景3都会锁等待 。
ps: 如果是唯一索引,只会加行锁。非唯一才会加间隙锁。
RC隔离级别:
事务1未命中,不会加任何锁。所以 场景1,场景2都不会锁等待 。
RR隔离级别:
事务1未命中,会加间隙锁。间隙锁与查询不冲突, 场景1不会发生锁等待 。 场景2会发生锁等待 。
分析
RC隔离级别:
事务1加了三个行锁。 场景1会锁等待。场景2,场景3不会发生锁等待 。
RR隔离级别:
事务1加个三个行锁和间隙锁。 场景1,场景3会发生锁等待 。间隙锁与查询不冲突, 场景2不会锁等待。
分析:
RC隔离级别:
事务1加的都是行锁。 场景1会发生锁等待 , 场景2,场景3不会发生锁等待 。
RR隔离级别:
事务1会对二级索引加行锁和间隙锁,对主键索引加行锁。
场景1,场景3会发生锁等待 。间隙锁与查询不冲突, 场景2不会锁等待。
这么看,二级索引和唯一索引没什么区别。
那如果是 select * from book where name < 'Jim' for update; 呢
如果name是唯一索引。因为找到jim就不会向后遍历了,所以jim和rose之间不会有间隙锁。
分析:
RC隔离级别:
由于没有走索引,所以只能全表扫描。在命中的主键索引上加行锁。 场景1会锁等待,场景2不会锁等待 。
RR隔离级别:
不开启innodb_locks_unsafe_for_binlog。 会发生锁表 。
开启innodb_locks_unsafe_for_binlog。和RC隔离级别一样。
RC隔离级别:
未命中不加锁。 场景1,场景2都不会锁等待 。
RR隔离级别:
未命中, 锁表 。
在RR隔离级别下,where条件没有索引,都会锁表。
加锁命令:
释放锁命令:
这里存在问题,当释放锁之前异常退出了。这个锁就永远不会被释放了。
怎么解决呢?加一个超时时间。
还有问题,不是原子操作。
redis 2.6.12之后,redis天然支持了
来看一下还有什么问题:
试想这样一个场景
看到了么,这里存在两个严重的问题:
释放别人的锁 :客户端 1 操作共享资源完成后,却又释放了客户端 2 的锁
锁过期 :客户端 1 操作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有
解决办法是:客户端在加锁时,设置一个只有自己知道的【唯一标识】进去。
例如,可以是自己的线程id,也可以是一个uuid
在释放锁时,可以这么写:
问题来了,还不是原子的。redis没有原生命令了。这里需要使用lua脚本
锁的过期时间如果评估不好,这个锁就会有“提前”过期的风险,一般的妥协方案是,尽量“冗余”过期时间,降低锁提前过期的概率。
其实可以有比较好的方案:
加锁时,先设置一个过期时间,然后我们开启一个“守护****线程****”,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行“续期”,重新设置过期时间。
这个守护线程我们一般把他叫做【看门狗】线程。
我们在使用 Redis 时,一般会采用 主从集群 + 哨兵 的模式部署,这样做的好处在于,当主库异常宕机时,哨兵可以实现“故障自动切换”,把从库提升为主库,继续提供服务,以此保证可用性。
试想这样的场景:
为此,Redis 的作者提出一种解决方案,就是我们经常听到的 Redlock(红锁) 。
现在我们来看,Redis 作者提出的 Redlock 方案,是如何解决主从切换后,锁失效问题的。
Redlock 的方案基于 2 个前提:
也就是说,想用使用 Redlock,你至少要部署 5 个 Redis 实例,而且都是主库,它们之间没有任何关系,都是一个个孤立的实例。
Redlock 具体如何使用呢?
整体的流程是这样的,一共分为 5 步:
有 4 个重点:
1) 为什么要在多个实例上加锁?
本质上是为了“容错”,部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。
2) 为什么大多数加锁成功,才算成功?
多个 Redis 实例一起来用,其实就组成了一个“分布式系统”。
在分布式系统中,总会出现“异常节点”,所以,在谈论分布式系统问题时,需要考虑异常节点达到多少个,也依旧不会影响整个系统的“正确性”。
这是一个分布式系统“容错”问题,这个问题的结论是: 如果只存在“故障”节点,只要大多数节点正常,那么整个系统依旧是可以提供正确服务的。
3) 为什么步骤 3 加锁成功后,还要计算加锁的累计耗时?
因为操作的是多个节点,所以耗时肯定会比操作单个实例耗时更久,而且,因为是网络请求,网络情况是复杂的,有可能存在 延迟、丢包、超时 等情况发生,网络请求越多,异常发生的概率就越大。
所以,即使大多数节点加锁成功,但如果加锁的累计耗时已经“超过”了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。
4) 为什么释放锁,要操作所有节点?
在某一个 Redis 节点加锁时,可能因为“网络原因”导致加锁失败。
例如,客户端在一个 Redis 实例上加锁成功,但在读取响应结果时,网络问题导致 读取失败 ,那这把锁其实已经在 Redis 上加锁成功了。
所以,释放锁时,不管之前有没有加锁成功,需要释放“所有节点”的锁,以保证清理节点上“残留”的锁。
好了,明白了 Redlock 的流程和相关问题,看似 Redlock 确实解决了 Redis 节点异常宕机锁失效的问题,保证了锁的“安全性”。
在martin的文章中,主要阐述了4个论点:
第一:效率 。
使用分布式锁的互斥能力,是避免不必要地做同样的工作两次。如果锁失效,并不会带来“恶性”的后果,例如发了 2 次邮件等,无伤大雅。
第二:正确性 。
使用锁用来防止并发进程相互干扰。如果锁失败,会造成多个进程同时操作同一条数据,产生的后果是数据严重错误、永久性不一致、数据丢失等恶性问题,后果严重。
他认为,如果你是为了前者——效率,那么使用单机版redis就可以了,即使偶尔发生锁失效(宕机、主从切换),都不会产生严重的后果。而使用redlock太重了,没必要。
而如果是为了正确性,他认为redlock根本达不到安全性的要求,也依旧存在锁失效的问题!
一个分布式系统,存在着你想不到的各种异常。这些异常场景主要包括三大块,这也是分布式系统会遇到的三座大山: NPC
martin用一个进程暂停(GC)的例子,指出了redlock安全性的问题:
又或者,当多个Redis节点时钟发生了问题时,也会导致redlock锁失效。
在混乱的分布式系统中,你不能假设系统时钟就是对的。
个人理解,相当于在业务层再做一层乐观锁。
一个好的分布式锁,无论 NPC 怎么发生,可以不在规定时间内给出结果,但并不会给出一个错误的结果。也就是只会影响到锁的“性能”(或称之为活性),而不会影响它的“正确性”。
1、Redlock 不伦不类 :它对于效率来讲,Redlock 比较重,没必要这么做,而对于正确性来说,Redlock 是不够安全的。
2、时钟假设不合理 :该算法对系统时钟做出了危险的假设(假设多个节点机器时钟都是一致的),如果不满足这些假设,锁就会失效。
3、无法保证正确性 :Redlock 不能提供类似 fencing token 的方案,所以解决不了正确性的问题。为了正确性,请使用有“共识系统”的软件,例如 Zookeeper。
好了,以上就是 Martin 反对使用 Redlock 的观点,看起来有理有据。
下面我们来看 Redis 作者 Antirez 是如何反驳的。
首先,Redis 作者一眼就看穿了对方提出的最为核心的问题: 时钟问题 。
Redis 作者表示,Redlock 并不需要完全一致的时钟,只需要大体一致就可以了,允许有“误差”。
例如要计时 5s,但实际可能记了 4.5s,之后又记了 5.5s,有一定误差,但只要不超过“误差范围”锁失效时间即可,这种对于时钟的精度的要求并不是很高,而且这也符合现实环境。
对于对方提到的“时钟修改”问题,Redis 作者反驳到:
Redis 作者继续论述,如果对方认为,发生网络延迟、进程 GC 是在客户端确认拿到了锁,去操作共享资源的途中发生了问题,导致锁失效,那这 不止是 Redlock 的问题,任何其它锁服务例如 Zookeeper,都有类似的问题,这不在讨论范畴内 。
这里我举个例子解释一下这个问题:
Redis 作者这里的结论就是:
所以,Redis 作者认为 Redlock 在保证时钟正确的基础上,是可以保证正确性的。
这个方案必须要求要操作的“共享资源服务器”有拒绝“旧 token”的能力。
例如,要操作 MySQL,从锁服务拿到一个递增数字的 token,然后客户端要带着这个 token 去改 MySQL 的某一行,这就需要利用 MySQL 的“事物隔离性”来做。
但如果操作的不是 MySQL 呢?例如向磁盘上写一个文件,或发起一个 HTTP 请求,那这个方案就无能为力了,这对要操作的资源服务器,提出了更高的要求。
也就是说,大部分要操作的资源服务器,都是没有这种互斥能力的。
再者,既然资源服务器都有了“互斥”能力,那还要分布式锁干什么?
利用 zookeeper 的同级节点的唯一性特性,在需要获取排他锁时,所有的客户端试图通过调用 create() 接口,在 /exclusive_lock 节点下创建临时子节点 /exclusive_lock/lock ,最终只有一个客户端能创建成功,那么此客户端就获得了分布式锁。同时,所有没有获取到锁的客户端可以在 /exclusive_lock 节点上注册一个子节点变更的 watcher 监听事件,以便重新争取获得锁。
锁释放依赖心跳。集群中占用锁的客户端失联时,锁能够被有效释放。一旦占用Znode锁的客户端与ZooKeeper集群服务器失去联系,这个临时Znode也将自动删除
zookeeper的高可用依赖zab。简单的说就是写入时,半数follower ack,写入成功。
zk是100%安全的么:
分析一个例子:
所以,得出一个结论: 一个分布式锁,在极端情况下,不一定是安全的。
redlock运维成本也比较高。单机有高可用问题。所以还是主从+哨兵这样的部署方式会好一些。
redis的缺点是:不是100%可靠。
mysql的缺点是:扛不住高流量请求。
可以二者结合,先用redis做分布式锁,扛住大部分流量缓解mysql压力。最后一定要用mysql做兜底保证100%的正确性。
⑩ Redis 分布式锁详细分析
锁的作用,我想大家都理解,就是让不同的线程或者进程可以安全地操作共享资源,而不会产生冲突。
比较熟悉的就是 Synchronized 和 ReentrantLock 等,这些可以保证同一个 jvm 程序中,不同线程安全操作共享资源。
但是在分布式系统中,这种方式就失效了;由于分布式系统多线程、多进程并且分布在不同机器上,这将使单机并发控制锁策略失效,为了解决这个问题就需要一种跨 JVM 的互斥机制来控制共享资源的访问。
比较常用的分布式锁有三种实现方式:
本篇文章主要讲解基于 Redis 分布式锁的实现。
分布式锁最主要的作用就是保证任意一个时刻,只有一个客户端能访问共享资源。
我们知道 redis 有 SET key value NX 命令,仅在不存在 key 的时候才能被执行成功,保证多个客户端只有一个能执行成功,相当于获取锁。
释放锁的时候,只需要删除 del key 这个 key 就行了。
上面的实现看似已经满足要求了,但是忘了考虑在分布式环境下,有以下问题:
最大的问题就是因为客户端或者网络问题,导致 redis 中的 key 没有删除,锁无法释放,因此其他客户端无法获取到锁。
针对上面的情况,使用了下面命令:
使用 PX 的命令,给 key 添加一个自动过期时间(30秒),保证即使因为意外情况,没有调用释放锁的方法,锁也会自动释放,其他客户端仍然可以获取到锁。
注意给这个 key 设置的值 my_random_value 是一个随机值,而且必须保证这个值在客户端必须是唯一的。这个值的作用是为了更加安全地释放锁。
这是为了避免删除其他客户端成功获取的锁。考虑下面情况:
因此这里使用一个 my_random_value 随机值,保证客户端只会释放自己获取的锁,即只删除自己设置的 key 。
这种实现方式,存在下面问题:
上面章节介绍了,简单实现存在的问题,下面来介绍一下 Redisson 实现又是怎么解决的这些问题的。
主要关注 tryAcquireOnceAsync 方法,有三个参数:
方法主要流程:
这个方法的流程与 tryLock(long waitTime, long leaseTime, TimeUnit unit) 方法基本相同。
这个方法与 tryAcquireOnceAsync 方法的区别,就是一个获取锁过期时间,一个是能否获取锁。即 获取锁过期时间 为 null 表示获取到锁,其他表示没有获取到锁。
获取锁最终都会调用这个方法,通过 lua 脚本与 redis 进行交互,来实现分布式锁。
首先分析,传给 lua 脚本的参数:
lua 脚本的流程:
为了实现无限制持有锁,那么就需要定时刷新锁的过期时间。
这个类最重要的是两个成员属性:
使用一个静态并发集合 EXPIRATION_RENEWAL_MAP 来存储所有锁对应的 ExpirationEntry ,当有新的 ExpirationEntry 并存入到 EXPIRATION_RENEWAL_MAP 集合中时,需要调用 renewExpiration 方法,来刷新过期时间。
创建一个超时任务 Timeout task ,超时时间是 internalLockLeaseTime / 3 , 过了这个时间,即调用 renewExpirationAsync(threadId) 方法,来刷新锁的过期时间。
判断如果是当前线程持有的锁,那么就重新设置过期时间,并返回 1 即 true 。否则返回 0 即 false 。
通过调用 unlockInnerAsync(threadId) 来删除 redis 中的 key 来释放锁。特别注意一点,当不是持有锁的线程释放锁时引起的失败,不需要调用 cancelExpirationRenewal 方法,取消定时,因为锁还是被其他线程持有。
传给这个 lua 脚本的值:
这个 lua 脚本的流程:
调用了 LockPubSub 的 subscribe 进行订阅。
这个方法的作用就是向 redis 发起订阅,但是对于同一个锁的同一个客户端(即 一个 jvm 系统) 只会发起一次订阅,同一个客户端的其他等待同一个锁的线程会记录在 RedissonLockEntry 中。
方法流程:
只有当 counter >= permits 的时候,回调 listener 才会运行,起到控制 listener 运行的效果。
释放一个控制量,让其中一个回调 listener 能够运行。
主要属性:
这个过程对应的 redis 中监控的命令日志:
因为看门狗的默认时间是 30 秒,而定时刷新程序的时间是看门狗时间的 1/3 即 10 秒钟,示例程序休眠了 15 秒,导致触发了刷新锁的过期时间操作。
注意 rLock.tryLock(10, TimeUnit.SECONDS); 时间要设置大一点,如果等待时间太短,小于获取锁 redis 命令的时间,那么就直接返回获取锁失败了。
分析源码我们了解 Redisson 模式的分布式,解决了锁过期时间和可重入的问题。但是针对 redis 本身可能存在的单点失败问题,其实是没有解决的。
基于这个问题, redis 作者提出了一种叫做 Redlock 算法, 但是这种算法本身也是有点问题的,想了解更多,请看 基于Redis的分布式锁到底安全吗?