写点什么

Redis 浅析(二)

作者:andy
  • 2022-10-29
    北京
  • 本文字数:11514 字

    阅读完需:约 38 分钟

1-分布式锁


应对并发问题的两种解决办法

(1)原子操作

(2)加锁的方式,控制并发操作对共享数据的修改

单机上的锁

单机上运行的多线程,锁通常使用一个变量表示

(1)变量值为 0 时,表示没有线程获取锁;

(2)变量值为 1 时,表示已经有线程获取到锁了。

枷锁和释放锁的代码模板


acquire_lock(){if(lock == 0){lock = 1;return 1;}else {return 0;}}
release_lock(){lock = 0;return 1;}
复制代码


分布式锁

在分布式系统中,多个客户端抢占获取分布式锁,锁是保存在一个共享存储系统中,被多个客户端共享访问和获取

原理与单机上的锁相同,同样需要一个变量来实现锁,关键点在于分布式场景下,锁变量由一个共享的存储系统维护

实现分布式锁的要求

(1)枷锁和释放锁的操作中,要保证锁的原子性(所有操作要么同时成功,要么同时失败)

(2)共享存储系统保存所变量,要保证共享存储系统的可用性,避免宕机导致锁无法使用

基于单个节点实现分布式锁

作为分布式锁实现过程中的共享存储系统,Redis 可以使用键值对来保存锁变量,再接收和处理不同客户端发送的加锁和释放锁的操作请求

加锁


释放锁


伪代码

// 加锁

SETNX lock_key 1

// 业务逻辑

DO THINGS

// 释放锁

DEL lock_key

风险一:

当客户端获取锁时,执行逻辑处理时出现异常,并未释放锁,导致其他客户端只能等待,影响业务流转

解决方法:设置过期时间,Expire key

风险二:

线程 A 获取锁,线程 B 执行 del 释放锁,导致锁失效

解决方法:区分不同的客户端来源


结合以上方案,可以两种方法一起使用,同时,SETNX 无法保证之后 Expire 设置是否成功,建议使用以下方式实现

// 加锁, unique_value 作为客户端唯一性的标识

SET lock_key unique_value NX PX 10000

//释放锁 比较 unique_value 是否相等,避免误释放

if redis.call("get",KEYS[1]) == ARGV[1] then

return redis.call("del",KEYS[1])

else

return 0

end


基于多个 Redis 节点实现高可靠的分布式锁

为了避免 Redis 实例故障而导致的锁无法工作的问题,提出了分布式锁算法 RedLock

RedLock 算法的基本思路,让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,就认为客户端成功获得了锁,否则,加锁失败

RedLock 算法

第一步是,客户端获取当前时间

第二步是,客户端按顺序依次向 N 个 Redis 实例执行加锁操作

第三步是,一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时

客户端只有在满足下面的这两个条件时,才能认为是加锁成功。

条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁;

条件二:客户端获取锁的总耗时没有超过锁的有效时间。


2-持久化


Redis 应用场景

作为缓存中间件,将后端数据库中的数据存储在内存中,便于应用直接从内存中读取,提高响应速度

问题

一旦服务器宕机,内存中的数据将全部丢失

解决方案

方案一

可以从后端数据库读取数据,但存在以下问题:

1、需要频繁访问数据库,增大数据库的访问压力;

2、从慢速数据库中读取数据,性能肯定比不上从 Redis 读取,导致程序响应变慢


方案二

Redis 实现数据持久化,避免从后端数据库中恢复数据

Redis 持久化机制

(1)AOF(Append Only File)日志

在文件末尾追加写的操作,记录每次写的操作指令,当服务器重启之时,重新执行命令来恢复原始的数据,Redis 可以对 AOF 文件进行后台重写,使得文件容量不至于过大

如何实现

AOF 日志是写后日志,即 Redis 先执行命令,把数据写入内存,再记录日志

目的:

第一,写后日志,先让系统执行命令,只有命令执行成功,才会被记录到日志之中,避免记录错误的指令,如果先记录,再执行命令,很可能因为记录了错误的指令,使得数据恢复时,出现错误

第二,先执行后记录,不会阻塞当前的写操作



风险

先执行,后记录,但在记录之前宕机,导致数据丢失

先执行,后记录,不会阻塞当前操作,但会阻塞后续的指令操作,因为磁盘压力大,导致写入慢

本质:与 AOF 写回磁盘的时机有关

写回策略

写回策略,即 AOF 提供的配置项 appendfsync 值

Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘

Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘

No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘


(2)RDB(Redis DataBase)快照文件

内存快照,即记录下某一个时刻的内存数据,再以文件的形式写入磁盘中,即使服务器宕机,也能够恢复到最近时刻的数据

关键问题

给哪些内存数据做快照?(涉及快照的执行效率问题)

快照时数据能修改吗?(关系 Redis 是否被阻塞,能够同时正常处理请求)

给哪些内存数据做快照?

针对所有数据做快照,即全量快照

Redis 生成 RDB 文件的两种方式: save 和 bgsave

save:在主线程中执行,会导致阻塞

bgsave:创建子线程,专门用于生成 RDB 文件,防止主线程阻塞,RDB 文件生成的默认方式


快照时数据能修改吗?

为了保证数据完整性,只能允许读操作,不能修改执行快照的数据。但是,这样显示是不行的,会对业务服务处理造成巨大影响

Redis 借助操作系统提供的写时复制技术(Copy-On-Write,COW),在执行快照的同时,正常处理写操作



AOF 与 RDB 比较

当没有进行持久化时,存储的数据只会在服务器运行的时候存在;如果同时使用两种持久化方式,服务器默认读取 AOF 文件,这是因为 AOF 文件存储的数据相比 RDB 文件要完整一些

RDB 持久化的优点

1、RDB 保存某个时间点的数据,适合进行数据备份;

2、RDB 是一个紧凑的单一文件,方便传递到远端,适用于灾难恢复;

3、RDB 保存文件时,父进程 fork 出一个子进程,子进程全权负责数据持久化;

4、相比 AOF,恢复大数据集时,RDB 方式会更快。

RDB 持久化的缺点

1、当出现 redis 停止工作的情况,即使可以通过提高保存频率来防止数据丢失,但是仍然会丢失掉一段时间的数据信息;

2、当保存的数据集比较大时,父进程 fork 出的子进程进行处理时,是非常耗时的。


AOF 持久化的优点

1、AOF 通过使用不同的策略:无 fsync、每秒 fsync、每秒写的时候 fsync,当出现 redis 停止工作的情况,AOF 依然维持着较好的数据存储,丢失的数据非常非常少;

2、AOF 是一个追加的文件,不要写入 seek,即使存入的命令不完整,依然可以使用工具进行修复;

3、Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写,重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合;

4、AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。

AOF 持久化的缺点

1、对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积;

2、根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。

如何选择使用哪种持久化方式?

建议同时使用两种持久化方式。

单纯使用 RDB,会容易丢失一定时间段的数据;单纯使用 AOF,相比 RDB 而言,不适合进行数据备份,并且恢复数据的速度要慢很多。


注意

Redis 配置文件默认不打开 AOF 持久化方式。建议一开始使用就配置启动 AOF,避免存储一定数据之后,再打开使用该方式,造成 Redis 读取 AOF 空文件,使得原有存储数据丢失


8-管道


TCP 服务

Redis 是一种基于客户端-服务端模型以及请求/响应协议的 TCP 服务。

这意味着通常情况下一个请求会遵循以下步骤:

客户端向服务端发送一个查询请求,并监听 Socket 返回,通常是以阻塞模式,等待服务端响应

服务端处理命令,并将结果返回给客户端。

客户端和服务器通过网络进行连接。这个连接可以很快(loopback 接口)或很慢(建立了一个多次跳转的网络连接)。无论网络延如何延时,数据包总是能从客户端到达服务器,并从服务器返回数据回复客户端。这个时间被称之为 RTT (Round Trip Time - 往返时间)

当客户端需要在一个批处理中执行多次请求时很容易看到这是如何影响性能的(例如添加许多元素到同一个 list,或者用很多 Keys 填充数据库)。例如,如果 RTT 时间是 250 毫秒(在一个很慢的连接下),即使服务器每秒能处理 100k 的请求数,我们每秒最多也只能处理 4 个请求。

如果采用 loopback 接口,RTT 就短得多(比如我的主机 ping 127.0.0.1 只需要 44 毫秒),但在一次批量写入操作中它仍然是一笔很大的开销。

管道(Pipelining)

一次发送多个命令,节省往返时间。

一次请求/响应,服务器能实现处理新的请求,即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。

管道技术不用为每个命令都花费了 RTT 开销,而是只需要用了一个命令的开销时间即可。

重要说明:

使用管道发送命令时,服务器将被迫回复一个队列答复,占用很多内存。所以,如果你需要发送大量的命令,最好是把他们按照合理数量分批次的处理,例如 10K 的命令,读回复,然后再发送另一个 10k 的命令,等等。这样速度几乎是相同的,但是在回复这 10k 命令队列需要非常大量的内存用来组织返回数据内容。

Jedis 实现管道技术

privatestaticvoid usePipeline(int count){

Jedis jr = null;

try {

jr = new Jedis("10.10.224.44", 6379);

Pipeline pl = jr.pipelined();

for(int i =0; i pl.incr("testKey2"); } pl.sync(); } catch (Exception e) { e.printStackTrace(); } finally{ if(jr!=null){ jr.disconnect(); } } }


9-发布订阅


发布订阅

发布订阅(pub/sub)是一种消息通信信道:发送者发送消息,订阅者接收消息。

发布者发布的消息分到不同的频道,不需要知道什么样的订阅者订阅。订阅者对一个或多个频道感兴趣,进行订阅,只需接收感兴趣的消息,不需要知道是什么样的发布者发布的。这种发布者和订阅者的解耦合可以带来更大的扩展性和更加动态的网络拓扑。发送者(发布者)不是计划发送消息给特定的接收者(订阅者)。

PUBLISH

客户端通过该指令,创建频道,从而可以在频道上发布消息内容。

192.168.199.173:6379> publish foo nihao

(integer) 1

返回结果 1,表示接收消息的客户端个数。

SUBSCRIBE

为了订阅 foo 和 bar,客户端发出一个订阅的频道名称:

SUBSCRIBE foo bar

其他客户端发到这些频道的消息将会被推送到所有订阅的客户端。

客户端订阅到一个或多个频道不必发出命令,尽管他能订阅和取消订阅其他频道。订阅和取消订阅的响应被封装在发送的消息中,以便客户端只需要读一个连续的消息流,其中第一个元素表示消息类型。

192.168.199.173:6379> subscribe foo

Reading messages... (press Ctrl-C to quit)

1) "subscribe"

2) "foo"

3) (integer) 1

1) "message"

2) "foo"

3) "hello world"

推送消息的格式

消息是一个有三个元素的多块响应 。

第一个元素是消息类型:

subscribe: 表示成功订阅的频道。第三个参数代表我们现在订阅的频道的数量。

unsubscribe:表示成功取消订阅的频道。第三个参数代表当前订阅的频道的数量。当最后一个参数是 0 的时候,表示不再订阅到任何频道。当在 Pub/Sub 以外状态,客户端可以发出任何 redis 命令。

message: 这是另外一个客户端发出的发布命令的结果。第二个元素是来源频道的名称,第三个参数是实际消息的内容。

数据库与作用域

发布/订阅与 key 所在空间没有关系,它不会受任何级别的干扰,包括不同数据库编码。 发布在 db 10,订阅可以在 db 1。 如果你需要区分某些频道,可以通过在频道名称前面加上所在环境的名称(例如:测试环境,演示环境,线上环境等)。

模式匹配订阅

Redis 的 Pub/Sub 实现支持模式匹配。客户端可以订阅全风格的模式以便接收所有来自能匹配到给定模式的频道的消息。

比如:

PSUBSCRIBE news.*

将接收所有发到 news.art.figurative, news.music.jazz 等等的消息,所有模式都是有效的,所以支持多通配符。

PUNSUBSCRIBE news.*

将取消订阅匹配该模式的客户端,这个调用不影响其他订阅。

192.168.199.173:6379> psubscribe f*

Reading messages... (press Ctrl-C to quit)

1) "psubscribe"

2) "f*"

3) (integer) 1

1) "pmessage"

2) "f*"

3) "foo"

4) "nihao"

当作模式匹配结果的消息会以不同的格式发送:

消息类型是 pmessage:这是另一客户端发出的 PUBLISH 命令的结果,匹配一个模式匹配订阅。第二个元素是原匹配的模式,第三个元素是原频道名称,最后一个元素是实际消息内容。

同样的,系统默认 SUBSCRIBE 和 UNSUBSCRIBE,PSUBSCRIBE 和 PUNSUBSCRIBE 命令在发送 psubscribe 和 punsubscribe 类型的消息时使用像 subscribe 和 unsubscribe 一样的消息格式。

同时匹配模式和频道订阅的消息

客户端可能多次接收一个消息,如果它订阅的多个模式匹配了同一个发布的消息,或者它订阅的模式和频道同时匹配到一个消息。就像下面的例子:

SUBSCRIBE foo

PSUBSCRIBE f*

上面的例子中,如果一个消息被发送到 foo,客户端会接收到两条消息:一条 message 类型,一条 pmessage 类型。

模式匹配统计的意义

在 subscribe,unsubscribe,psubscribe 和 punsubscribe 消息类型中,最后一个参数是依然活跃的订阅数。 这个数字是客户端依然订阅的频道和模式的总数。只有当退订频道和模式的数量下降到 0 时客户端才会退出 Pub/Sub 状态。


12-分区


分区

分区就是将数据分配到多个 Redis 实例进行储存,每一个 Redis 实例保存的就是所有 key 的一个子集。

分区的优势

利用多台计算机的内存,构造更大的数据库。

利用多核和多台计算机,扩展计算能力。

利用多台计算机和网络适配器,扩展网络带宽。

分区的不足

分区的不足,主要是 Redis 的一些特性在分区的情况表现不是很好。

一、涉及多个 key 的操作是不被支持的。例如,不同的 Redis 实例子集无法求交集。

二、涉及多个 key 的事务不能使用,这也是分布式事务产生的原因。

三、当使用分区时,数据处理较为复杂。例如需要处理多个 rdb/aof 文件,并且从多个实例和主机备份持久化文件。

分区类型

通过不同的方式,针对给定的 key,会存储到不同的 Redis 实例上。

一、范围分区

按照范围分区,映射一定范围内的 key 到特定的 Redis 实例上存储。

例如,ID 在 0~10000 以内的存储在 R0,ID 在 10001~20000 以内的存储在 R1,以此类推。这样的方式,实际运行时可行的,但是,需要对映射范围的表进行维护管理,同时,动态扩展时,如何分配范围,这也是一个麻烦的问题。

二、哈希分区

通过对存储的 key 进行 hash 函数计算,得到一个整数值,然后再使用该整数值,对 Redis 集群个数取模,从而得到该 key 存储的实际 Redis 实例。

例如,假设 Redis 集群个数为 4,key 为 foobar 的数据,通过 crc32 函数计算,得到 93024922 的整数。然后,再将 93024922%4=2,最终,该 foobar 数据存储在索引为 2 的 Redis 实例上。


13-集群服务


Redis 集群

Redis 集群就是多个 Redis 服务器节点之间共享数据的程序集

Redis 集群并不支持处理多个 keys 的命令,例如交集,因为这需要在不同的节点间移动数据,达不到原有 Redis 那样的性能,在高负载的情况下可能会导致不可预料的错误。

Redis 集群通过分区提供一定程度的可用性,在实际环境中,当某个节点宕机或者不可达的情况下继续处理命令。

Redis 集群的优势

1、自动分割数据到不同的节点,也就是分区;

2、通过分区和主从模式来提供一定程度的可用性,整个集群的部分节点失败或者不可达的情况下能够继续处理命令

数据分片

Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念。

Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16(key) mod 16384 来决定放置哪个槽。集群的每个节点负责一部分 hash 槽

举个例子,比如当前集群有 3 个节点,那么:

节点 A 包含 0 到 5500 号哈希槽;

节点 B 包含 5501 到 11000 号哈希槽;

节点 C 包含 11001 到 16384 号哈希槽。

这种结构很容易添加或者删除节点。比如如果想新添加个节点 D,只需要从节点 A,B,C 中将部分槽分配到 D 上。如果想移除节点 A,需要将 A 中的槽移到 B 和 C 节点上,然后将没有任何槽的 A 节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。这样的过程是必须保证所有哈希槽都是一直存在着的,而没有出现节点宕机丢失的情况。

主从复制模式

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,集群使用了主从复制模型,每个节点都会有 N-1 个复制品

在数据分片的例子中具有 A,B,C 三个节点的集群,在没有复制模型的情况下,如果节点 B 失败了,那么整个集群就会因为缺少 5501-11000 范围的哈希槽而不可用。出现了槽不可用,集群就会停止服务。

如果在集群创建的时候(或者过一段时间),为每个节点添加一个从节点 A1,B1,C1,那么整个集群便由三个 master 节点和三个 slave 节点组成。这样在节点 B 失败后,集群便会选举 B1 为新的主节点继续服务,整个集群便不会因为槽找不到而不可用了。

不过当 B 和 B1 都失败后,集群就真的不可用的了。

一致性保证

Redis 并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。

第一个原因是因为集群用了异步复制。写操作过程:

客户端向主节点 B 写入一条命令;

主节点 B 向客户端回复命令状态;

主节点将写操作复制给他的从节点 B1、B2 和 B3。

主节点对命令的复制工作发生在返回命令回复之后,因为如果每次处理命令请求都需要等待复制操作完成的话,那么主节点处理命令请求的速度将极大地降低——我们必须在性能和一致性之间做出权衡。 注意:Redis 集群可能会在将来提供同步写的方法。

Redis 集群另外一种可能会丢失命令的情况是集群出现了网络分区,并且一个客户端与至少包括一个主节点在内的少数实例被孤立。

举个例子。设集群包含 A、B、C、A1、B1、C1 六个节点,其中 A、B、C 为主节点,A1、B1、C1 为 A,B,C 的从节点,还有一个客户端 Z1。假设集群中发生网络分区,那么集群可能会分为两方,大部分的一方包含节点 A、C、A1、B1 和 C1,小部分的一方则包含节点 B 和客户端 Z1。

Z1 仍然能够向主节点 B 中写入,如果网络分区发生时间较短,那么集群将会继续正常运作。如果分区的时间足够让大部分的一方将 B1 选举为新的 master,那么 Z1 写入 B 中得数据便丢失了。据估计,这样的情况下,B 无法提供集群服务,Z1 是无法使用 B 节点的。

注意,在网络分裂出现期间,客户端 Z1 可以向主节点 B 发送写命令的最大时间是有限制的,这一时间限制称为节点超时时间(node timeout),是 Redis 集群的一个重要的配置选项。


14-集群搭建与使用


前期准备

Redis 配置集群模式开启 Redis 集群模式实例,便可以使用集群特有的命令和特性

修改 redis.conf 配置文件,设置集群模式。

port 7000

cluster-enabled yes

cluster-config-file nodes.conf

cluster-node-timeout 5000

appendonly yes

文件中的 cluster-enabled 选项用于开实例的集群模式,而 cluster-conf-file 选项则设定了保存节点配置文件的路径,默认值为 nodes.conf,不同服务应当命名不同的文件。节点配置文件无须人为修改,它由 Redis 集群在启动时创建,并在有需要时自动进行更新。

要让集群正常运作至少需要三个主节点,不过在刚开始试用集群功能时,强烈建议使用六个节点:其中三个为主节点,而其余三个则是各个主节点的从节点。

首先,让我们进入一个新目录,并创建六个以端口号为名字的子目录,稍后我们在将每个目录中运行一个 Redis 实例。命令如下:

mkdir cluster-test

cd cluster-test

mkdir 7000 7001 7002 7003 7004 7005

在文件夹 7000 至 7005 中,各创建一个 redis.conf 文件,文件的内容可以使用上面的示例配置文件,但记得将配置中的端口号从 7000 改为与文件夹名字相同的号码。

从 Redis Github 页面的 unstable 分支中取出最新的 Redis 源码,编译出可执行文件 redis-server, 并将文件复制到 cluster-test 文件夹,然后使用类似以下命令,在每个标签页中打开一个实例:

cd 7000

../redis-server ./redis.conf

实例打印的日志显示,因为 nodes.conf 文件不存在,所以每个节点都为它自身指定了一个新的 ID:

[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1

实例会一直使用同一个 ID,从而在集群中保持一个独一无二(unique)的名字。

搭建集群

现在我们已经有了六个正在运行中的 Redis 实例,接下来我们需要使用这些实例来创建集群,并为每个节点编写配置文件。

通过使用 Redis 集群命令行工具 redis-trib,编写节点配置文件的工作可以非常容易地完成:redis-trib 位于 Redis 源码的 src 文件夹中,它是一个 Ruby 程序,这个程序通过向实例发送特殊命令来完成创建新集群,检查集群,或者对集群进行重新分片(reshared)等工作

./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

这个命令在这里用于创建一个新的集群,选项–replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。之后跟着的其他参数则是这个集群实例的地址列表,3 个 master,3 个 slave。redis-trib 会打印出一份预想中的配置给你看,如果你觉得没问题的话,就可以输入 yes,redis-trib 就会将这份配置应用到集群当中,让各个节点开始互相通讯,最后可以得到如下信息:

[OK] All 16384 slots covered

这表示集群中的 16384 个槽都有至少一个主节点在处理,集群运作正常。

使用 create-cluster 脚本创建 Redis 集群

不想通过手动配置和启动 Redis 实例来启动 Redis 集群服务,可以使用 utils/create-cluster 目录下的 create-cluster 脚本文件,这是一个 bash 脚本文件。使用该脚本,需要修改文件内容,便于正确使用 redis 节点服务。

开启 6 个节点,3 个主节点,3 个从节点,使用以下命令:

1、create-cluster start

2、create-cluster create

第二步服务发送是否接受集群布局时,回复 yes 即可。

现在可以与集群交互,第一个节点默认使用 30001 端口开启。

停止集群服务使用以下命令:

create-cluster stop

使用集群

使用客户端连接 Redis 集群服务,建议以下方式:

1、Redis unstable 分支中的 redis-cli 程序实现了非常基本的集群支持,可以使用命令 redis-cli -c 来启动;

2、使用最多的是 java 客户端,Jedis 添加了对集群的支持,详细请查看项目 README 中 Jedis Cluster 部分。

以下使用 redis-cli -c 进行演示。

$ redis-cli -c -p 7000

redis 127.0.0.1:7000> set foo bar

-> Redirected to slot [12182] located at 127.0.0.1:7002

OK

redis 127.0.0.1:7002>set hello world

-> Redirected to slot [866] located at 127.0.0.1:7000

OK

redis 127.0.0.1:7000> get foo

-> Redirected to slot [12182] located at 127.0.0.1:7002

"bar"

redis 127.0.0.1:7000> get hello

-> Redirected to slot [866] located at 127.0.0.1:7000

"world"

redis-cli 对集群的支持是非常基本的,所以它总是依靠 Redis 集群节点来将它转向(redirect)至正确的节点。

一个真正的(serious)集群客户端应该做得比这更好:

它应该用缓存记录起哈希槽与节点地址之间的映射(map),从而直接将命令发送到正确的节点上面。这种映射只会在集群的配置出现某些修改时变化,比如说,在一次故障转移(failover)之后,或者系统管理员通过添加节点或移除节点来修改了集群的布局(layout)之后,诸如此类。

集群重新分片

重新分片并不会对正在运行的集群程序产生任何影响,重新分片操作基本上就是将某些节点上的哈希槽移动到另外一些节点上面,和创建集群一样,重新分片也可以使用 redis-trib 程序来执行。执行以下命令可以开始一次重新分片操作:

./redis-trib.rb reshard 127.0.0.1:7000

你只需要指定集群中其中一个节点的地址,redis-trib 就会自动找到集群中的其他节点。

目前 redis-trib 只能在管理员的协助下完成重新分片的工作,要让 redis-trib 自动将哈希槽从一个节点移动到另一个节点,目前来说还做不到。

尝试从集群中将 1000 个槽重新分片,除了需要移动的哈希槽数量之外,redis-trib 还需要知道重新分片的目标,也即是,负责接收这 1000 个哈希槽的节点。

$ redis-cli -p 7000 cluster nodes | grep myself

97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460

目标节点是 97a3a64667477371c4479320d683e4c8db5858b1。

现在需要指定从哪些节点来移动 keys 到目标节点。输入的是 all,这样就会从其他每个 master 上取一些哈希槽。

最后确认后会看到每个 redis-trib 移动的槽的信息,每个 key 的移动的信息也会打印出来。在重新分片的过程中,你的客户端程序是不会受到影响的,可以停止或者重新启动多次。

在重新分片结束后你可以通过如下命令检查集群状态:

./redis-trib.rb check 127.0.0.1:7000

手动故障转移

有的时候在主节点没有任何问题的情况下强制手动故障转移也是很有必要的,比如想要升级主节点的 Redis 进程,我们可以通过故障转移将其转为 slave 再进行升级操作来避免对集群的可用性造成很大的影响。

Redis 集群使用 CLUSTER FAILOVER 命令来进行故障转移,不过要在被转移的主节点的从节点上执行该命令。手动故障转移比主节点失败自动故障转移更加安全,因为手动故障转移时客户端的切换是在确保新的主节点完全复制了失败的旧的主节点数据的前提下发生的,所以避免了数据的丢失。

其基本过程如下:

客户端不再链接我们淘汰的主节点,同时主节点向从节点发送复制偏移量。从节点得到复制偏移量后故障转移开始,接着通知主节点进行配置切换,当客户端在旧的 master 上解锁后重新连接到新的主节点上。

添加一个新节点

添加新的节点的基本过程就是添加一个空的节点,然后移动一些数据传给它,有两种情况:添加一个主节点和添加一个从节点(添加从节点时需要将这个新的节点设置为集群中某个节点的复制)。

两种情况第一步都是要添加一个空的节点。

启动新的 7006 节点,使用的配置文件和以前的一样,只要把端口号改一下即可,过程如前。

接下来使用 redis-trib 来添加这个节点到现有的集群中去。

./redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000

可以看到,使用 add-node 命令来添加节点,第一个参数是新节点的地址,第二个参数是任意一个已经存在的节点的 IP 和端口。然后进入集群客户端查看所有节点:cluster nodes。

新节点现在已经连接上了集群,成为集群的一份子,并且可以对客户端的命令请求进行转向了, 但是和其他主节点相比, 新节点还有两点区别:

1、新节点没有包含任何数据, 因为它没有包含任何哈希槽;

2、尽管新节点没有包含任何哈希槽,但它仍然是一个主节点,所以在集群需要将某个从节点升级为新的主节点时,这个新节点不会被选中。

接下来,只要使用 redis-trib 程序,将集群中的某些哈希桶移动到新节点里面,新节点就会成为真正的主节点了。使用重新分片指令 reshard。

添加一个从节点

有两种方法添加从节点,可以像添加主节点一样使用 redis-trib 命令,然后附加-slave 选项:

./redis-trib.rb add-node --slave 127.0.0.1:7006 127.0.0.1:7000

此处的命令和添加一个主节点命令类似,此处并没有指定添加的这个从节点的主节点,这种情况下系统会在其他的复制集中的主节点中随机选取一个作为这个从节点的主节点。

也可以使用 CLUSTER REPLICATE 命令添加,这个命令也可以改变一个从节点的主节点。

例如,要给主节点 127.0.0.1:7005 添加一个从节点,该节点哈希槽的范围 1423-16383, 节点 ID 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e,我们需要链接新的节点(已经是空的主节点)并执行命令:

redis 127.0.0.1:7006>cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

我们新的从节点有了一些哈希槽,其他的节点也知道(过几秒后会更新他们自己的配置),可以使用如下命令确认:

$ redis-cli -p 7000 cluster nodes | grep slave | grep 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

f093c80dde814da99c5cf72a7dd01590792b783b 127.0.0.1:7006 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617702 3 connected

2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617198 3 connected

移除一个节点

只要使用 del-node 命令即可:

./redis-trib del-node 127.0.0.1:7000 ``

第一个参数是任意一个节点的地址,第二个节点是你想要移除的节点地址。

使用同样的方法移除主节点,不过在移除主节点前,需要确保这个主节点是空的. 如果不是空的,需要将这个节点的数据重新分片到其他主节点上。

替代移除主节点的方法是手动执行故障恢复,被移除的主节点会作为一个从节点存在,不过这种情况下不会减少集群节点的数量,也需要重新分片数据。

从节点的迁移

在 Redis 集群中会存在改变一个从节点的主节点的情况,需要执行如下命令:

CLUSTER REPLICATE

在特定的场景下,不需要系统管理员的协助下,自动将一个从节点从当前的主节点切换到另一个主节点的自动重新配置的过程叫做复制迁移(从节点迁移),从节点的迁移能够提高整个 Redis 集群的可用性。

简短的概况一下从节点迁移:

集群会在有从节点数量最多的主节点上进行从节点的迁移;

要在一个主节点上添加多个从节点;

参数来控制从节点迁移 replica-migration-barrier。


用户头像

andy

关注

还未添加个人签名 2019-11-21 加入

还未添加个人简介

评论

发布
暂无评论
Redis浅析(二)_andy_InfoQ写作社区