redis cluster 分布式锁_Redis的分布式锁的实现原理

在了解Redi的分布式锁之前先来看一下线程锁和进程锁、分布式锁的区别 线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。

进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。

分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

  • 分布式锁解决了什么问题呢?

随着业务越来越复杂,应用服务都会朝着分布式、集群方向部署,而分布式CAP原则告诉我们,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。实践中一般满足CP或AP。 很多场景中,需要使用分布式事务、分布式锁等技术来保证数据最终一致性。有的时候,我们需要保证某一方法同一时刻只能被一个线程执行。 在单机环境中,多个线程对共享变量进行访问时,我们可以简单的通过Java自身的同步操作来协调同一时刻对变量的串行访问。然而在分布式环境中,进程的独立性,进程之间无法访问相互之间的资源,无法像之前那样的方式实现进程锁,故需要一个独立的中心节点,以协调多个系统对共享变量的访问,所有进程在访问该变量时,都从同一个地方进行取值并控制,从而实现在类似于单机环境中同步控制的效果。

  • 怎么实现? 分布式锁的三个主要元素:加锁,解锁,锁超时。 1:使用setnx、expire两个命令实现 在redis2.6.12版本之前,分布式锁常使用setnx来实现,加锁的时候使用 setnx(key,value)命令 ,这个命令setnx(set if not exists)的意思是也就是当值不存在时,才可以创建成功返回1,否则返回0。但是,setnx无法在插入值的同时设置超时时间,setnx 与 expire 是两条独立的语句,这样加锁操作就是非原子性的,那么就会带来问题。(比如,当setnx成功后,准备执行expire前,程序突然出现错误,则添加的数据就无法清除了,因为没有超时时间,不会自动清除) 2:使用 set key randomvalue [EX seconds] [PX milliseconds] [NX|XX] randomvalue 是客户端生成的唯一的字符串。 NX 代表只在键不存在时,才对键进行设置操作。 EX seconds 设置键的过期时间为seconds 毫秒。 PX milliseconds 设置键的过期时间为milliseconds毫秒。

在redis2.6.12版本之后,redis支持通过set在设置值得同时设置超时时间,此操作是原子操作。

// 设置lock的值为123,存在6秒127.0.0.1:6379> set lock 123 EX 6 NXOK// 6秒内,重复设置lock的值为123,返回nil(也就是null)127.0.0.1:6379> set lock 123 EX 6 NX(nil)// 6秒内,获取值,能够获取到127.0.0.1:6379> get lock"123"// 6秒后,获取值,获取为nil,又可以重新set值了127.0.0.1:6379> get lock(nil)

解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候random_value的作用就体现出来。删除的时候比较一下value的值是否一样。也可以将value的值设置为当前线程的ID,del数据之前,增加锁判断机制:判断要删除的锁是否属于本线程。操作流程:   (1)加锁:set(id, threadId,expire),其中value为当前线程ID;   (2)解锁:执行del命令时,根据id和threadId数据判断该锁是否仍属于本线程。是,则删除。 这套redis加解锁机制看起来很完美,然而有一个无法避免的硬伤,就是过期时间如何设置。如果客户端在操作共享资源的过程中,因为长期阻塞的原因,导致锁过期,那么接下来访问共享资源就不安全。 业务场景:我们有一个内容修改页面,为了避免出现多个客户端修改同一个页面的请求,采用分布式锁。只有获得锁的客户端,才能修改页面。那么正常修改一次页面的流程如下图所示

9cd933f0212945e985dfa0d0b92302df

注意看,上面的步骤(3)-->步骤(4.1)并不是原子性操作。也就说,你可能出现在步骤(3)的时候返回的是有效这个标志位,但是在传输过程中,因为延时等原因,在步骤(4.1)的时候,锁已经超时失效了。那么,这个时候锁就会被另一个客户端锁获得。就出现了两个客户端共同操作共享资源的情况。 大家可以思考一下,无论你如何采用任何补偿手段,你都只能降低多个客户端操作共享资源的概率,而无法避免。例如,你在步骤(4.1)的时候也可能发生长时间GC停顿,然后在停顿的时候,锁超时失效,从而锁也有可能被其他客户端获得。这些大家可以自行思考推敲。 在集群情况下 为了redis的高可用,一般都会给redis的节点挂一个slave,然后采用哨兵模式进行主备切换。但由于Redis的主从复制(replication)是异步的,这可能会出现在数据同步过程中,master宕机,slave来不及同步数据就被选为master,从而数据丢失。具体流程如下所示:

(1)客户端1从Master获取了锁。(2)Master宕机了,存储锁的key还没有来得及同步到Slave上。(3)Slave升级为Master。(4)客户端2从新的Master获取到了对应同一个资源的锁。

为了应对这个情形, redis的作者antirez提出了RedLock算法,步骤如下(该流程出自官方文档),假设我们有N个master节点(官方文档里将N设置成5,其实大等于3就行) (1)获取当前时间(单位是毫秒)。 (2)轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。 (3)客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。 (4)如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。 (5)如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁 分析:RedLock算法细想一下还存在下面的问题 节点崩溃重启,会出现多个客户端持有锁 假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列: (1)客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。 (2)节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。 (3)节点C重启后,客户端2锁住了C, D, E,获取锁成功。 这样,客户端1和客户端2同时获得了锁(针对同一资源)。

为了应对节点重启引发的锁失效问题,redis的作者antirez提出了延迟重启的概念,即一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,等待的时间大于锁的有效时间。采用这种方式,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。这其实也是通过人为补偿措施,降低不一致发生的概率。 时间跳跃问题 (1)假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列: (2)客户端1从Redis节点A, B, C成功获取了锁(多数节点)。由于网络问题,与D和E通信失败。 (3)节点C上的时钟发生了向前跳跃,导致它上面维护的锁快速过期。 客户端2从Redis节点C, D, E成功获取了同一个资源的锁(多数节点)。 客户端1和客户端2现在都认为自己持有了锁。

为了应对始终跳跃引发的锁失效问题,redis的作者antirez提出了应该禁止人为修改系统时间,使用一个不会进行“跳跃”式调整系统时钟的ntpd程序。这也是通过人为补偿措施,降低不一致发生的概率。 超时导致锁失效问题 RedLock算法并没有解决,操作共享资源超时,导致锁失效的问题。回忆一下RedLock算法的过程,如下图所示

f92456045dc74267a501d8f6bd6bb0b1

如图所示,我们将其分为上下两个部分。对于上半部分框图里的步骤来说,无论因为什么原因发生了延迟,RedLock算法都能处理,客户端不会拿到一个它认为有效,实际却失效的锁。然而,对于下半部分框图里的步骤来说,如果发生了延迟导致锁失效,都有可能使得客户端2拿到锁。因此,RedLock算法并没有解决该问题。至于Redis分布式锁的代码实现,可以自行百度即可 这里只进行说明了实现的方式以及存在的问题。

- Redis 的持久化 RDB、AOF

Redis是基于内存型的数据库,在提供高性能的同时也需要对内存里的数据做持久化操作,Redis的持久化分为RDM、AOF两种持久化方案。这两种方案各有优点和缺点,可以互相配合使用。这主要是看你的业务场景对数据的容忍程度,根据业务数据来选择采用哪种方案。

  • RDB(Redis DataBase)持久化 这种持久化的方式是将数据库的数据以快照(二进制)的形式由内存保存到磁盘中,RDB方式的持久化几乎不损耗Redis本身的性能,在进行RDB持久化时,Redis主进程唯一需要做的事情就是fork出一个子进程,所有持久化工作都由子进程完成。下面看一下RDB在配置文件的配置信息: RDB默认是开启的,图中的 save [seconds] [changes] 意思为在 [seconds]秒的时间里数据发生了 [changes] 变化的话就进行一次RDB快照操作,同样的Redis可以配置多条save指令,让Redis执行多级的快照保存策略。 save 900 1 #Redis每900秒检查一次数据变更情况,如果发生了1次或以上的数据变更,则进行RDB快照保存 save 300 10 #Redis每300秒检查一次数据变更情况,如果发生了10次或以上的数据变更,则进行RDB快照保存 save 60 10000 #Redis每60秒检查一次数据变更情况,如果发生了10000次或以上的数据变更,则进行RDB快照保存 当然也可以进行手工触发Redis执行RDB快照,通过执行BGSAVE命令。 下面看一下RDB相关的其他命令如下图:   1、stop-writes-on-bgsave-error :默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了  2、rdbcompression ;默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。  3、rdbchecksum :默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。  4、dbfilename :设置快照的文件名,默认是 dump.rdb  5、dir:设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。使用上面的 dbfilename 作为保存的文件名。  总的来说RDB文件适合数据的容灾备份与恢复,通过RDB文件恢复数据库耗时较短,可以快速恢复数据。但是RDB持久化只会周期性的保存数据,在未触发下一次存储时服务宕机,就会丢失增量数据。当数据量较大的情况下,fork子进程这个操作很消耗cpu,可能会发生长达秒级别的阻塞情况。RDB持久化分为SAVE、BGSAVE两种方式: SAVE是阻塞式持久化,执行命令时Redis主进程把内存数据写入到RDB文件中直到创建完毕,期间Redis不能处理任何命令。 BGSAVE属于非阻塞式持久化,创建一个子进程把内存中数据写入RDB文件里同时主进程处理命令请求。 BGSAVE实现细节

RDB方式的持久化是通过快照实现的,符合条件时Redis会自动将内存数据进行快照并存储在硬盘上,以BGSAVE为例,一次完整数据快照的过程: 1、Redis使用fork函数创建子进程; 2、父进程继续接收并处理命令请求,子进程将内存数据写入临时文件; 3、子进程写入所有数据后会用临时文件替换旧RDB文件;

执行fork的时OS会使用写时拷贝策略,对子进程进行快照过程优化。Redis在进行快照过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是任何时候RDB文件都是完整的。我们可以通过定时备份RDB文件来实现Redis数据库备份,RDB文件是经过压缩的,占用的空间会小于内存中的数据大小。除了自动快照还可以手动发送SAVE或BGSAVE命令让Redis执行快照。通过RDB方式实现持久化,由于RDB保存频率的限制,如果数据很重要则考虑使用AOF方式进行持久化。 优点: 1、只有一个文件 dump.rdb,方便持久化。 2、容灾性好,一个文件可以保存到安全的磁盘。 3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能 4.相对于数据集大时,比 AOF 的启动效率更高。

缺点: 1 数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机了。如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候 2 备份时占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。所以Redis 的持久化和数据的恢复要选择在夜深人静的时候执行是比较合理的。

  • AOF(Append Only File)持久化 AOF持久化是默认没有开启的,他的出现主要解决RDB数据丢失的现象,他会将Redis的每一条写命令通过写函数记录到appendonly.aof(默认文件名)文件中,当做数据恢复的时候Redis会将appendonly.aof的写命令从前到后重新执行一遍,完成数据的回复工作,AOF的功能实现主要通过命令追加(append),)、文件写入( write )、文件同步( sync )、文件重写(rewrite)和重启加载(load)。AOF的流程如下: 1:将所有的写命令追加到对应的AOF缓冲区中。 2:AOF根据配置文件中选定的策略去将AOF缓冲区的数据同步到磁盘文件。 3:定期的对AOF文件进行重写操作,以此来达到减少AOF的大小,避免了许多无用的过期的数据。 4:当需要数据恢复或者重启的时候会加载AOF文件重新执行对应的写命令进行恢复数据。
f342e2763c5349d19364db30da671277

从图中可以看出,AOF的默认文件名字为 appendonly.aof ,AOF功能默认关闭,缓冲区数据同步达到AOF文件有三个值可以选择 always、everysec、no 默认的是everysec,下面介绍下这三个值代表的不同含义: always:每个事件循环都会讲AOF的全部数据写入到AOF文件中,这样最大程度的保证数据的完整性,但是损失了性能,三种策略性能最差的一个,在极端情况下会损失一个事件循环的数据。 everysec:每隔一秒就会在子线程中将数据进行AOF文件写入,这种事默认的策略,同时兼顾了性能和数据完整性,但是同样在极端情况下也会产生丢失1秒的数据可能。 no :不做AOF文件写入,将数据交给操作系统来处理依赖于系统调度,缓存区也空间写满或者达到周期时间,速度最快,但是同样数据安全性和完整最差。当出现宕机或者严重故障的时候可能丢失数据更多。

966c44ba49da41a5bf4a99efeb557aa4

no-appendfsync-on-rewrite:在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。 设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes。Linux的默认fsync策略是30秒。可能丢失30秒数据。默认值为no。

 auto-aof-rewrite-percentage:默认值为100。aof自动重写配置,当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件增长到一定大小的时候,Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。

  auto-aof-rewrite-min-size:64mb。设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写。

  aof-load-truncated:aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项,出现这种现象 redis宕机或者异常终止不会造成尾部不完整现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。如果是no,用户必须手动redis-check-aof修复AOF文件才可以。默认值为 yes。

Redis数据恢复 1:如果只配置 AOF ,重启时加载 AOF 文件恢复数据; 2:如果同时配置了 RDB 和 AOF ,启动只加载 AOF 文件恢复数据; 3:如果只配置 RDB,启动将加载 dump 文件恢复数据。 AOF数据恢复详情 在进行AOF数据恢复的时候,其实就是模仿一遍客户端的写操作,需要创建一个没有网络连接的伪客户端,将AOF的写命令逐个读入然后逐个执行,直到处理完所有命令为止。 AOF重写 当AOF文件记录客户端的写命令是会产生一种现象 比如 我操作了很多次set name yichen 或者执行 set age 21 ->set age 24 ->set age 21,其实左后一次是需要的,但是AOF文件都记录下来了,再比如AOF记录了一些过期数据,或者有新的数据进行写入操作,当数据文件达到一定大小,或者达到增长的百分比,这时候就需要对AOF进行重写操作,由于AOF重写操作会进行大量的写操作,当进行重写的时候会进行阻塞操作,所以会用子进程来进行AOF重写操作,步骤如下 1:子进程进行 AOF 重写期间,Redis 进程可以继续处理客户端命令请求。 2:子进程带有父进程的内存数据拷贝副本(这里你有没有想到java的CopyAndWriteArrayList呢?),在不适用锁的情况下,也可以保证数据的安全性。 这里有一个问题,在子进程进行重写期间,如果有新的客户端进行写命令操作,这样就会导致当前的数据与AOF文件里的不一致,为了解决这个问题Redis专门设置了一个AOF重写缓冲区,当子进程创建完成后这个缓冲区开始使用,在执行AOF重写期间如果有客户端进行写操作的时候会将写命令同时发送给AOF缓冲区和AOF重写缓冲区。当子进程完成 AOF 重写工作之后,它会向父进程发送一个信号,父进程在接收到该信号之后,会调用一个信号处理函数,并执行以下工作: 1:将 AOF 重写缓冲区中的所有内容写入到新的 AOF 文件中。 2:对新的 AOF 文件进行改名,原子地覆盖现有 AOF 文件,完成新旧文件的替换。 3:继续处理客户端请求命令。 在整个 AOF 后台重写过程中,只有信号处理函数执行时会对 Redis 主进程造成阻塞,在其他时候,AOF 后台重写都不会阻塞主进程。 新型混合持久化 RDB和AOF都有各自的缺点: 1: RDB是每隔一段时间持久化一次, 故障时就会丢失宕机时刻与上一次持久化之间的数据,无法保证数据完整性 2: AOF存储的是指令序列, 恢复重放时要花费很长时间并且文件更大

Redis 4.0 提供了更好的混合持久化选项: 创建出一个同时包含 RDB 数据和 AOF 数据的 AOF 文件, 其中 RDB 数据位于 AOF 文件的开头, 它们储存了服务器开始执行重写操作时的数据库状态,至于那些在重写操作执行之后执行的 Redis 命令, 则会继续以 AOF 格式追加到 AOF 文件的末尾, 也即是 RDB 数据之后。

afcec7f8a641401ea0b85225e01dac83

(引自:后端技术指南)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值