Redis进阶:单线程模型、过期策略、高可用、哨兵、cluster、双写一致性、并发竞争、分布式锁

【本篇资料参考Java工程师进阶扫盲】

Redis基础查看这篇文章

目录

1、首先redis是单线程的,为什么redis会是单线程的呢?

2、聊一聊redis的单线程模型构造部分?

3、redis单线程模型的大致工作流程及原理

4、为什么 Redis 单线程模型也能效率这么高?

5、过期策略:

6、Redis的 主从复制原理/ 同步机制 了解么? 

7、Redis哨兵(sentinel)机制

8、如何保证缓存数据库双写一致性?

9、缓存不一致的问题:

10、Redis 的并发竞争问题

11、生产环境中的 Redis 是怎么部署的?(参考)

12、分布式锁


1、首先redis是单线程的,为什么redis会是单线程的呢?

  • 从redis的性能上进行考虑,单线程避免了上下文频繁切换问题,效率高;
  • 从redis的内部结构设计原理进行考虑,redis是基于Reactor模式开发了自己的网络事件处理器: 这个处理器被称为文件事件处理器(file event handler)。而这个文件事件处理器是单线程的,所以才叫redis的单线程模型,这也决定了redis是单线程的。

2、聊一聊redis的单线程模型构造部分?


redis单线程模型中最为核心的就是文件事件处理器,而文件事件处理器结构包含5个部分,其实真正包含为4个部分(不包含socket队列,加上主要方便后面理解):多个socket、IO多路复用程序、socket队列、文件事件分派器、以及事件处理器。而事件处理器又分为3个部分为:连接应答处理器、命令请求处理器、命令回复处理器。如图:

3、redis单线程模型的大致工作流程及原理

客户端与redis进行通信大致流程:

①首先在redis启动初始化的时候,redis会先将事件处理器中的连接应答处理器和AE_READABLE事件关联起来;

②如果客户端向redis发起连接,会产生AE_READABLE事件(步骤A);产生该事件后会被IO多路复用程序监听到(步骤B);IO多路复用程序会把监听到的socket信息放入到队列中(步骤C);事件分配器每次从队列中取出一个socket(步骤D);事件分派器把socket给对应的事件处理器(步骤E)。(由于连接应答处理器和AE_READABLE事件在redis初始化的时候已经关联起来,所以由连接应答处理器来处理跟客户端建立连接);然后通过ServerSocket创建一个与客户端一对一对应的socket(如叫socket01)。同时将这个socket01的AE_READABLE事件和命令请求处理器关联起来。

③当客户端向redis发生请求时(读、写操作),首先就会在对应的socket如socket01上会产生AE_READABLE事件(步骤A),产生该事件后会被IO多路复用程序监听到(步骤B),然后IO多路复用程序会把监听到的socket信息放入到队列中(步骤C),事件分配器每次从队列中取出一个socket(步骤D),然后事件分派器把socket给对应的事件处理器(步骤E)。由于命令处理器和socket01的AE_READABLE事件关联起来了,然后对应的命令请求处理器来处理。这个命令请求处理器会从事件分配器传递过来的socket01上读取相关的数据,如何执行相应的读写处理。操作执行完之后,redis就会将准备好相应的响应数据(如你在redis客户端输入 set a 123回车时会看到响应ok),并将socket01的AE_WRITABLE事件和命令回复处理器关联起来。

④当客户端会查询redis是否完成相应的操作,就会在socket01上产生一个AE_WRITABLE事件,会由对应的命令回复处理器来处理,就是将准备好的相应数据写入socket01(由于socket连接是双向的)返回给客户端,如读操作,客户端会显示ok。

⑤如果命令回复处理器执行完成后,就会删除这个socket01的AE_WRITABLE事件和命令回复处理器的关联。
⑥这样客户端就和redis进行了一次通信。由于连接应答处理器执行一次就够了,如果客户端在次进行操作就会由命令请求处理器来处理,反复执行。

4、为什么 Redis 单线程模型也能效率这么高?

  • 纯内存操作。
  • 核心是基于非阻塞的 IO 多路复用机制。
  • C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。
  • 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。

注:Redis 6.0 之后的版本也开始选择性地使用多线程模型。因为读写网络的 Read/Write 系统调用在 Redis 执行期间占用了大部分 CPU 时间,如果把网络读写做成多线程的方式对性能会有很大提升。Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。

5、过期策略:

定期删除+惰性删除。Redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。如果此时 key 已经过期,删除不会返回任何东西。

但这样仍然会堆积大量过期key,redis还会有一个内存淘汰机制


内存淘汰机制:
allkeys-lru:写入内存不足时,在键空间中,移除最近最少使用的 key(最常用)。
allkeys-random:随机删除。
volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。
volatile-random:在设置了过期时间的键空间中,随机移除
volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
noeviction: 当写入时内存不足,新写入操作会报错,这个一般没人用。

6、Redis主从架构

Redis主要模式包括单机、主从(主从+哨兵)、Redis cluster集群模式三种。单机的 Redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读,所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。

 

 

7、Redis的 主从复制原理/ 同步机制 了解么? 

  • 全量复制        

        一个 master node 是可以配置多个 slave node 的,形成主从架构。全量复制是对 Redis 中的数据执行周期性的持久化。一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份生成RDB文件。缺点是如果宕机会丢失最近一次快照到当前写入的数据。

        主从复制主要过程是,启动一个slave以后会发送psync命令给master,master服务器fork()一个子进程去生成RDB快照文件,并且用缓冲区记录此后的写操作,然后RDB快照文件发送给slave进行写入,最后再把缓冲区中的写命令也发给slave来写入。

数据传输的时候断网了或者服务器挂了怎么办啊?
传输过程中有什么网络问题啥的,会自动重连的,并且连接之后会把缺少的数据补上的。

  • 增量复制

        增量复制是指Slave初始化后开始正常工作时master发生的写操作同步到slave的过程。增量数据通过AOF日志文件同步即可,有点类似数据库的binlog。AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中。

slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。

 

过期 key 处理

slave 不会过期 key,只会等待 master 过期 key。如果 master 过期了一个 key,或者通过 LRU 淘
汰了一个 key,那么会模拟一条 del 命令发送给 slave。
 

8、Redis哨兵(sentinel)机制

        主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。哨兵+Redis主从只能保证高可用,不能保证数据0丢失。

        哨兵服务器相当于注册中心,redis服务器上各自存在一个Sentinel,监控本机redis的运行情况,。先从注册中心获取redis master 服务地址,然后再发起链接。当master宕机 哨兵会进行投票决定master是否真正死亡,然后选举最健康的slave作为新的master,然后客户端再次发起新的链接。哨兵服务器主要有以下功能:

  • 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

哨兵有两个作用

①通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
②当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。多个哨兵之间通过发布订阅模式pub/sub,

故障切换(failover)的过程

①每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个PING命令。
②假设主服务器宕机,距离最后一次有效回复PING命令的时间超过所设定的值,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,会被判定为“主观下线”。
③当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时就会标记为“客观下线”,进行failover操作。然后发布订阅模式,把从服务器切换为主服务器。

3个哨兵和1主2从的Redis服务器示例

 

Redis 哨兵主备切换的数据丢失问题 导致数据丢失的两种情况

  • 主备切换的过程,可能会导致数据丢失:

        异步复制导致的数据丢失。因为 master->slave 的复制是异步的,所以可能有部分数据还没复制到 slave,master 就宕机了,此时这部分数据就丢失了

  • 脑裂导致的数据丢失

        脑裂,也就是说,某个 master 所在机器突然脱离了正常的网络,跟其他 slave 机器不能连接,但是实际上 master 还运行着。此时哨兵可能就会认为 master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时候,集群里就会有两个 master ,也就是所谓的脑裂。
此时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 写数据。因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master上去,自己的数据会清空,重新从新的 master 复制数据。而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。

数据丢失问题的解决方案

进行如下配置:

min-slaves-to-write 1 
min-slaves-max-lag 10

这个配置表示:要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。
如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。

  • 减少异步复制数据的丢失

        有了 min-slaves-max-lag 这个配置,就可以确保说,一旦 slave 复制数据和 ack 延时太长,就认为可能 master 宕机后损失的数据太多了,那么就拒绝写请求,这样可以把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低的可控范围内。

  • 减少脑裂的数据丢失

        如果一个 master 出现了脑裂,跟其他 slave 丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的 slave 发送数据,而且 slave 超过 10 秒没有给自己 ack 消息,那么就直接拒绝客户端的写请求。因此在脑裂场景下,最多就丢失 10 秒的数据。

9、Redis cluster

        Redis cluster,集群模式,主要是针对海量数据+高并发+高可用的场景。Redis cluster 支撑 N 个 Redis master node,每个 master node 都可以挂载多个 slave node。类似于多个Redis主从放一起。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个master 节点就能存放更多的数据了。可以自动将数据进行分片,每个 master 上放一部分数据。提供内置的高可用支持,部分 master 不可用时,还是可以继续工作。

        在 Redis cluster 架构下,每个 Redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w的端口号,比如 16379。16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议, gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

节点间的内部通信机制 基本通信原理

集群元数据的维护有两种方式:
集中式:将集群元数据(节点信息、故障等等)集中存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的 storm 。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper(分布式协调的中间件)对所有元数据进行存储维护。


 

gossip 协议:Redis 维护集群元数据的方式。所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据变更。

分布式寻址算法

  • hash 算法(大量缓存重建)
  • 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
  • Redis cluster 的 hash slot 算法

hash 算法:

来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦
某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去
取数据。这会导致大部分的请求过来,全部无法拿到有效的缓存,导致大量的流量涌入数据
库。

一致性 hash 算法:

一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。
来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。
然而,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。

 

Redis cluster 的 hash slot 算法:

Redis cluster 有固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384取模,可以获取 key 对应的 hash slot。 Redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个hash slot,通过 hash tag 来实现。任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。

Redis cluster 的高可用与主备切换原理

Redis cluster 的高可用的原理,几乎跟哨兵是类似的。

10、如何保证缓存数据库双写一致性?

1、严格一致性: 读请求和写请求串行化,串到一个内存队列里去。如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案。

2、Cache Aside Pattern(缓存+数据库读写的模式)

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,返回响应。
  • 更新的时候,先更新数据库,然后再删除缓存。

为什么是删除而不是更新?

因为很多情况下缓存的值是需要进行处理或者多表查询才有结果的,但很可能并不会被频繁访问,所以采取用到缓存才计算缓存的方式。

11、缓存不一致的问题:

缓存不一致问题及解决方案

场景一:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓
存中是旧数据,数据就出现了不一致。
解决思路:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓
存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,
然后更新到缓存中。

场景二:

数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了。
为什么上亿流量高并发场景下,缓存会出现这个问题?
只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。 
解决方案如下:

先删除缓存,更新数据库,预计数据库更新需要时间t,然后在提交数据库更新t时间后再次删除缓存

12、Redis 的并发竞争问题

场景一:多客户端同时获取一个 key修改值之后再写回去,例如有多个请求一起去对某个商品减库存,假设当前库存值为 20,现在有2个连接都要减 5,结果库存值应该是 10 才对,但存在下面这种情况:

场景二
比如有3个请求有序的修改某个key,按正常顺序的话,数据版本应该是 1->2->3,最后应该是 3。
但如果第二个请求由于网络原因迟到了,数据版本就变为了1->3->2,最后值为 2,出问题了。

解决方法:
①可以基于 zookeeper 实现分布式锁。每个系统通过 zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 key,别人都不允许读和写。

 
②写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。

适合有序需求场景,例如 A 需要把 key 设置为 a,然后 B 设置为 b,C 再设置为 c,最后的值应该是 c。这时就可以考虑使用时间戳的方式:

A => set key1 {a 11:01}
B => set key1 {b 11:02}
C => set key1 {c 11:03}

        就是在写入时保存一个时间戳,写入前先比较自己的时间戳是不是早于现有记录的时间戳,如果早于,就不写入了。假设 B 先执行了,key1 的值为 {b 11:02},当A执行时,发现自己的时间戳11:01早于现有值,就不执行 set 操作了。

13、生产环境中的 Redis 是怎么部署的?(参考)

        如果是Redis cluster模式,10 台机器,5 台机器部署了 Redis 主实例,另外 5 台机器部署了 Redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰 QPS 可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求每秒。

机器是什么配置?
        32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 Redis 进程的是 10g 内存,一般线上生产环境,Redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。5 台机器对外提供读写,一共有 50g 内存。因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,Redis 从实例会自动变成主实例继续提供读写服务。

你往内存里写的是什么数据?
        每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。

14、分布式锁

  • 普通实现

        说到Redis分布式锁大部分人都会想到:setnx+lua,或者说set key value px milliseconds nx

        主要分为两步setnx加锁并设置过期时间、释放锁时先验证当前锁的value和我们的是否一致,一致的话del锁。(既然有del锁为什么还要设置过期时间?主要是为了防止当前线程不释放锁造成死锁)

- 获取锁(unique_value可以是UUID等)
SET resource_name unique_value NX PX 30000
- 释放锁,以下为 lua 脚本,一定要比较value,防止误解锁
-- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

 这种实现方式有3大要点(也是面试概率非常高的地方):

  1. set命令要用set key value px milliseconds nx

  2. value要具有唯一性;

  3. 释放锁时要验证value值,不能误解锁;

  4. key用 random_value 随机值。因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,比如说超过了 30s,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除 key 的话会有问题,所以得用随机值加上面的 lua 脚本来释放锁。

事实上这类琐最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况

  1. 在Redis的master节点上拿到了锁;

  2. 但是这个加锁的key还没有同步到slave节点;

  3. master故障,发生故障转移,slave节点升级为master节点;

  4. 导致锁丢失。

正因为如此,Redis作者antirez基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock。笔者认为,Redlock也是Redis所有分布式锁实现方式中唯一能让面试官高潮的方式。

一般redis的分布式锁都可以使用redisson框架来做。 使用:直接lock("key")

原理:当lock的时候,当前客户端会生成一串lua脚本发送到redis服务端。服务端根据这个key是否存在以及key的value状态判断是否已被加锁。
如果加锁成功(有效期30s):生成的value里面带有当前客户端的id,实现可重入效果。然后在这个线程执行过程中,一旦加锁成功还会有一个watch dog每10s去刷新锁的状态。直到释放或者宕机,如果当前线程死锁或阻塞,那么这个锁就一起死锁。如果加锁失败:
自旋到获取锁。

以上普通实现的redisson版本。

        需要注意的是在redisson中会有watch doh看门狗机制(适用于setnx,redlock等等),当我们没有设置过期时间时,看门狗会默认30s过期,并且每隔10s(设置的过期时间的1/3)去查询一次是否已经过期,没有的话重置为30s,给这个锁“续命”。直到业务完成后主动.unlock()。如果我们设置了超时时间但redis挂掉了,只能等待超时时间之后才能释放锁;

// 构造redisson实现分布式锁必要的Config
Config config = new Config();
config.useSingleServer().setAddress("redis://172.29.1.180:5379").setPassword("a123456").setDatabase(0);
// 构造RedissonClient
RedissonClient redissonClient = Redisson.create(config);
// 设置锁定资源名称
RLock disLock = redissonClient.getLock("DISLOCK");
boolean isLock;
try {
    //尝试获取分布式锁
    isLock = disLock.tryLock(500, 15000, TimeUnit.MILLISECONDS);
    if (isLock) {
        //TODO if get lock success, do something;
        Thread.sleep(15000);
    }
} catch (Exception e) {
} finally {
    // 无论如何, 最后都要解锁
    disLock.unlock();
}

单机、哨兵、集群模式的config不一样,具体看这篇 

  • Zookeeper分布式锁

        zk 分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。因为创建的是临时 znode,如果客户端挂了,znode 就没了,此时就自动释放锁。

  • 这两种分布式锁如何选型?
  1. redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
  2. zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小
  3. 公司没有配套的Zookeeper集群,就直接用现有的redis,节约资源

  • 分布式锁

Redis如何解决锁丢失问题?
通过RedLock 算法来实现,Redlock分布式锁的实现完全基于普通分布式锁。这个场景是假设有一个 redis cluster,有 5 个 redis master 实例。然后执行如下步骤获取一把锁:
1. 获取当前时间戳,单位是毫秒;
2. 跟上面类似,轮流尝试在每个 master 节点上创建锁,过期时间较短,一般就几十毫秒;
3. 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点 n / 2 + 1;
4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
5. 要是锁建立失败了,那么就依次之前建立过的锁删除;
6. 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。

Config config1 = new Config();
config1.useSingleServer().setAddress("redis://172.29.1.180:5378")
        .setPassword("a123456").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://172.29.1.180:5379")
        .setPassword("a123456").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://172.29.1.180:5380")
        .setPassword("a123456").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);

String resourceName = "REDLOCK";
RLock lock1 = redissonClient1.getLock(resourceName);
RLock lock2 = redissonClient2.getLock(resourceName);
RLock lock3 = redissonClient3.getLock(resourceName);

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
    isLock = redLock.tryLock(500, 30000, TimeUnit.MILLISECONDS);
    System.out.println("isLock = "+isLock);
    if (isLock) {
        //TODO if get lock success, do something;
        Thread.sleep(30000);
    }
} catch (Exception e) {
} finally {
    // 无论如何, 最后都要解锁
    System.out.println("");
    redLock.unlock();
}

最核心的变化就是RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);,因为我这里是以三个节点为例。
那么如果是哨兵模式呢?需要搭建3个,或者5个sentinel模式集群(具体多少个,取决于你)。
那么如果是集群模式呢?需要搭建3个,或者5个cluster模式集群(具体多少个,取决于你)。

redlock算法:

在Redis的分布式环境中,我们假设有5个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。

1、为了取到锁,客户端应该执行以下操作:
获取当前Unix时间,以毫秒为单位。

2、依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。

3、客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。

4、如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。

5、如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

zookeeper or redis?就性能而言,redis很明显优于zookeeper;就分布式锁实现的健壮性而言,zookeeper很明显优于redis。如何选择,取决于你的业务。



分布式锁之Redis实现 - 简书

Redisson实现Redis分布式锁的N种姿势 - 简书

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值