第 17 章 集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移的功能
一、命令
CLUSTER MEET命令:用来连接不同的开启集群支持的 Redis 节点,以进入工作集群。
CLUSTER MEET <ip> <port>
CLUSTER NODES命令:提供了当前连接节点所属集群的配置信息。
CLUSTER NODES
CLUSTER ADDSLOTS命令:把一组hash slots分配给接收命令的节点。
CLUSTER ADDSLOTS slot [slot ...]
MOVED错误 :当前节点(服务器)发现键所在的槽不是由自己负责,节点就会向客户端返回一个Moved错误,指引客户端转向正确节点
MOVED <slot> <host>:<ip>
ASK错误:在重新分片期间,客户端请求源节点槽的数据,发现数据不在源节点,数据已经迁移到目标节点时,源节点会向客户端返回一个ASK错误,指引客户端转向正确节点
ASK <slot> <host>:<ip>
CLUSTER REPLICATE命令:向一个节点发送该命令,让接受命令的节点称为node_id所指定节点的从节点,并开始对主节点进行复制:
CLUSTER REPLICATE <node_id>
二、节点
1. 集群如何添加节点:
- 一开始,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,需要将各个独立的节点连接起来,构成一个包含多个节点的集群。
- 向一个节点发送CLUSTER MEET命令,可以让节点与指定ip和port的节点进行握手(handshake),握手成功时,节点会把指定ip和port的节点添加到当前所在集群中。
2. 启动节点:
3. 集群数据结构:
- ClusterNode结构保存了一个节点的当前状态,具体信息如下。ClusterNode结构的link属性是一个ClusterLink结构,该结构保存了链接节点的所有信息,具体如下:
- 每个节点都保存着一个clusterState结构,这个结构记录了当前节点的视角下,集群目前所处的状态,具体如下:
4. 握手过程:
三、槽指派
Redis集群通过分片来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),集群中每个节点可以处于0~16384个槽位。
当数据库中的16348个槽位都有节点在处理时,集群处于上线状态(ok),相反地,如果数据库中有任何一个槽位没有得到处理,那么集群处于下线状态(fail)。
1. 记录节点的槽位指派信息:
2. 传播节点的槽指派信息:
一个节点(A节点)除了处理记录自己负责的槽记录,还会将自己的槽记录(A节点)通过消息发送给集群中的其他节点(B节点),其他节点(B节点)会在自己的clusterState.nodes字典中查找节点(A节点)对应的clusterNode结构,更新槽记录。
3. 记录集群所有槽的指派信息:
4. clusterState. slots数组与clusterNode.slots数组区别:
- clusterState. slots记录了集群中所有槽的指派信息。
- clusterNode.slots数组只记录了clusterNode结构所代表的节点的槽指派信息。
四、在集群中执行命令
1. 流程图:
2. 计算键属于哪个槽位:
3. 判断槽是否有当前节点负责处理:
计算出来键所属的槽 i 之后,节点就会检查自己在cluster.slots数组中的项i,判断键所在的槽是否由自己负责:
- 如果clusterState.slots[i]等于clusterState.myself,那么说明槽i由当前节点负责,节点可以执行客户端发送的命令。
- 如果clusterState.slots[i]不等于clusterState.myself,结构会根据clusterState.slots[i]指向的clusterNode结构记录的节点IP和端口号,向客户端返回MOVED错误,指引客户端专转向正在处理槽 i 的节点
4. MOVED错误:
5. 节点数据库的实现:
集群节点保存键值对以及过期时间的方式,与单价Redis服务器完全相同。
五、重新分片
1. 概念:
重启分片可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。
2. 实现原理:
Redis集群的从新分片操作由Redis的集群管理软件redis-trib负责执行的,Redis提供进行重新分片的所需命令,而redis-trib则通过源节点和目标节点发送命令来进行重新分片操作。
六、ASK错误
1. 概念:
在重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:
- 源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令。
- 相反地,如果源节点没能在自己的数据库里面找到指定的键,那么这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。
2. 流程:
3. AKSING命令:
- 作用:打开发送该命令的客户端的Redis_Asking标识。
4. ASK错误和MOVED错误的区别:
- MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点。
- ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:
- 在客户端收到关于槽 i 的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽 i 的命令请求发送至ASK错误所指示的节点,因为可能该槽位还有没迁移完的元素,所以只有全部迁移完之后才会把这个槽位标记成 target 节点的。
- 所以这种转向不会对客户端今后发送关于槽 i 的命令请求产生任何影响,客户端仍然会将关于槽i的命令请求发送至目前负责处理槽i的节点,除非ASK错误再次出现。
七、复制与故障转移
1. 相关概念:
- Redis集群中的节点为主节点(master)和从节点(slave),其中主节点处理槽,而从节点则用于复制某个节点,并在复制的主节点下线时,代替下线主节点继续处理命令请求。
- 集群中的所有节点都会在代表主节点的clusterNode结构的slave属性和numslave属性中记录正在复制这个主节点的从节点名单。
2. 故障检测:
- 集群中的每个节点都会定期地向集群中的其他节点发送PING消息,检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线(probable fail, PFAIL)。
- 集群中各个节点会通过相互发送消息的方式来交换集群中各个节点的状态,如果半数以上复制处理槽的主节点都将某个主节点报告为疑似下线,那么这个主节点将被标记为已下线。
3. 故障转移:
- 1) 下线主节点的所有从节点里面,会有一个从节点被选中。
- 2) 被选中的从节点会执行 SLAVEOF no one命令,成为新的主节点。
- 3) 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
- 4) 新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负处理的槽。
- 5) 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。
4. 选举新的主节点:
基于Raft算法的领头选举方法(leader election)实现的,具体见P281
八、消息
1. 节点发送消息的种类(5种):
- MEET消息:当发送者接到客户端发送的 CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群里面。
- PING消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,那么节点A也会向节点B发PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后。
- PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET消息或者PING消息已到达,接收者会向发送者返回一条PONG消息。另外,一个节点也可以通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新关于这个节点的认识。例如当一次故障转移操作成功执行之后,新的主节点会向集群广播一条PONG消息,以此来让集群中的其他节点立即知道这个节点已经变成了主节点,并且接管了已下线节点负责的槽。
- FAIL消息:当一个主节点A判断另一个主节点B已经进入FAIL状态时,节点A会向集群广播一条关于节点B的FAIL消息,所有收到这条消息的节点都会立即将节点B标记为已下线。
- PUBLISH消息:当节点接收到一个 PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条 PUBLISH消息的节点都会执行相同的 PUBLISH命令。
2. 消息的组成(两部分):消息头和消息正文
3. 消息头结构:
4. 消息正文结构:
5. MEET、PING、PONG消息的实现
- Redis集群中各个节点通过Gossip协议来交互各自关于不同节点的状态信息,其中Gossip协议由MEET、PING、PONG三种消息实现,这三种消息的正文都由两个cluster.h/clusterMsgDataGossip机构组成,其他资料见P285
6. FAIL消息的实现:
在集群的节点数量比较大的情况下,单纯使用Gossip协议来传播节点的下线信息会有些延迟。FALL消息的正文由cluster.h/clusterMsgDataFail结构表示,这个机构只包括弄得那么属性:
7. PUBLISH消息的实现:
当客户端向集群中某个节点发送命令的时候,接受命令的节点不仅向channel频道发送消息message,它还会向集群广播一条publish消息,所以接收到这条publish消息的节点都会向channel频道发送message消息。
九、重点回顾
- 节点通过握手来将其他节点添加到自己所处的集群当中。
- 集群中的16384个槽可以分别指派给集群中的各个节点,每个节点都会记录哪些槽指派给了自己,而哪些槽又被指派给了其他节点。
- 节点在接到一个命令请求时,会先检查这个命令请求要处理的键所在的槽是否由自己负责,如果不是的话,节点将向客户端返回一个 MOVED错误,MOVED错误携带的信息可以指引客户端转向至正在负责相关槽的节点。
- 对 Redis集群的重新分片工作是由redis-trib负责执行的,重新分片的关键是将属于某个槽的所有键值对从一个节点转移至另一个节点。
- 如果节点A正在迁移槽 i 至节点B,当节点A没能在自己的数据库中找到命令指定的数据库键时,节点A会向客户端返回一个ASK错误,指引客户端到节点B继续查找指定的数据库键。
- MOVED错误表示槽的负责权已经从一个节点转移到了另一个节点,而ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施。
- 集群里的从节点用于复制主节点,并在主节点下线时,代替主节点继续处理命令请求。
- 集群中的节点通过发送和接收消息来进行通信,常见的消息包括MEET、PING、PONG、FAIL、 PUBLISH五种。
第 18 章 发布与订阅
一、命令
Publish命令:用于将信息发送到指定的频道
PUBLISH channel_name message
Subscribe命令:用于客户端订阅给定的一个或多个频道的信息
SUBSCRIBE channel_name [channel_name ...]
Unsubscribe命令: 用于客户端退订给定的一个或多个频道的信息
UNSUBSCRIBE channel_name [channel_name ...]
Psubscribe命令:用于客户端订阅一个或多个符合给定模式的频道
PSUBSCRIBE pattern_name [pattern_name ...]
Punsubscribe命令: 用于客户端退订所有给定模式的频道
PUNSUBSCRIBE pattern_name [pattern_name ...]
Pubsub命令:用于查看订阅与发布系统状态,它由数个不同格式的子命令组成,详情见《五、查看订阅信息》
PUBSUB <subcommand> [argument [argument ...]]
二、频道的订阅与退订
1. 频道的存储位置与数据结构:
Redis将所有频道的订阅关系都保存在服务状态的pubsub_channels字典里面,这个字典的键存储的是频道,值是一个链表,链表里记录了所有订阅这个频道的客户端。
2. 频道订阅步骤:
- 如果频道已经有其他订阅者,将客户端添加到订阅者链表的末尾。
- 如果频道没有任何订阅者,首先为频道创建一个键,并将这个键的值设置为空链表,然后将客户端添加到链表,成为链表的第一个元素。
3. 频道退订步骤:
- 根据退订频道的名字,从字典中找到该频道对应订阅链表,再从链表中删除。
- 如果删除订阅客户端后,链表为空,则从字典中删除频道对应的键。
三、模式的订阅与退订
1. 模式的存储位置与数据结构:
Redis将所有模式的订阅关系都保存在服务器状态的pubsub_patterns属性里面,pubsub_patterns属性是一个链表,链表中包含一个pubsubPattern结构。
2. 模式的订阅步骤:
- 1)新建一个pubsubPattern结构,将结构的pattern属性设置为被订阅的模式,client属性设置为订阅模式的客户端。
- 2)将pubsubPattern结构添加到pubsub_patterns链表的表尾。
3. 模式的退订步骤:
- 从链表中查找并删除节点。
四、消息发送
服务器执行消息发送的两个动作:
- 将消息发送给频道的所有订阅者。
- 如果有一个或多个模式与频道匹配,将消息发送给模式的订阅者。
五、查看订阅信息
1. Pubsub命令:查看频道或模式相关信息。
2. Pubsub Channels子命令:
PUBSUB CHANNLES [pattern]
- 作用:返回服务器当前被订阅的频道,其中pattern参数时可选的:
- 如果不给定pattern参数,那么命令返回服务器当前被订阅的所有频道。
- 如果给定pattern参数,那么命令返回服务器当前被订阅的频道中那些与pattern模式相匹配的频道。
3. Pubsub Numsub子命令:
PUBSUB NUMSUB [channel-1 channel-2 ... channel-n]
- 作用:接受任意多个频道作为输入参数,并返回这些频道的订阅者数量。
3. Pubsub Numpat子命令:
PUBSUB NUMPAT
- 作用:返回服务器当前被订阅,模式的数量。
六、重点回顾
- 服务器状态在 pubsub channels字典保存了所有频道的订阅关系:SUBSCRIBE命令负责将客户端和被订阅的频道关联到这个字典里面,而 UNSUBSCRIBE命令则负责解除客户端和被退订频道之间的关联。
- 服务器状态在pubsub patterns链表保存了所有模式的订阅关系:PSUBSCRIBE命令负责将客户端和被订阅的模式记录到这个链表中,而 PUNSUBSCRIBE命令则负责移除客户端和被退订模式在链表中的记录。
- PUBLISH命令通过访问 pubsub channels字典来向频道的所有订阅者发送消息,通过访问pubsub patterns链表来向所有匹配频道的模式的订阅者发送消息。
- PUBSUB命令的三个子命令都是通过读取 pubsub channels字典和 pubsub patterns链表中的信息来实现的。
第 19 章 事务
事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改变去执行其他客户端的命令请求,它会将事务中所有命令都执行完毕,然后才去处理其他客户端的命令请求。
一、命令
Multi命令:用于标记一个事务块的开始。
Multi
Exec命令:用于执行所有事务块内的命令。
Exec
Watch命令:用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
WATCH key [key ...]
二、事务的实现
1. 事务的阶段:
事务开始、命令入队、事务执行。
2. 事务开始:
- Multi命令开启事务。
- 客户端从非事务状态切换成事务状态:是通过在客户端状态的flags属性中打开REDIS_MULTL标识完成的。
3. 命令入队:
客户端切换成事务状态后,服务器判断不同命令之后,需要执行的操作流程图
4. 事务队列:
- 每个Redis客户端都有自己的事务状态,这个事务状态保存在客户端的mstate属性里面:
- 事务状态包含一个事务队列,以及一个已入队命令的计数器:
- 事务队列是一个mutilCmd类型的数组,数组中的每一个multiCmd结构都保存了一个已入队列的相关信息,包括指向命令实现函数的指针,命令的参数,以及参数的数量。
- 事务队列以FIFO(先进先出)的方式保存入队命令。
- 事务状态的数据结构:
5. 执行事务:
- 处于事务装的客户端向服务器发送EXEC命令时,这个EXEC命令将立即被服务器执行。
- 执行EXEC命令时,服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行命令所得的结果全部返回给客户端。
三、WATCH命令的实现
1. 概念:
Watch命令是一个乐观锁,它可以在EXEC命令执行之前,监视任意数量的数据库键,并在Exec命令执行时,检查监视的键是否至少有一个已经被修改过了,如果被修改,则拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
2. watch_keys字典:
每个Redis数据库都保存着一个watch_keys字典,这个字典的键是某个被Watch命令监视的数据库键,值则是一个链表,链表中记录了所有监视相应数据库键的客户端:
3. 监视机制的触发:
所有对数据库进行修改的命令,在执行之后都会调用mutil.c/touchWatchKey函数对watch_keys字典进行检查,查看是否有客户端正在监视刚刚被命令修改的数据库键。如果有修改,那么touchWatchKey函数会将监视被修改键的客户端的REDIS_DIRTY_CAS标识打开,标识该客户端的事务安全性已经被破坏。
4. 判断事务是否安全:
当服务器接收到一个客户端发来的Exec命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务。
四、事务的ACID性质
在Redis中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当Redis运行在某种特定的持久化模式下,事务也具有耐久性(Durability)
1. Redis的事务和传统关系型数据库事务的区别:
Redis不支持事务回滚机制(rollback),即事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。
2. Redis事务可能出错的三个地方:
- 入队错误:
命令不存在,或者命令的格式不正确(此时会提示错误),Redis将决绝执行这个事务。
- 执行错误:
不能再入队时被服务器发现的错误,这些错误只会在命令实际执行时被触发。
即使在事务的执行过程中发生了错误,服务器也不会中断事务的执行,它会继续执行事务中余下的命令,并且已执行的命令(包括执行命令产生的结果)不会被出错的命令影响。
出错的命令会被服务器识别出来,并进行响应的错误处理,这些出错的命令不会对数据库作出任何修改,也不会对事务的一致性产生任何影响。
- 服务器停机:
执行事务的过程中停机,那么根据服务器使用的持久化模式,可能有以下情况出现:
如果服务器运行在无持久化的内存模式下,那么重启之后的数据库将是空白的,因此数据总是一致的。
如果服务器运行在RDB模式下,那么在事务中途停机不会导致不一致性,因为服务器可以根据现有的RDB文件来恢复数据,从而将数据库还原到一个一致的状态。如果找不到可供使用的RDB文件,那么重启之后的数据库将是空白的,而空白数据库总是一致的。
如果服务器运行在AOF模式下,那么在事务中途停机不会导致不一致性,因为服务器可以根据现有的AOF文件来恢复数据,从而将数据库还原到一个一致的状态。如果找不到可供使用的AOF文件,那么重启之后的数据库将是空白的,而空白数据库总是一致的。
3. Redis事务的耐久性由Redis所使用的持久化模式决定:
- 当服务器在无持久化的内存模式下运作时,事务不具有耐久性:一旦服务器停机,包括事务数据在内的所有服务器数据都将丢失。
- 当服务器在RDB持久化模式下运作时,服务器只会在特定的保存条件被满足时,才会执行 BGSAVE命令,对数据库进行保存操作,并且异步执行的 BGSAE不能保证事务数据被第一时间保存到硬盘里面,因此RDB持久化模式下的事务也不具有耐久性。
- 当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,程序总会在执行命令之后调用同步(syne)函数,将命令数据真正地保存到硬盘里面,因此这种配置下的事务是具有耐久性。
- 当服务器运行在AOF持久化模式下,并且appendfsync选项的值为everysec时,程序会每秒同步一次命令数据到硬盘。因为停机可能会恰好发生在等待同步的那一秒钟之内,这可能会造成事务数据丢失,所以这种配置下的事务不具有耐久性。
五、重点回顾
- 事务提供了一种将多个命令打包,然后一次性、有序地执行的机制。
- 多个命令会被入列到事务队列中,然后按FIFO的顺序执行。
- 事务咋执行过程中不会被中断,当事务队列中的所有命令都被执行完毕之后,事务才会结束。
- 带有Watch命令的事务会将客户端和被监视的键在数据库的watched_keys字典中进行关联,当键被修改时,程序会将所有监视被修改键的客户端的Redis_DIRTY_CAS标识打开。
- 只有在客户端的Redis_DIRTY_CAS标识未被打开是,服务器才会执行客户端提交的事务,否则,服务器拒绝执行客户端提交的事务。
- Redis的事务总是具体ACID中原子性,一致性和隔离性,当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。