引言?
这个文档是正在开发中的 Redis 集群功能的规范(specification)文档,文档分为两个部分:
- 第一部分介绍目前已经在 unstable 分支中实现了的那些功能。
- 第二部分介绍目前仍未实现的那些功能。
文档各个部分的内容可能会随着集群功能的设计修改而发生改变,其中,未实现功能发生修改的几率比已实现功能发生修改的几率要高。
这个规范包含了编写客户端库(client library)所需的全部知识,不过请注意,这里列出的一部分细节可能会在未来发生变化。
什么是 Redis 集群?
Redis 集群是一个分布式(distributed)、容错(fault-tolerant)的 Redis 实现,集群可以使用的功能是普通单机 Redis 所能使用的功能的一个子集(subset)。
Redis 集群中不存在中心(central)节点或者代理(proxy)节点,集群的其中一个主要设计目标是达到线性可扩展性(linear scalability)。
Redis 集群为了保证一致性(consistency)而牺牲了一部分容错性:系统会在保证对网络断线(net split)和节点失效(node failure)具有有限(limited)抵抗力的前提下,尽可能地保持数据的一致性。
Note
集群将节点失效视为网络断线的其中一种特殊情况。
集群的容错功能是通过使用主节点(master)和从节点(slave)两种角色(role)的节点(node)来实现的:
- 主节点和从节点使用完全相同的服务器实现,它们的功能(functionally)也完全一样,但从节点通常仅用于替换失效的主节点。
- 不过,如果不需要保证“先写入,后读取”操作的一致性(read-after-write consistency),那么可以使用从节点来执行只读查询。
Redis 集群实现的功能子集?
Redis 集群实现了单机 Redis 中,所有处理单个数据库键的命令。
针对多个数据库键的复杂计算操作,比如集合的并集操作、合集操作没有被实现,那些理论上需要使用多个节点的多个数据库键才能完成的命令也没有被实现。
在将来,用户也许可以通过 MIGRATE COPY 命令,在集群的计算节点(computation node)中执行针对多个数据库键的只读操作,但集群本身不会去实现那些需要将多个数据库键在多个节点中移来移去的复杂多键命令。
Redis 集群不像单机 Redis 那样支持多数据库功能,集群只使用默认的 0 号数据库,并且不能使用 SELECT 命令。
Redis 集群协议中的客户端和服务器?
Redis 集群中的节点有以下责任:
- 持有键值对数据。
- 记录集群的状态,包括键到正确节点的映射(mapping keys to right nodes)。
- 自动发现其他节点,识别工作不正常的节点,并在有需要时,在从节点中选举出新的主节点。
为了执行以上列出的任务,集群中的每个节点都与其他节点建立起了“集群连接(cluster bus)”,该连接是一个 TCP 连接,使用二进制协议进行通讯。
节点之间使用 Gossip 协议 来进行以下工作:
- 传播(propagate)关于集群的信息,以此来发现新的节点。
- 向其他节点发送 PING 数据包,以此来检查目标节点是否正常运作。
- 在特定事件发生时,发送集群信息。
除此之外,集群连接还用于在集群中发布或订阅信息。
因为集群节点不能代理(proxy)命令请求,所以客户端应该在节点返回 -MOVED 或者 -ASK 转向(redirection)错误时,自行将命令请求转发至其他节点。
因为客户端可以自由地向集群中的任何一个节点发送命令请求,并可以在有需要时,根据转向错误所提供的信息,将命令转发至正确的节点,所以在理论上来说,客户端是无须保存集群状态信息的。
不过,如果客户端可以将键和节点之间的映射信息保存起来,可以有效地减少可能出现的转向次数,籍此提升命令执行的效率。
键分布模型?
Redis 集群的键空间被分割为 16384 个槽(slot),集群的最大节点数量也是 16384 个。
Note
推荐的最大节点数量为 1000 个左右。
每个主节点都负责处理 16384 个哈希槽的其中一部分。
当我们说一个集群处于“稳定”(stable)状态时,指的是集群没有在执行重配置(reconfiguration)操作,每个哈希槽都只由一个节点进行处理。
Note
重配置指的是将某个/某些槽从一个节点移动到另一个节点。
Note
一个主节点可以有任意多个从节点,这些从节点用于在主节点发生网络断线或者节点失效时,对主节点进行替换。
以下是负责将键映射到槽的算法:
HASH_SLOT = CRC16(key) mod 16384
以下是该算法所使用的参数:
- 算法的名称: XMODEM (又称 ZMODEM 或者 CRC-16/ACORN)
- 结果的长度: 16 位
- 多项数(poly): 1021 (也即是 x16 + x12 + x5 + 1)
- 初始化值: 0000
- 反射输入字节(Reflect Input byte): False
- 发射输出 CRC (Reflect Output CRC): False
- 用于 CRC 输出值的异或常量(Xor constant to output CRC): 0000
- 该算法对于输入 "123456789" 的输出: 31C3
附录 A 中给出了集群所使用的 CRC16 算法的实现。
CRC16 算法所产生的 16 位输出中的 14 位会被用到。
在我们的测试中, CRC16 算法可以很好地将各种不同类型的键平稳地分布到 16384 个槽里面。
集群节点属性?
每个节点在集群中都有一个独一无二的 ID ,该 ID 是一个十六进制表示的 160 位随机数,在节点第一次启动时由 /dev/urandom 生成。
节点会将它的 ID 保存到配置文件,只要这个配置文件不被删除,节点就会一直沿用这个 ID 。
节点 ID 用于标识集群中的每个节点。一个节点可以改变它的 IP 和端口号,而不改变节点 ID 。集群可以自动识别出 IP/端口号的变化,并将这一信息通过 Gossip 协议广播给其他节点知道。
以下是每个节点都有的关联信息,并且节点会将这些信息发送给其他节点:
- 节点所使用的 IP 地址和 TCP 端口号。
- 节点的标志(flags)。
- 节点负责处理的哈希槽。
- 节点最近一次使用集群连接发送 PING 数据包(packet)的时间。
- 节点最近一次在回复中接收到 PONG 数据包的时间。
- 集群将该节点标记为下线的时间。
- 该节点的从节点数量。
- 如果该节点是从节点的话,那么它会记录主节点的节点 ID 。如果这是一个主节点的话,那么主节点 ID 这一栏的值为 0000000 。
以上信息的其中一部分可以通过向集群中的任意节点(主节点或者从节点都可以)发送 CLUSTER NODES 命令来获得。
以下是一个向集群中的主节点发送 CLUSTER NODES 命令的例子,该集群由三个节点组成:
$ redis-cli cluster nodes
d1861060fe6a534d42d8a19aeb36600e18785e04 :0 myself - 0 1318428930 connected 0-1364
3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 connected 1365-2729
d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 connected 2730-4095
在上面列出的三行信息中,从左到右的各个域分别是:节点 ID , IP 地址和端口号,标志(flag),最后发送 PING 的时间,最后接收 PONG 的时间,连接状态,节点负责处理的槽。
节点握手(已实现)?
节点总是应答(accept)来自集群连接端口的连接请求,并对接收到的 PING 数据包进行回复,即使这个 PING 数据包来自不可信的节点。
然而,除了 PING 之外,节点会拒绝其他所有并非来自集群节点的数据包。
要让一个节点承认另一个节点同属于一个集群,只有以下两种方法:
- 一个节点可以通过向另一个节点发送 MEET 信息,来强制让接收信息的节点承认发送信息的节点为集群中的一份子。一个节点仅在管理员显式地向它发送 CLUSTER MEET ip port 命令时,才会向另一个节点发送 MEET 信息。
- 另外,如果一个可信节点向另一个节点传播第三者节点的信息,那么接收信息的那个节点也会将第三者节点识别为集群中的一份子。也即是说,如果 A 认识 B , B 认识 C ,并且 B 向 A 传播关于 C 的信息,那么 A 也会将 C 识别为集群中的一份子,并尝试连接 C 。
这意味着如果我们将一个/一些新节点添加到一个集群中,那么这个/这些新节点最终会和集群中已有的其他所有节点连接起来。
这说明只要管理员使用 CLUSTER MEET 命令显式地指定了可信关系,集群就可以自动发现其他节点。
这种节点识别机制通过防止不同的 Redis 集群因为 IP 地址变更或者其他网络事件的发生而产生意料之外的联合(mix),从而使得集群更具健壮性。
当节点的网络连接断开时,它会主动连接其他已知的节点。
MOVED 转向?
一个 Redis 客户端可以向集群中的任意节点(包括从节点)发送命令请求。节点会对命令请求进行分析,如果该命令是集群可以执行的命令,那么节点会查找这个命令所要处理的键所在的槽。
如果要查找的哈希槽正好就由接收到命令的节点负责处理,那么节点就直接执行这个命令。
另一方面,如果所查找的槽不是由该节点处理的话,节点将查看自身内部所保存的哈希槽到节点 ID 的映射记录,并向客户端回复一个 MOVED 错误。
以下是一个 MOVED 错误的例子:
GET x
-MOVED 3999 127.0.0.1:6381
错误信息包含键 x 所属的哈希槽 3999 ,以及负责处理这个槽的节点的 IP 和端口号 127.0.0.1:6381 。客户端需要根据这个 IP 和端口号,向所属的节点重新发送一次 GET 命令请求。
注意,即使客户端在重新发送 GET 命令之前,等待了非常久的时间,以至于集群又再次更改了配置,使得节点 127.0.0.1:6381 已经不再处理槽 3999,那么当客户端向节点 127.0.0.1:6381 发送 GET 命令的时候,节点将再次向客户端返回 MOVED 错误,指示现在负责处理槽 3999 的节点。
虽然我们用 ID 来标识集群中的节点,但是为了让客户端的转向操作尽可能地简单,节点在 MOVED 错误中直接返回目标节点的 IP 和端口号,而不是目标节点的 ID 。
虽然不是必须的,但一个客户端应该记录(memorize)下“槽 3999 由节点 127.0.0.1:6381 负责处理“这一信息,这样当再次有命令需要对槽 3999执行时,客户端就可以加快寻找正确节点的速度。
注意,当集群处于稳定状态时,所有客户端最终都会保存有一个哈希槽至节点的映射记录(map of hash slots to nodes),使得集群非常高效:客户端可以直接向正确的节点发送命令请求,无须转向、代理或者其他任何可能发生单点故障(single point failure)的实体(entiy)。
除了 MOVED 转向错误之外,一个客户端还应该可以处理稍后介绍的 ASK 转向错误。
集群在线重配置(live reconfiguration)?
Redis 集群支持在集群运行的过程中添加或者移除节点。
实际上,节点的添加操作和节点的删除操作可以抽象成同一个操作,那就是,将哈希槽从一个节点移动到另一个节点:
- 添加一个新节点到集群,等于将其他已存在节点的槽移动到一个空白的新节点里面。
- 从集群中移除一个节点,等于将被移除节点的所有槽移动到集群的其他节点上面去。
因此,实现 Redis 集群在线重配置的核心就是将槽从一个节点移动到另一个节点的能力。因为一个哈希槽实际上就是一些键的集合,所以 Redis 集群在重哈希(rehash)时真正要做的,就是将一些键从一个节点移动到另一个节点。
要理解 Redis 集群如何将槽从一个节点移动到另一个节点,我们需要对 CLUSTER 命令的各个子命令进行介绍,这些命理负责管理集群节点的槽转换表(slots translation table)。
以下是 CLUSTER 命令可用的子命令:
- CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]
- CLUSTER DELSLOTS slot1 [slot2] ... [slotN]
- CLUSTER SETSLOT slot NODE node
- CLUSTER SETSLOT slot MIGRATING node
- CLUSTER SETSLOT slot IMPORTING node
最开头的两条命令 ADDSLOTS 和 DELSLOTS 分别用于向节点指派(assign)或者移除节点,当槽被指派或者移除之后,节点会将这一信息通过 Gossip 协议传播到整个集群。 ADDSLOTS 命令通常在新创建集群时,作为一种快速地将各个槽指派给各个节点的手段来使用。
CLUSTER SETSLOT slot NODE node 子命令可以将指定的槽 slot 指派给节点 node 。
至于 CLUSTER SETSLOT slot MIGRATING node 命令和 CLUSTER SETSLOT slot IMPORTING node 命令,前者用于将给定节点 node 中的槽 slot 迁移出节点,而后者用于将给定槽 slot 导入到节点 node :
-
当一个槽被设置为 MIGRATING 状态时,原来持有这个槽的节点仍然会继续接受关于这个槽的命令请求,但只有命令所处理的键仍然存在于节点时,节点才会处理这个命令请求。
如果命令所使用的键不存在与该节点,那么节点将向客户端返回一个 -ASK 转向(redirection)错误,告知客户端,要将命令请求发送到槽的迁移目标节点。
-
当一个槽被设置为 IMPORTING 状态时,节点仅在接收到 ASKING 命令之后,才会接受关于这个槽的命令请求。
如果客户端没有向节点发送 ASKING 命令,那么节点会使用 -MOVED 转向错误将命令请求转向至真正负责处理这个槽的节点。
上面关于 MIGRATING 和 IMPORTING 的说明有些难懂,让我们用一个实际的实例来说明一下。
假设现在,我们有 A 和 B 两个节点,并且我们想将槽 8 从节点 A 移动到节点 B ,于是我们:
- 向节点 B 发送命令 CLUSTER SETSLOT 8 IMPORTING A
- 向节点 A 发送命令 CLUSTER SETSLOT 8 MIGRATING B
每当客户端向其他节点发送关于哈希槽 8 的命令请求时,这些节点都会向客户端返回指向节点 A 的转向信息:
- 如果命令要处理的键已经存在于槽 8 里面,那么这个命令将由节点 A 处理。
- 如果命令要处理的键未存在于槽 8 里面(比如说,要向槽添加一个新的键),那么这个命令由节点 B 处理。
这种机制将使得节点 A 不再创建关于槽 8 的任何新键。
与此同时,一个特殊的客户端 redis-trib 以及 Redis 集群配置程序(configuration utility)会将节点 A 中槽 8 里面的键移动到节点 B 。
键的移动操作由以下两个命令执行:
CLUSTER GETKEYSINSLOT slot count
上面的命令会让节点返回 count 个 slot 槽中的键,对于命令所返回的每个键, redis-trib 都会向节点 A 发送一条 MIGRATE 命令,该命令会将所指定的键原子地(atomic)从节点 A 移动到节点 B (在移动键期间,两个节点都会处于阻塞状态,以免出现竞争条件)。
以下为 MIGRATE 命令的运作原理:
MIGRATE target_host target_port key target_database id timeout
执行 MIGRATE 命令的节点会连接到 target 节点,并将序列化后的 key 数据发送给 target ,一旦 target 返回 OK ,节点就将自己的 key 从数据库中删除。
从一个外部客户端的视角来看,在某个时间点上,键 key 要么存在于节点 A ,要么存在于节点 B ,但不会同时存在于节点 A 和节点 B 。
因为 Redis 集群只使用 0 号数据库,所以当 MIGRATE 命令被用于执行集群操作时, target_database 的值总是 0 。
target_database 参数的存在是为了让 MIGRATE 命令成为一个通用命令,从而可以作用于集群以外的其他功能。
我们对 MIGRATE 命令做了优化,使得它即使在传输包含多个元素的列表键这样的复杂数据时,也可以保持高效。
不过,尽管 MIGRATE 非常高效,对一个键非常多、并且键的数据量非常大的集群来说,集群重配置还是会占用大量的时间,可能会导致集群没办法适应那些对于响应时间有严格要求的应用程序。
ASK 转向?
在之前介绍 MOVED 转向的时候,我们说除了 MOVED 转向之外,还有另一种 ASK 转向。
当节点需要让一个客户端长期地(permanently)将针对某个槽的命令请求发送至另一个节点时,节点向客户端返回 MOVED 转向。
另一方面,当节点需要让客户端仅仅在下一个命令请求中转向至另一个节点时,节点向客户端返回 ASK 转向。
比如说,在我们上一节列举的槽 8 的例子中,因为槽 8 所包含的各个键分散在节点 A 和节点 B 中,所以当客户端在节点 A 中没找到某个键时,它应该转向到节点 B 中去寻找,但是这种转向应该仅仅影响一次命令查询,而不是让客户端每次都直接去查找节点 B :在节点 A 所持有的属于槽 8 的键没有全部被迁移到节点 B 之前,客户端应该先访问节点 A ,然后再访问节点 B 。
因为这种转向只针对 16384 个槽中的其中一个槽,所以转向对集群造成的性能损耗属于可接受的范围。
因为上述原因,如果我们要在查找节点 A 之后,继续查找节点 B ,那么客户端在向节点 B 发送命令请求之前,应该先发送一个 ASKING 命令,否则这个针对带有 IMPORTING 状态的槽的命令请求将被节点 B 拒绝执行。
接收到客户端 ASKING 命令的节点将为客户端设置一个一次性的标志(flag),使得客户端可以执行一次针对 IMPORTING 状态的槽的命令请求。
从客户端的角度来看, ASK 转向的完整语义(semantics)如下:
- 如果客户端接收到 ASK 转向,那么将命令请求的发送对象调整为转向所指定的节点。
- 先发送一个 ASKING 命令,然后再发送真正的命令请求。
- 不必更新客户端所记录的槽 8 至节点的映射:槽 8 应该仍然映射到节点 A ,而不是节点 B 。
一旦节点 A 针对槽 8 的迁移工作完成,节点 A 在再次收到针对槽 8 的命令请求时,就会向客户端返回 MOVED 转向,将关于槽 8 的命令请求长期地转向到节点 B 。
注意,即使客户端出现 Bug ,过早地将槽 8 映射到了节点 B 上面,但只要这个客户端不发送 ASKING 命令,客户端发送命令请求的时候就会遇上MOVED 错误,并将它转向回节点 A 。
容错?
节点失效检测?
以下是节点失效检查的实现方法:
-
当一个节点向另一个节点发送 PING 命令,但是目标节点未能在给定的时限内返回 PING 命令的回复时,那么发送命令的节点会将目标节点标记为PFAIL (possible failure,可能已失效)。
等待 PING 命令回复的时限称为“节点超时时限(node timeout)”,是一个节点选项(node-wise setting)。
-
每次当节点对其他节点发送 PING 命令的时候,它都会随机地广播三个它所知道的节点的信息,这些信息里面的其中一项就是说明节点是否已经被标记为 PFAIL 或者 FAIL 。
-
当节点接收到其他节点发来的信息时,它会记下那些被其他节点标记为失效的节点。这称为失效报告(failure report)。
-
如果节点已经将某个节点标记为 PFAIL ,并且根据节点所收到的失效报告显式,集群中的大部分其他主节点也认为那个节点进入了失效状态,那么节点会将那个失效节点的状态标记为 FAIL 。
-
一旦某个节点被标记为 FAIL ,关于这个节点已失效的信息就会被广播到整个集群,所有接收到这条信息的节点都会将失效节点标记为 FAIL 。
简单来说,一个节点要将另一个节点标记为失效,必须先询问其他节点的意见,并且得到大部分主节点的同意才行。
因为过期的失效报告会被移除,所以主节点要将某个节点标记为 FAIL 的话,必须以最近接收到的失效报告作为根据。
在以下两种情况中,节点的 FAIL 状态会被移除:
-
如果被标记为 FAIL 的是从节点,那么当这个节点重新上线时, FAIL 标记就会被移除。
保持(retaning)从节点的 FAIL 状态是没有意义的,因为它不处理任何槽,一个从节点是否处于 FAIL 状态,决定了这个从节点在有需要时能否被提升为主节点。
-
如果一个主节点被打上 FAIL 标记之后,经过了节点超时时限的四倍时间,再加上十秒钟之后,针对这个主节点的槽的故障转移操作仍未完成,并且这个主节点已经重新上线的话,那么移除对这个节点的 FAIL 标记。
在第二种情况中,如果故障转移未能顺利完成,并且主节点重新上线,那么集群就继续使用原来的主节点,从而免去管理员介入的必要。
集群状态检测(已部分实现)?
每当集群发生配置变化时(可能是哈希槽更新,也可能是某个节点进入失效状态),集群中的每个节点都会对它所知道的节点进行扫描(scan)。
一旦配置处理完毕,集群会进入以下两种状态的其中一种:
- FAIL :集群不能正常工作。当集群中有某个节点进入失效状态时,集群不能处理任何命令请求,对于每个命令请求,集群节点都返回错误回复。
- OK :集群可以正常工作,负责处理全部 16384 个槽的节点中,没有一个节点被标记为 FAIL 状态。
这说明即使集群中只有一部分哈希槽不能正常使用,整个集群也会停止处理任何命令。
不过节点从出现问题到被标记为 FAIL 状态的这段时间里,集群仍然会正常运作,所以集群在某些时候,仍然有可能只能处理针对 16384 个槽的其中一个子集的命令请求。
以下是集群进入 FAIL 状态的两种情况:
- 至少有一个哈希槽不可用,因为负责处理这个槽的节点进入了 FAIL 状态。
- 集群中的大部分主节点都进入下线状态。当大部分主节点都进入 PFAIL 状态时,集群也会进入 FAIL 状态。
第二个检查是必须的,因为要将一个节点从 PFAIL 状态改变为 FAIL 状态,必须要有大部分主节点进行投票表决,但是,当集群中的大部分主节点都进入失效状态时,单凭一个两个节点是没有办法将一个节点标记为 FAIL 状态的。
因此,有了第二个检查条件,只要集群中的大部分主节点进入了下线状态,那么集群就可以在不请求这些主节点的意见下,将某个节点判断为 FAIL 状态,从而让整个集群停止处理命令请求。
从节点选举?
一旦某个主节点进入 FAIL 状态,如果这个主节点有一个或多个从节点存在,那么其中一个从节点会被升级为新的主节点,而其他从节点则会开始对这个新的主节点进行复制。
新的主节点由已下线主节点属下的所有从节点中自行选举产生,以下是选举的条件:
- 这个节点是已下线主节点的从节点。
- 已下线主节点负责处理的槽数量非空。
- 从节点的数据被认为是可靠的,也即是,主从节点之间的复制连接(replication link)的断线时长不能超过节点超时时限(node timeout)乘以REDIS_CLUSTER_SLAVE_VALIDITY_MULT 常量得出的积。
如果一个从节点满足了以上的所有条件,那么这个从节点将向集群中的其他主节点发送授权请求,询问它们,是否允许自己(从节点)升级为新的主节点。
如果发送授权请求的从节点满足以下属性,那么主节点将向从节点返回 FAILOVER_AUTH_GRANTED 授权,同意从节点的升级要求:
- 发送授权请求的是一个从节点,并且它所属的主节点处于 FAIL 状态。
- 在已下线主节点的所有从节点中,这个从节点的节点 ID 在排序中是最小的。
- 这个从节点处于正常的运行状态:它没有被标记为 FAIL 状态,也没有被标记为 PFAIL 状态。
一旦某个从节点在给定的时限内得到大部分主节点的授权,它就会开始执行以下故障转移操作:
- 通过 PONG 数据包(packet)告知其他节点,这个节点现在是主节点了。
- 通过 PONG 数据包告知其他节点,这个节点是一个已升级的从节点(promoted slave)。
- 接管(claiming)所有由已下线主节点负责处理的哈希槽。
- 显式地向所有节点广播一个 PONG 数据包,加速其他节点识别这个节点的进度,而不是等待定时的 PING / PONG 数据包。
所有其他节点都会根据新的主节点对配置进行相应的更新,特别地:
- 所有被新的主节点接管的槽会被更新。
- 已下线主节点的所有从节点会察觉到 PROMOTED 标志,并开始对新的主节点进行复制。
- 如果已下线的主节点重新回到上线状态,那么它会察觉到 PROMOTED 标志,并将自身调整为现任主节点的从节点。
在集群的生命周期中,如果一个带有 PROMOTED 标识的主节点因为某些原因转变成了从节点,那么该节点将丢失它所带有的 PROMOTED 标识。
发布/订阅(已实现,但仍然需要改善)?
在一个 Redis 集群中,客户端可以订阅任意一个节点,也可以向任意一个节点发送信息,节点会对客户端所发送的信息进行转发。
在目前的实现中,节点会将接收到的信息广播至集群中的其他所有节点,在将来的实现中,可能会使用 bloom filter 或者其他算法来优化这一操作。
附录 A: CRC16 算法的 ANSI 实现参考?
/*
* Copyright 2001-2010 Georges Menie (www.menie.org)
* Copyright 2010 Salvatore Sanfilippo (adapted to Redis coding style)
* All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the University of California, Berkeley nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* CRC16 implementation acording to CCITT standards.
*
- Note by @antirez: this is actually the XMODEM CRC 16 algorithm, using the
- following parameters:
- Name : “XMODEM”, also known as “ZMODEM”, “CRC-16/ACORN”
- Width : 16 bit
- Poly : 1021 (That is actually x^16 + x^12 + x^5 + 1)
- Initialization : 0000
- Reflect Input byte : False
- Reflect Output CRC : False
- Xor constant to output CRC : 0000
- Output for “123456789” : 31C3
*/
static const uint16_t crc16tab[256]= {
0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,
0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,
0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,
0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,
0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,
0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,
0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,
0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,
0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,
0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,
0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,
0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,
0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,
0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,
0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f,
0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067,
0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e,
0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256,
0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d,
0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405,
0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c,
0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634,
0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab,
0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3,
0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a,
0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92,
0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9,
0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1,
0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8,
0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0
};
uint16_t crc16(const char *buf, int len) {
int counter;
uint16_t crc = 0;
for (counter = 0; counter < len; counter++)
crc = (crc<<8) ^ crc16tab[((crc>>8) ^ *buf++)&0x00FF];
return crc;
}
Redis Cluster 功能特性
Redis 集群是分布式的redis 实现,具有以下特性:
1. 高可用性与可线性扩张到1000个节点 2. 数据自动路由到多个节点 3. 节点间数据共享 4. 可动态添加或者删除节点 5. 部分节点不可达时,集群仍可用 6. 数据通过异步复制,不保证数据的强一致性 7. 可动态调整数据分布Redis 集群架构图
其中一: Redis 集群协议 1、Redis 集群,节点负责存储数据、记录集群状态,集群节点能自动发现其他节点,检测出节点的状态,并在需要的时候,推选中主节点 2、Redis 集群节点中通过TCP连接和一个二级制协议(cluster bus) 建立通信。发现新的节点、发送PING包、特定的情况下发送集群消息。集群连接能够发布与订阅消息 3、Redis 集群节点不能代理请求,客户端发起请求后,接收到重定向(MOVED\ASK)错误,会自动重定向到其他节点。理论上来说,客户端是可以自由地向集群中的所有节点发送请求,在需要的时候把请求重定向到其他节点,所以客户端是不需要保存集群状态。 不过客户端可以缓存键值和节点之间的映射关系,这样能明显提高命令执行的效率。 二: 安全写入 Redis 集群节点之间使用异步复制,在分区过程中存在窗口,容易导致丢失写入数据,Redis集群即使努力尝试所有写入,但是以下两种情况可能丢失数据: 1、命令操作到达主节点后,但在主节点回复的时候,此时写入可能还没有通过主节点复制到从节点那里。如果这时候主库宕机了,这条命令永久丢失。以防主节点长时间不可达而它的一个从节点已经被提升为主节点。 2、分区导致一个主节点不可达,然而集群发送故障转移(failover),提升从节点为主节点,原来的主节点再次恢复。一个没有更新路由表(routing table)的客户端或许会在集群把这个主节点变成一个从节点(新主节点的从节点)之前对它进行写入操作。导致数据彻底丢失 三: 可用性 Redis 集群少数节点不可用后,在经过cluster-node-timeout时间后,集群根据自动故障机制,将从节点提升为主节点。这事集群恢复可用 举个例子,一个由 N 个主节点组成的集群,每个主节点都只有一个从节点。当有一个节点(因为故障)被分割出去后,集群的多数节点这边仍然是可访问的。当有两个节点(因故障)被分割出去后集群仍可用的概率是 1-(1/(N*2-1))(在第一个节点故障出错后总共剩下 N*2-1 个节点,那么失去冗余备份(即失去从节点)的那个主节点也故障出错的概率是 1/(N*2-1)))。 比如一个拥有6个节点的集群,每个节点都只有一个从节点,那么在两个节点从多数节点这边分割出去后集群不再可用的概率是 1/(6*2-1) = 0.0909,即有大约 9% 的概率。</pre><h3 style="word-wrap:break-word; margin:20px 0px 10px; padding:0px; font-size:1.2em; font-family:'lucida grande',Helvetica,Arial,Georgia,'Xin Gothic','Hiragino Sans GB','Droid Sans Fallback','Microsoft YaHei',sans-serif; line-height:1.1; color:rgb(51,51,51)"><a name="t18"></a>Redis 集群数据分布</h3><pre style="word-wrap:break-word; margin-top:0px; margin-bottom:10px; padding:9.5px; overflow:auto; font-family:Menlo,Monaco,Consolas,'Courier New',monospace; font-size:13px; line-height:1.42857; color:rgb(51,51,51); word-break:break-all; border:1px solid rgb(204,204,204); background-color:rgb(245,245,245)">Redis 集群没有使用一致性hash,引入了哈希槽(HASH SLOT).
Redis 集群中所有的主节点都负责 16384 个哈希槽中的一部分。当集群处于稳定状态时,集群中没有在执行重配置(reconfiguration)操作,每个哈希槽都只由一个节点进行处理(不过主节点可以有一个或多个从节点,可以在网络断线或节点失效时替换掉主节点)
slot = CRC16(KEY) / 16384
Redis 集群键HASH标签
目标:
HASH 标签是确保两个KEY 都能在同一个HASH槽的一种方式。
实现方式:
HASH 槽是用另一种不同的计算方式计算的。基本来说,如果KEY包含一个"{…}“这样的模式,只有“{” 和 “}” 之间的字符串会被用来做HASH以获取HAS槽。如果同时出现多个“{}” 计算方式如下:
* 如果KEY 包含一个 “{” 字符
* 那么在 “{”的右边就会字符 “}”
* 在字符 “{” 和 “}“直接会有一个或多个字符。但是第一个”}” 一定会出现在第一个”{"之后
* 只有在第一个 { 和它右边第一个 } 之间的内容会被用来计算哈希值
例子:
1、比如这两个键 user:{1000}.following 和user:{1000}.followers 会被哈希到同一个哈希槽里,因为只有 “1000” 这个子串会被用来计算哈希值。
2、对于 user{}{list} 这个键,整个键都会被用来计算哈希值,因为第一个出现的 { 和它右边第一个出现的 } 之间没有任何字符。
3、对于 user{{momoid}}following 这个键,用来计算哈希值的是 “{momoid” 这个子串,因为它是第一个 { 及其右边第一个 } 之间的内容。
4、对于 user{momoid}{following} 这个键,用来计算哈希值的是 “momoid” 这个子串,因为算法会在第一次有效或无效(比如中间没有任何字节)地匹配到 { 和 } 的时候停止。
5、按照这个算法,如果一个键是以 {} 开头的话,那么就当作整个键会被用来计算哈希值。当使用二进制数据做为键名称的时候,这是非常有用的。
Redis 集群相关命令
集群
1、CLUSTER INFO 打印集群的信息
2、CLUSTER NODES 列出集群当前已知的所有节点(node),以及这些节点的相关信息。
3、CLUSTER FAILOVER 手动故障转移,需要在转移的主节点的从节点上执行
节点
1、CLUSTER MEET 将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。
2、CLUSTER FORGET 从集群中移除 node_id 指定的节点。
3、CLUSTER REPLICATE 将当前节点设置为 node_id 指定的节点的从节点。
4、CLUSTER SAVECONFIG 将节点的配置文件保存到硬盘里面。
槽(slot)
1、CLUSTER ADDSLOTS [slot …] 将一个或多个槽(slot)指派(assign)给当前节点。
2、CLUSTER DELSLOTS [slot …] 移除一个或多个槽对当前节点的指派。
3、CLUSTER FLUSHSLOTS 移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
4、CLUSTER SETSLOT NODE 将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽,然后再进行指派。
1、CLUSTER SETSLOT MIGRATING 将本节点的槽 slot 迁移到 node_id 指定的节点中。
2、CLUSTER SETSLOT IMPORTING 从 node_id 指定的节点中导入槽 slot 到本节点。
3、CLUSTER SETSLOT STABLE 取消对槽 slot 的导入(import)或者迁移(migrate)。
键
1、CLUSTER KEYSLOT 计算键 key 应该被放置在哪个槽上。
2、CLUSTER COUNTKEYSINSLOT 返回槽 slot 目前包含的键值对数量。
3、CLUSTER GETKEYSINSLOT 返回 count 个 slot 槽中的键。
不支持的命令:
1、不支持SELECT 命令,集群只使用数据库 0
2、不支持多个KEY的操作 如 MSET、SUION、SINTER等命令 (因为KEYS 无法hash到同一个slot中)
redis-trib.rb 相关命令
1、redis-trib.rb create [–replicas N] host:ip [host:ip …] 创建集群
2、redis-trib.rb add-node host:ip host:ip 将前面的host:ip 添加到集群中
3、redis-trib.rb check host:ip 检查集群的状态
4、redis-trib.rb reshard host:ip OR redis-trib.rb reshard --from host:port --to host:port --slots --yes集群重新分片
5、redis-trib.rb del-node host:ip ‘NODE ID’ 将节点从集群中移除
Redis 集群配置
redis 集群需要运行在 集群模式的redis实例,不是普通的redis实例。集群模式需要添加集群相关的配置。开启集群模式的redis实例,可以使用集群特有的命令和特性
其中相关配置如下:
必须配置:
cluster-enabled yes --> 开启集群模式
cluster-config-file nodes-30000.conf --> 集群相关的信息
cluster-node-timeout 15000 --> 节点超时时间,用来failover的操作
可选配置:
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage yes
Redis Cluster 主从搭建
启动集群模式的实例(与普通启动方式一致),不需搭建主从关系
搭建集群: 在上述启动的6个redis实例中,搭建集群。通过redis自带的集群命令行工具 redis-trib.rb 。 redis-trib.rb 位于redis源码包中src文件中。它可以完成创建集群、检查集群、集群reshard、添加节点、删除节点等操作
创建集群 redis-trib.rb create [–replicas [N]] host:ip [host:ip]
redis-trib.rb create --replicas 1 127.0.0.1:30000 127.0.0.1:30001 127.0.0.1:30001 127.0.0.1:31000 127.0.0.1:31001 127.0.0.1:31002
其中 --replicas N 选项表明集群中的每个节点带几个从节点
输出日志:
[OK] All 16384 slots covered
集群状态检查 redis-trib.rb check host:ip
redis-trib.rb check 127.0.0.1:30000
输出日志:
Connecting to node 127.0.0.1:30000: OK
Connecting to node 127.0.0.1:30002: OK
Connecting to node 127.0.0.1:31000: OK
Connecting to node 127.0.0.1:31001: OK
Connecting to node 127.0.0.1:30001: OK
Connecting to node 127.0.0.1:31002: OK
>>> Performing Cluster Check (using node 127.0.0.1:30000)
M: 36801ef9849f12526be1e954f9e6f6fa24c50d46 127.0.0.1:30000
slots:0-5961,10923-11421 (6461 slots) master
1 additional replica(s)
M: 98c4c66ee189569dec47a9600b057f90626cc6a7 127.0.0.1:30002
slots:11422-16383 (4962 slots) master
1 additional replica(s)
S: 54d7d1241b1d9c24f76d99e9814d8cf8d8db474e 127.0.0.1:31000
slots: (0 slots) slave
replicates 36801ef9849f12526be1e954f9e6f6fa24c50d46
S: 3f5ae989b9b1b6617c53e77ed4853b618408bbe6 127.0.0.1:31001
slots: (0 slots) slave
replicates 6b880ae14f8c9dbfd54f8c4811cf0c039d523216
M: 6b880ae14f8c9dbfd54f8c4811cf0c039d523216 127.0.0.1:30001
slots:5962-10922 (4961 slots) master
1 additional replica(s)
S: 81a3a70ce2fbb8bcce2e9be9ed77e34d9d4d5b21 127.0.0.1:31002
slots: (0 slots) slave
replicates 98c4c66ee189569dec47a9600b057f90626cc6a7
[OK] All nodes agree about slots configuration.
>>> Check for open slots…
>>> Check slots coverage…
[OK] All 16384 slots covered.
添加节点: 启动一个新的集群模式的redis实例。使用 redis-trib.rb add-node host:ip host:ip
redis-trib.rb add-node 127.0.0.1:30004 127.0.0.1:3000
其中 127.0.0.1:30004 为新节点 127.0.0.1:30000 为集群中任意节点
查看集群节点:
redis-cli -c -p 30000
127.0.0.1:30000> CLUSTER NODES
98c4c66ee189569dec47a9600b057f90626cc6a7 127.0.0.1:30002 master - 0 1429686483614 3 connected 11422-16383
54d7d1241b1d9c24f76d99e9814d8cf8d8db474e 127.0.0.1:31000 slave 36801ef9849f12526be1e954f9e6f6fa24c50d46 0 1429686485615 7 connected
3f5ae989b9b1b6617c53e77ed4853b618408bbe6 127.0.0.1:31001 slave 6b880ae14f8c9dbfd54f8c4811cf0c039d523216 0 1429686484614 5 connected
2eb135bf03dbdbc57e704578b2833cc3fb860b6e 127.0.0.1:30004 master - 0 1429686479607 0 connected --> 新节点
6b880ae14f8c9dbfd54f8c4811cf0c039d523216 127.0.0.1:30001 master - 0 1429686481612 2 connected 5962-10922
81a3a70ce2fbb8bcce2e9be9ed77e34d9d4d5b21 127.0.0.1:31002 slave 98c4c66ee189569dec47a9600b057f90626cc6a7 0 1429686482613 6 connected
36801ef9849f12526be1e954f9e6f6fa24c50d46 127.0.0.1:30000 myself,master - 0 0 7 connected 0-5961 10923-11421
输出信息解析:
1、节点ID
2、IP:PORT
3、节点状态标识: master、slave、myself、fail?、fail
4、如果是从节点,表示主节点的ID。如果是主节点,为 ‘-’
5、集群最近一次向各个节点发送PING命令后,过去多长时间还没有接到回复
6、节点最近一次返回PONG的时间戳
7、节点的配置纪元
8、本节点的网络连接情况: connected、disconnected
9、如果是主节点,表示节点包含的曹
添加从节点: CLUSTER REPLICATE ID
127.0.0.1:31004> CLUSTER REPLICATE 2eb135bf03dbdbc57e704578b2833cc3fb860b6e
其中 2eb135bf03dbdbc57e704578b2833cc3fb860b6e 为主库的集群ID
集群reshard: 为新节点分片slots redis-trib.rb reshard host:port
redis-trib.rb reshard 127.0.0.1:30004
日志输出:
Shell# redis-trib.rb reshard 127.0.0.1:30004
Connecting to node 127.0.0.1:30004: OK
Connecting to node 127.0.0.1:30000: OK
Connecting to node 127.0.0.1:31001: OK
Connecting to node 127.0.0.1:30001: OK
Connecting to node 127.0.0.1:31000: OK
Connecting to node 127.0.0.1:30002: OK
Connecting to node 127.0.0.1:31002: OK
>>> Performing Cluster Check (using node 127.0.0.1:30004)
M: 2eb135bf03dbdbc57e704578b2833cc3fb860b6e 127.0.0.1:30004 --> 新节点信息
slots: (0 slots) master
0 additional replica(s)
M: 36801ef9849f12526be1e954f9e6f6fa24c50d46 127.0.0.1:30000
slots:0-5961,10923-11421 (6461 slots) master
1 additional replica(s)
S: 3f5ae989b9b1b6617c53e77ed4853b618408bbe6 127.0.0.1:31001
slots: (0 slots) slave
replicates 6b880ae14f8c9dbfd54f8c4811cf0c039d523216
M: 6b880ae14f8c9dbfd54f8c4811cf0c039d523216 127.0.0.1:30001
slots:5962-10922 (4961 slots) master
1 additional replica(s)
S: 54d7d1241b1d9c24f76d99e9814d8cf8d8db474e 127.0.0.1:31000
slots: (0 slots) slave
replicates 36801ef9849f12526be1e954f9e6f6fa24c50d46
M: 98c4c66ee189569dec47a9600b057f90626cc6a7 127.0.0.1:30002
slots:11422-16383 (4962 slots) master
1 additional replica(s)
S: 81a3a70ce2fbb8bcce2e9be9ed77e34d9d4d5b21 127.0.0.1:31002
slots: (0 slots) slave
replicates 98c4c66ee189569dec47a9600b057f90626cc6a7
[OK] All nodes agree about slots configuration.
>>> Check for open slots…
>>> Check slots coverage…
[OK] All 16384 slots covered.
How many slots do you want to move (from 1 to 16384)? 1000 --> slot数量
What is the receiving node ID? 2eb135bf03dbdbc57e704578b2833cc3fb860b6e --> 接收集群NODE ID
Please enter all the source node IDs.
Type ‘all’ to use all the nodes as source nodes for the hash slots.
Type ‘done’ once you entered all the source nodes IDs.
Source node #1:36801ef9849f12526be1e954f9e6f6fa24c50d46 --> 来源NODE ID
Source node #2:done
…
Redis Cluster failover机制
节点心跳
Redis 集群在运行的过程中,每个节点每秒会随机ping几个节点,不过每个节点都会保证去PING满足这个条件的其他节点:在过去的一半"cluster-node-timeout"时间里都没有发送PING包过去或者没有接收从那节点发来的PONG包的节点。在"cluster-node-timeout"时间过去之前,若TCP连接有问题,节点会尝试去重试连接,确保自己不会被当做不可达的节点。
如果 “cluster-node-time” 被设为一个很小的数而节点数量(N)非常大,那么消息交流数量会比 O(N) 更大,因为每个节点都会尝试去 ping 每一个在过去一半 NODE_TIMEOUT 时间里都没更新信息的节点。
失效检测
Redis 集群失效检测是用来识别出大多数节点何时无法访问某一个主节点或从节点。当这个事件发生时,就提升一个从节点来做主节点;若如果无法提升从节点来做主节点的话,那么整个集群就置为错误状态并停止接收客户端的查询
每个节点都有一份跟其他已知节点相关的标识列表。其中有两个标识是用于失效检测,分别是 PFAIL 和 FAIL.
PFAIL 标识: 表示可能失效(Possible failure),这是一个非公认的(non acknowledged)失效类型。
当一个节点在超过 “cluster-node-timeout” 时间后仍无法访问某个节点,那么它会用 PFAIL 来标识这个不可达的节点。无论节点类型是什么,主节点和从节点都能标识其他的节点为 PFAIL
FAIL 表示一个节点已经失效,而且这个情况已经被大多数主节点在某段固定时间内确认过的了。
满足以下条件,PFAIL 状态升级为 FAIL 状态:(设定集群含有 A B C AS BS CS 六个节点)
1、节点A正常,节点C 状态为 PFAIL
2、节点A 通过gossip字段收集集群中大部分节点标识节点C的状态信息
3、如果大部分节点标识节点C为 PFAIL 状态,或者 cluster-node-timeout * FAIL_REPORT_VALIDITY_MULT 这段时间内处于 PFAIL状态此时节点A 会标记 节点C 为 FAIL 状态,并向所有的节点发送关于节点C的 FAIL 信息。 FAIL 信息会强制接收的节点把节点C 标识为 FAIL 状态
NOTE:
FAIL 标识基本都是单向的,也就是说,一个节点能从 PFAIL 状态升级到 FAIL 状态. 清除FAIL状态的方法:
1、节点已经恢复可达的,并且它是一个从节点。在这种情况下,FAIL 标识可以清除掉,因为从节点并没有被故障转移。
2、节点已经恢复可达的,而且它是一个主节点,但经过了很长时间(N * NODE_TIMEOUT)后也没有检测到任何从节点被提升了。
从选举与提升
从节点的选举与提升都是由从节点处理的,主节点会投票要提升哪个从节点。当满足以下条件,一个节点可以发起选举:
1、该从节点的主节点处理 FALI 状态
2、这个主节点负责的HASH曹个数不为O
3、从节点和主节点之间的重复连接(replication link)断线不超过一段给定的时间,这是为了确保从节点的数据是可靠
一旦从节点被推选出来,就会想主节点请求投票,一旦从节点赢得投票,它会响所有其他节点发送PING 和 PONG 数据包,宣布自己已经成为主节点,并且提供它的HASH槽信息,并配置 currentEpoch 信息
为了加速其他节点的重新配置,该节点会广播一个 pong 包 给集群里的所有节点(那些现在访问不到的节点最终也会收到一个 ping 包或 pong 包,并且进行重新配置)。其他节点会检测到有一个新的主节点(带着更大的configEpoch)在负责处理之前一个旧的主节点负责的哈希槽,然后就升级自己的配置信息。 旧主节点的从节点,或者是经过故障转移后重新加入集群的该旧主节点,不仅会升级配置信息,还会配置新主节点的备份。
模拟宕机(实现故障转移)
集群当期转态:
127.0.0.1:30000> CLUSTER NODES
98c4c66ee189569dec47a9600b057f90626cc6a7 127.0.0.1:30002 master - 0 1429696352375 3 connected 11422-16383
54d7d1241b1d9c24f76d99e9814d8cf8d8db474e 127.0.0.1:31000 slave 36801ef9849f12526be1e954f9e6f6fa24c50d46 0 1429696349869 9 connected
3f5ae989b9b1b6617c53e77ed4853b618408bbe6 127.0.0.1:31001 slave 6b880ae14f8c9dbfd54f8c4811cf0c039d523216 0 1429696346364 5 connected
6b880ae14f8c9dbfd54f8c4811cf0c039d523216 127.0.0.1:30001 master - 0 1429696351373 2 connected 5962-10922
81a3a70ce2fbb8bcce2e9be9ed77e34d9d4d5b21 127.0.0.1:31002 slave 98c4c66ee189569dec47a9600b057f90626cc6a7 0 1429696350370 6 connected
36801ef9849f12526be1e954f9e6f6fa24c50d46 127.0.0.1:30000 myself,master - 0 0 9 connected 0-5961 10923-11421
其中 127.0.0.1:31000 节点为 127.0.0.1:30000 从节点
关闭其中的一个节点后(127.0.0.1:30000)的集群状态:
127.0.0.1:30001> CLUSTER NODES
81a3a70ce2fbb8bcce2e9be9ed77e34d9d4d5b21 127.0.0.1:31002 slave 98c4c66ee189569dec47a9600b057f90626cc6a7 0 1429696448146 6 connected
98c4c66ee189569dec47a9600b057f90626cc6a7 127.0.0.1:30002 master - 0 1429696447143 3 connected 11422-16383
6b880ae14f8c9dbfd54f8c4811cf0c039d523216 127.0.0.1:30001 myself,master - 0 0 2 connected 5962-10922
36801ef9849f12526be1e954f9e6f6fa24c50d46 127.0.0.1:30000 master,fail? - 1429696434521 1429696430116 9 disconnected 0-5961 10923-11421
3f5ae989b9b1b6617c53e77ed4853b618408bbe6 127.0.0.1:31001 slave 6b880ae14f8c9dbfd54f8c4811cf0c039d523216 0 1429696445139 5 connected
54d7d1241b1d9c24f76d99e9814d8cf8d8db474e 127.0.0.1:31000 slave 36801ef9849f12526be1e954f9e6f6fa24c50d46 0 1429696449148 9 connected
其中 127.0.0.1:30000 的状态fail? 表示正在判断是否失败
127.0.0.1:30001> CLUSTER NODES
81a3a70ce2fbb8bcce2e9be9ed77e34d9d4d5b21 127.0.0.1:31002 slave 98c4c66ee189569dec47a9600b057f90626cc6a7 0 1429696473317 6 connected
98c4c66ee189569dec47a9600b057f90626cc6a7 127.0.0.1:30002 master - 0 1429696474317 3 connected 11422-16383
6b880ae14f8c9dbfd54f8c4811cf0c039d523216 127.0.0.1:30001 myself,master - 0 0 2 connected 5962-10922
36801ef9849f12526be1e954f9e6f6fa24c50d46 127.0.0.1:30000 master,fail - 1429696434521 1429696430116 9 disconnected
3f5ae989b9b1b6617c53e77ed4853b618408bbe6 127.0.0.1:31001 slave 6b880ae14f8c9dbfd54f8c4811cf0c039d523216 0 1429696472315 5 connected
54d7d1241b1d9c24f76d99e9814d8cf8d8db474e 127.0.0.1:31000 master - 0 1429696471313 10 connected 0-5961 10923-11421
其中 127.0.0.1:30000 的状态 fail 表示节点失败,127.0.0.1:30000 节点提升为主库。
恢复关闭的实例
127.0.0.1:30001> CLUSTER NODES
81a3a70ce2fbb8bcce2e9be9ed77e34d9d4d5b21 127.0.0.1:31002 slave 98c4c66ee189569dec47a9600b057f90626cc6a7 0 1429696545465 6 connected
98c4c66ee189569dec47a9600b057f90626cc6a7 127.0.0.1:30002 master - 0 1429696542960 3 connected 11422-16383
6b880ae14f8c9dbfd54f8c4811cf0c039d523216 127.0.0.1:30001 myself,master - 0 0 2 connected 5962-10922
36801ef9849f12526be1e954f9e6f6fa24c50d46 127.0.0.1:30000 slave 54d7d1241b1d9c24f76d99e9814d8cf8d8db474e 0 1429696542458 10 connected
3f5ae989b9b1b6617c53e77ed4853b618408bbe6 127.0.0.1:31001 slave 6b880ae14f8c9dbfd54f8c4811cf0c039d523216 0 1429696546467 5 connected
54d7d1241b1d9c24f76d99e9814d8cf8d8db474e 127.0.0.1:31000 master - 0 1429696547470 10 connected 0-5961 10923-11421
其中 127.0.0.1:30000 变成 127.0.0.1:31000的从库
总结:
优点:Redis 是一个高性能的key-value数据库。 redis的出现,很大程度补偿了memcached这类key-value存储的不足,在部 分场合可以对关系数据库起到很好的补充作用。它提供了Python,Ruby,Erlang,PHP客户端,使用很方便。
1、redis 在主节点下线后,从节点会自动提升为主节点,提供服务
2、redis 宕机节点恢复后,自动会添加到集群中,变成从节点
缺点:
1、由于redis的复制使用异步机制,在自动故障转移的过程中,集群可能会丢失写命令。然而 redis 几乎是同时执行(将命令恢复发送给客户端,以及将命令复制到从节点)这两个操作,所以实际中,命令丢失的窗口非常小。
1. 按照我们一般的使用Redis的场景应该是这样的:
![](https://i-blog.csdnimg.cn/blog_migrate/21508d0c698e13953271c6099e7490d5.jpeg)
也就是说:我们会先去redis中判断数据是否存在,如果存在,则直接返回缓存好的数据。而如果不存在的话,就会去数据库中,读取数据,并把数据缓存到Redis中。
适用场合:如果数据量比较大,但不是经常更新的情况(比如用户排行)
2. 而第二种Redis的使用,跟第一种的情况完成不同,具体的情况请看:
![](https://i-blog.csdnimg.cn/blog_migrate/11e4276b6dbfd793b59d4b953e4c34f6.png)
这里我们会先去redis中判断数据是否存在,如果存在,则直接更新对应的数据(这一步会把对应更新过的key记录下来,比如也保存到redis中比如:key为:save_update_keys【用lpush列表记录】),并把更新后的数据返回给页面。而如果不存在的话,就会去先更新数据库中内容,然后把数据保存一份到Redis中。后面的工作:后台会有相关机制把Redis中的save_update_keys存储的key,分别读取出来,找到对应的数据,更新到DB中。
优点:这个流程的主要目的是把Redis当作数据库使用,更新获取数据比DB快。非常适合大数据量的频繁变动(比如微博)。
缺点:对Redis的依赖很大,要做好宕机时的数据保存。(不过可以使用redis的快照AOF,快速恢复的话,应该不会有多大影响,因为就算Redis不工作了,也不会影响后续数据的处理。)
难点:在前期规划key的格式,存储类型很重要,因为这会影响能否把数据同步到DB。
Redis提供了丰富的命令(command)对数据库和各种数据类型进行操作,这些command可以在Linux终端使用。在编程时,比如使用Redis 的Java语言包,这些命令都有对应的方法。下面将Redis提供的命令做一总结。
官网命令列表:http://redis.io/commands (英文)
1、连接操作相关的命令
- quit:关闭连接(connection)
- auth:简单密码认证
2、对value操作的命令
- exists(key):确认一个key是否存在
- del(key):删除一个key
- type(key):返回值的类型
- keys(pattern):返回满足给定pattern的所有key
- randomkey:随机返回key空间的一个key
- rename(oldname, newname):将key由oldname重命名为newname,若newname存在则删除newname表示的key
- dbsize:返回当前数据库中key的数目
- expire:设定一个key的活动时间(s)
- ttl:获得一个key的活动时间
- select(index):按索引查询
- move(key, dbindex):将当前数据库中的key转移到有dbindex索引的数据库
- flushdb:删除当前选择数据库中的所有key
- flushall:删除所有数据库中的所有key
3、对String操作的命令
- set(key, value):给数据库中名称为key的string赋予值value
- get(key):返回数据库中名称为key的string的value
- getset(key, value):给名称为key的string赋予上一次的value
- mget(key1, key2,…, key N):返回库中多个string(它们的名称为key1,key2…)的value
- setnx(key, value):如果不存在名称为key的string,则向库中添加string,名称为key,值为value
- setex(key, time, value):向库中添加string(名称为key,值为value)同时,设定过期时间time
- mset(key1, value1, key2, value2,…key N, value N):同时给多个string赋值,名称为key i的string赋值value i
- msetnx(key1, value1, key2, value2,…key N, value N):如果所有名称为key i的string都不存在,则向库中添加string,名称key i赋值为value i
- incr(key):名称为key的string增1操作
- incrby(key, integer):名称为key的string增加integer
- decr(key):名称为key的string减1操作
- decrby(key, integer):名称为key的string减少integer
- append(key, value):名称为key的string的值附加value
- substr(key, start, end):返回名称为key的string的value的子串
4、对List操作的命令
- rpush(key, value):在名称为key的list尾添加一个值为value的元素
- lpush(key, value):在名称为key的list头添加一个值为value的 元素
- llen(key):返回名称为key的list的长度
- lrange(key, start, end):返回名称为key的list中start至end之间的元素(下标从0开始,下同)
- ltrim(key, start, end):截取名称为key的list,保留start至end之间的元素
- lindex(key, index):返回名称为key的list中index位置的元素
- lset(key, index, value):给名称为key的list中index位置的元素赋值为value
- lrem(key, count, value):删除count个名称为key的list中值为value的元素。count为0,删除所有值为value的元素,count>0从头至尾删除count个值为value的元素,count<0从尾到头删除|count|个值为value的元素。 lpop(key):返回并删除名称为key的list中的首元素 rpop(key):返回并删除名称为key的list中的尾元素 blpop(key1, key2,… key N, timeout):lpop命令的block版本。即当timeout为0时,若遇到名称为key i的list不存在或该list为空,则命令结束。如果timeout>0,则遇到上述情况时,等待timeout秒,如果问题没有解决,则对keyi+1开始的list执行pop操作。
- brpop(key1, key2,… key N, timeout):rpop的block版本。参考上一命令。
- rpoplpush(srckey, dstkey):返回并删除名称为srckey的list的尾元素,并将该元素添加到名称为dstkey的list的头部
5、对Set操作的命令
- sadd(key, member):向名称为key的set中添加元素member
- srem(key, member) :删除名称为key的set中的元素member
- spop(key) :随机返回并删除名称为key的set中一个元素
- smove(srckey, dstkey, member) :将member元素从名称为srckey的集合移到名称为dstkey的集合
- scard(key) :返回名称为key的set的基数
- sismember(key, member) :测试member是否是名称为key的set的元素
- sinter(key1, key2,…key N) :求交集
- sinterstore(dstkey, key1, key2,…key N) :求交集并将交集保存到dstkey的集合
- sunion(key1, key2,…key N) :求并集
- sunionstore(dstkey, key1, key2,…key N) :求并集并将并集保存到dstkey的集合
- sdiff(key1, key2,…key N) :求差集
- sdiffstore(dstkey, key1, key2,…key N) :求差集并将差集保存到dstkey的集合
- smembers(key) :返回名称为key的set的所有元素
- srandmember(key) :随机返回名称为key的set的一个元素
6、对zset(sorted set)操作的命令
- zadd(key, score, member):向名称为key的zset中添加元素member,score用于排序。如果该元素已经存在,则根据score更新该元素的顺序。
- zrem(key, member) :删除名称为key的zset中的元素member
- zincrby(key, increment, member) :如果在名称为key的zset中已经存在元素member,则该元素的score增加increment;否则向集合中添加该元素,其score的值为increment
- zrank(key, member) :返回名称为key的zset(元素已按score从小到大排序)中member元素的rank(即index,从0开始),若没有member元素,返回“nil”
- zrevrank(key, member) :返回名称为key的zset(元素已按score从大到小排序)中member元素的rank(即index,从0开始),若没有member元素,返回“nil”
- zrange(key, start, end):返回名称为key的zset(元素已按score从小到大排序)中的index从start到end的所有元素
- zrevrange(key, start, end):返回名称为key的zset(元素已按score从大到小排序)中的index从start到end的所有元素
- zrangebyscore(key, min, max):返回名称为key的zset中score >= min且score <= max的所有元素 zcard(key):返回名称为key的zset的基数 zscore(key, element):返回名称为key的zset中元素element的score zremrangebyrank(key, min, max):删除名称为key的zset中rank >= min且rank <= max的所有元素 zremrangebyscore(key, min, max) :删除名称为key的zset中score >= min且score <= max的所有元素
- zunionstore / zinterstore(dstkeyN, key1,…,keyN, WEIGHTS w1,…wN, AGGREGATE SUM|MIN|MAX):对N个zset求并集和交集,并将最后的集合保存在dstkeyN中。对于集合中每一个元素的score,在进行AGGREGATE运算前,都要乘以对于的WEIGHT参数。如果没有提供WEIGHT,默认为1。默认的AGGREGATE是SUM,即结果集合中元素的score是所有集合对应元素进行SUM运算的值,而MIN和MAX是指,结果集合中元素的score是所有集合对应元素中最小值和最大值。
7、对Hash操作的命令
- hset(key, field, value):向名称为key的hash中添加元素field<—>value
- hget(key, field):返回名称为key的hash中field对应的value
- hmget(key, field1, …,field N):返回名称为key的hash中field i对应的value
- hmset(key, field1, value1,…,field N, value N):向名称为key的hash中添加元素field i<—>value i
- hincrby(key, field, integer):将名称为key的hash中field的value增加integer
- hexists(key, field):名称为key的hash中是否存在键为field的域
- hdel(key, field):删除名称为key的hash中键为field的域
- hlen(key):返回名称为key的hash中元素个数
- hkeys(key):返回名称为key的hash中所有键
- hvals(key):返回名称为key的hash中所有键对应的value
- hgetall(key):返回名称为key的hash中所有的键(field)及其对应的value
8、持久化
- save:将数据同步保存到磁盘
- bgsave:将数据异步保存到磁盘
- lastsave:返回上次成功将数据保存到磁盘的Unix时戳
- shundown:将数据同步保存到磁盘,然后关闭服务
9、远程服务控制
- info:提供服务器的信息和统计
- monitor:实时转储收到的请求
- slaveof:改变复制策略设置
- config:在运行时配置Redis服务器
安装及配置脚本:
点击(此处)折叠或打开
- #!/bin/bash
- #
- if [[ $1 == "" ]];then
- echo "Usage: $0 ipaddr***"
- exit 0
- fi
- masterIp=$1
- cd /usr/local/src/
- tar -zxvf redis-2.6.12.tar.gz
- cd /usr/local/src/redis-2.6.12
- make && make install
- mkdir -p /etc/redis/
- mkdir -p /export/data/redis_data/6379
- mkdir -p /export/data/redis_data/6380
- sed -i "s/127.0.0.1/$masterIp/g" /usr/local/src/6380.conf
- cp -rf /usr/local/src/6379.conf /etc/redis/
- cp -rf /usr/local/src/6380.conf /etc/redis/
- cat >> /etc/sysctl.conf <<eof <="" li="" style="word-wrap: break-word;">
- net.core.somaxconn = 32768
- net.core.wmem_default = 8388608
- net.core.rmem_default = 8388608
- net.core.rmem_max = 16777216
- net.core.wmem_max = 16777216
- net.ipv4.tcp_timestamps = 0
- net.ipv4.tcp_synack_retries = 1
- net.ipv4.tcp_syn_retries = 0
- net.ipv4.tcp_tw_recycle = 1
- net.ipv4.tcp_tw_reuse = 1
- net.ipv4.tcp_mem = 94500000 915000000 927000000
- net.ipv4.tcp_max_orphans = 3276800
- net.ipv4.ip_local_port_range = 1024 65535
- net.ipv4.tcp_fin_timeout = 10
- net.ipv4.tcp_keepalive_time = 100
- net.ipv4.tcp_syncookies = 1
- net.ipv4.tcp_max_syn_backlog = 8192
- net.ipv4.tcp_max_tw_buckets = 20000
- EOF
- modprobe bridge
- sysctl -p
- /usr/local/bin/redis-server /etc/redis/6379.conf
- /usr/local/bin/redis-server /etc/redis/6380.conf
6379.sh
6380.sh点击(此处)折叠或打开
- daemonize yes
- pidfile /var/run/redis_6379.pid
- port 6379
- timeout 300
- tcp-keepalive 0
- loglevel notice
- #logfile stdout
- databases 16
- unixsocketperm 0
- maxmemory 2000000000
- maxmemory-policy volatile-lru
- maxmemory-samples 3
- maxclients 10000
- #save 900 1
- #save 300 10
- #save 60 10000
- stop-writes-on-bgsave-error yes
- rdbcompression yes
- rdbchecksum yes
- dbfilename dump.rdb
- dir /export/data/redis_data/6379
- slave-serve-stale-data yes
- slave-read-only yes
- repl-ping-slave-period 10
- repl-timeout 60
- repl-disable-tcp-nodelay no
- slave-priority 100
- appendonly no
- appendfsync everysec
- no-appendfsync-on-rewrite no
- auto-aof-rewrite-percentage 100
- auto-aof-rewrite-min-size 64mb
- lua-time-limit 5000
- slowlog-log-slower-than 10000
- slowlog-max-len 1024
- hash-max-ziplist-entries 512
- hash-max-ziplist-value 64
- list-max-ziplist-entries 512
- list-max-ziplist-value 64
- set-max-intset-entries 512
- zset-max-ziplist-entries 128
- zset-max-ziplist-value 64
- activerehashing yes
- client-output-buffer-limit normal 0 0 0
- client-output-buffer-limit slave 256mb 64mb 60
- client-output-buffer-limit pubsub 32mb 8mb 60
- hz 10
点击(此处)折叠或打开
- daemonize yes
- pidfile /var/run/redis_6380.pid
- port 6380
- timeout 300
- tcp-keepalive 0
- loglevel notice
- #logfile stdout
- databases 16
- unixsocketperm 0
- maxmemory 2000000000
- maxmemory-policy volatile-lru
- maxmemory-samples 3
- maxclients 10000
- #save 900 1
- #save 300 10
- #save 60 10000
- stop-writes-on-bgsave-error yes
- rdbcompression yes
- rdbchecksum yes
- dbfilename dump.rdb
- dir /export/data/redis_data/6380
- slaveof 127.0.0.1 6379
- slave-serve-stale-data yes
- slave-read-only yes
- repl-ping-slave-period 10
- repl-timeout 60
- repl-disable-tcp-nodelay no
- slave-priority 100
- appendonly yes
- appendfsync everysec
- no-appendfsync-on-rewrite no
- auto-aof-rewrite-percentage 100
- auto-aof-rewrite-min-size 64mb
- lua-time-limit 5000
- slowlog-log-slower-than 10000
- slowlog-max-len 1024
- hash-max-ziplist-entries 512
- hash-max-ziplist-value 64
- list-max-ziplist-entries 512
- list-max-ziplist-value 64
- set-max-intset-entries 512
- zset-max-ziplist-entries 128
- zset-max-ziplist-value 64
- activerehashing yes
- client-output-buffer-limit normal 0 0 0
- client-output-buffer-limit slave 256mb 64mb 60
- client-output-buffer-limit pubsub 32mb 8mb 60
- hz 10
Agent`K 最近在使用Redis,忽然发现以前很多费神的事情都迎刃而解了,又应了经典:我们要做的99%的事情,别人都早已做过了!
(扫盲:Redis是内存型、键值对型数据库,独立运行,不是第三方库)
排行榜
游戏服务器中涉及到很多排行信息,比如玩家等级排名、金钱排名、战斗力排名等。
一般情况下仅需要取排名的前N名就可以了,这时可以利用数据库的排序功能,或者自己维护一个元素数量有限的top集合。
但是有时候我们需要每一个玩家的排名,玩家的数量太多,不能利用数据库(全表排序压力太大),自己维护也会比较麻烦。
使用Redis可以很好的解决这个问题。它提供的有序Set,支持每个键值(比如玩家id)拥有一个分数(score),每次往这个set里添加元素,
Redis会对其进行排序,修改某一元素的score后,也会更新排序,在获取数据时,可以指定排序范围。
更重要的是,这个排序结果会被保存起来,不用在服务器启动时重新计算。
通过它,排行榜的实时刷新、全服排行都不再成为麻烦事。
消息队列(可跨服)
Redis提供的List数据类型,可以用来实现一个消息队列。
由于它是独立于游戏服务器的,所以多个游戏服务器可以通过它来交换数据、发送事件。
Redis还提供了发布、订阅的事件模型。
利用这些,我们就不必自己去实现一套服务器间的通信框架,方便地实现服务器组。
数据库缓存
Redis提供了较为丰富数据类型,使我们可以更为容易地将数据对象缓存起来(序列化、protobuffer)。
当需要请求某一数据时,先从Redis中查找,如果没有再查数据库,同时交给Redis缓存起来。
当对数据进行修改时,则先将修改后的数据保存到Redis,然后保存至数据库(2)。
第2步可以有另外的思路:
A不实时保存到数据库,而是交由另外的线程(甚至是专门的程序)去保存,以提高逻辑层的响应速度。
B部分数据交给Redis保存(Reids自身有持久化功能),像玩家已经完成过的任务ID集合,利用Redis的Set类型保存更为合适。
C玩家瞬时变化的数据不见得每次修改都需要保存(比如金钱、经验),但如果游戏服务器自己维护在内存中,出现宕机就会导致回档。
Redis是独立于游戏服务器的,交由它来保存,可以防止宕机回档的问题,也可以减少游戏服务器自己维护数据所占用的内存。
Redis Cluster还具有一些其他方面的不同,譬如它不支持多个数据库,不支持select命令等等,但其中最大的不同还是Redis Cluster不支持复杂的多主键操作。关于这一点,Redis Cluster的官方文档有这样一段描述,现摘录如下:Commands performing complex multi key operations like Set type unions or intersections are not implemented, and in general all the operations where in theory keys are not available in the same node are not implemented。由这段描述可知,在Redis Cluster中任何跨节点的命令都是不允许的,但是落实到具体的系统实现中,Redis Cluster又是如何去处理这些命令的呢?我们不妨通过两种典型的命令来验证一下,其中一种是MULTI/EXEC事务命令,该命令可以将多条Redis命令一次执行,所以这类命令可能同时操作多个主键,另外一种是本身就支持多主键的命令如mset和mget等。下面,本博主就在之前搭建的Redis Cluster上,分别尝试了以上两类命令的不同执行场景,以此来感性地认识Redis Cluster的反应!情景一:MULTI/EXEC事务中的所有命令均操作相同的主键,且该主键就在当前连接的Redis节点上[root@compute-09-00 ~]# /usr/local/redis/bin/redis-cliredis 127.0.0.1:6379> multiOKredis 127.0.0.1:6379> set hello worldQUEUEDredis 127.0.0.1:6379> set hello earthQUEUEDredis 127.0.0.1:6379> set hello chinaQUEUEDredis 127.0.0.1:6379> exec1) OK2) OK3) OK执行结果:事务中的每条命令都可以正确执行!情景二:MULTI/EXEC事务中的所有命令均操作相同的主键,但该主键不在当前连接的Redis节点上redis 127.0.0.1:6379> multiOKredis 127.0.0.1:6379> get foo(error) MOVED 12182 192.168.32.4:6379redis 127.0.0.1:6379> set foo bar(error) MOVED 12182 192.168.32.4:6379redis 127.0.0.1:6379> exec(empty list or set)执行结果:事务中的任何命令都无法执行,对于每条命令均返回MOVED信息!情景三:MULTI/EXEC事务中的所有命令操作不同的主键,且某些主键不在当前连接的Redis节点上redis 127.0.0.1:6379> multiOKredis 127.0.0.1:6379> set hello americaQUEUEDredis 127.0.0.1:6379> set foo rab(error) MOVED 12182 192.168.32.4:6379redis 127.0.0.1:6379> exec1) OKredis 127.0.0.1:6379> get hello"america"执行结果:主键在当前连接的Redis节点上的命令可以正确执行,主键不在当前连接的Redis节点上的命令返回MOVED信息!情景四:MULTI/EXEC事务中的所有命令操作不同的主键,且所有主键均在当前连接的Redis节点上redis 192.168.32.3:6379> multiOKredis 192.168.32.3:6379> set id 10003QUEUEDredis 192.168.32.3:6379> set number 20004QUEUEDredis 192.168.32.3:6379> set student 30098QUEUEDredis 192.168.32.3:6379> exec1) OK2) OK3) OK执行结果:事务中的所有命令均可以成功执行!情景五:Multiple主键命令包含了不同的主键,且所有主键均在当前连接的Redis节点上redis 192.168.32.3:6379> mset id 10004 number 20005 student 30099(error) ERR Multi keys request invalid in clusterredis 192.168.32.3:6379> mget id number student(error) ERR Multi keys request invalid in cluster执行结果:命令无效,无法执行!情景六:Multiple主键命令包含了多个相同的主键,且该主键在当前连接的Redis节点上redis 192.168.32.3:6379> mset id 10004 id 20005 id 30099OKredis 192.168.32.3:6379> mget id id id1) "30099"2) "30099"3) "30099"执行结果:命令有效,成功执行!情景七:Multiple主键命令包含了多个相同的主键,但该主键不在当前连接的Redis节点上redis 192.168.32.3:6379> mset hello world hello earth hello universe(error) MOVED 866 192.168.32.2:6379redis 192.168.32.3:6379> mget hello hello hello(error) MOVED 866 192.168.32.2:6379执行结果:命令有效,返回MOVED信息!基于以上执行结果,我们可以知道对于MULTI/EXEC事务来说,执行效果与逐条处理单个命令一样,被事务包裹的Redis命令连接节点能够处理就处理,无法处理的就返回MOVED信息。但是对于Multiple主键的命令来说,只要在命令中包含了多个不同的主键,那么无论这些主键能不能被连接节点所处理,命令都是无效的!当然,以上只是通过一些具体的命令执行实例来看Redis Cluster的执行效果,如果想对Redis Cluster的命令处理有更详细的了解,那么推荐的方法还是去看源码中的cluster.c文件。