分布式缓存Redis使用心得

我的未来我决定 提交于 2019-12-25 13:32:51

一、缓存在系统中用来做什么

 

1. 少量数据存储,高速读写访问。通过数据全部in-momery 的方式来保证高速访问,同时提供数据落地的功能,实际这正是Redis最主要的适用场景。

 

2. 海量数据存储,分布式系统支持,数据一致性保证,方便的集群节点添加/删除。Redis3.0以后开始支持集群,实现了半自动化的数据分片,不过需要smart-client的支持。

 

二、从不同的角度来详细介绍redis

 

网络模型:Redis使用单线程的IO复用模型,自己封装了一个简单的AeEvent事件处理框架,主要实现了epoll、kqueue和select,对于单纯只有IO操作来说,单线程可以将速度优势发挥到最大,但是Redis也提供了一些简单的计算功能,比如排序、聚合等,对于这些操作,单线程模型实际会严重影响整体吞吐量,CPU计算过程中,整个IO调度都是被阻塞住的。

 

内存管理:Redis使用现场申请内存的方式来存储数据,并且很少使用free-list等方式来优化内存分配,会在一定程度上存在内存碎片,Redis跟据存储命令参数,会把带过期时间的数据单独存放在一起,并把它们称为临时数据,非临时数据是永远不会被剔除的,即便物理内存不够,导致swap也不会剔除任何非临时数据(但会尝试剔除部分临时数据),这点上Redis更适合作为存储而不是cache。

 

数据一致性问题:在一致性问题上,个人感觉redis没有memcached实现的好,Memcached提供了cas命令,可以保证多个并发访问操作同一份数据的一致性问题。 Redis没有提供cas 命令,并不能保证这点,不过Redis提供了事务的功能,可以保证一串命令的原子性,中间不会被任何操作打断。

 

支持的KEY类型:Redis除key/value之外,还支持list,set,sorted set,hash等众多数据结构,提供了KEYS进行枚举操作,但不能在线上使用,如果需要枚举线上数据,Redis提供了工具可以直接扫描其dump文件,枚举出所有数据,Redis还同时提供了持久化和复制等功能。

 

客户端支持:redis官方提供了丰富的客户端支持,包括了绝大多数编程语言的客户端,比如我此次测试就选择了官方推荐了Java客户端Jedis.里面提供了丰富的接口、方法使得开发人员无需关系内部的数据分片、读取数据的路由等,只需简单的调用即可,非常方便。

 

数据复制:从2.8开始,Slave会周期性(每秒一次)发起一个Ack确认复制流(replication stream)被处理进度, Redis复制工作原理详细过程如下:

 

 

1. 如果设置了一个Slave,无论是第一次连接还是重连到Master,它都会发出一个SYNC命令;

 

2. 当Master收到SYNC命令之后,会做两件事:

a) Master执行BGSAVE:后台写数据到磁盘(rdb快照);

b) Master同时将新收到的写入和修改数据集的命令存入缓冲区(非查询类);

 

3. 当Master在后台把数据保存到快照文件完成之后,Master会把这个快照文件传送给Slave,而Slave则把内存清空后,加载该文件到内存中;

 

4. 而Master也会把此前收集到缓冲区中的命令,通过Reids命令协议形式转发给Slave,Slave执行这些命令,实现和Master的同步;

 

5. Master/Slave此后会不断通过异步方式进行命令的同步,达到最终数据的同步一致;

 

6. 需要注意的是Master和Slave之间一旦发生重连都会引发全量同步操作。但在2.8之后,也可能是部分同步操作。

 

2.8开始,当Master和Slave之间的连接断开之后,他们之间可以采用持续复制处理方式代替采用全量同步。

 

Master端为复制流维护一个内存缓冲区(in-memory backlog),记录最近发送的复制流命令;同时,Master和Slave之间都维护一个复制偏移量(replication offset)和当前Master服务器ID(Masterrun id)。

 

当网络断开,Slave尝试重连时:

a. 如果MasterID相同(即仍是断网前的Master服务器),并且从断开时到当前时刻的历史命令依然在Master的内存缓冲区中存在,则Master会将缺失的这段时间的所有命令发送给Slave执行,然后复制工作就可以继续执行了;

b. 否则,依然需要全量复制操作。

 

读写分离:redis支持读写分离,而且使用简单,只需在配置文件中把redis读服务器和写服务器进行配置,多个服务器使用逗号分开如下:

 

 

1. 一个节点可以通过向另一个节点发送 MEET 信息,来强制让接收信息的节点承认发送信息的节点为集群中的一份子。 一个节点仅在管理员显式地向它发送CLUSTER MEET ipport 命令时, 才会向另一个节点发送 MEET 信息。

 

2. 如果一个可信节点向另一个节点传播第三者节点的信息, 那么接收信息的那个节点也会将第三者节点识别为集群中的一份子。也即是说, 如果 A 认识 B , B 认识 C , 并且 B 向 A 传播关于 C 的信息, 那么 A 也会将 C 识别为集群中的一份子, 并尝试连接 C 。

 

这意味着如果我们将一个/一些新节点添加到一个集群中, 那么这个/这些新节点最终会和集群中已有的其他所有节点连接起来。

这说明只要管理员使用 CLUSTER MEET 命令显式地指定了可信关系,集群就可以自动发现其他节点。这种节点识别机制通过防止不同的 Redis 集群因为 IP 地址变更或者其他网络事件的发生而产生意料之外的联合(mix), 从而使得集群更具健壮性。当节点的网络连接断开时,它会主动连接其他已知的节点。

 

MOVED 转向:

 

一个 Redis 客户端可以向集群中的任意节点(包括从节点)发送命令请求。节点会对命令请求进行分析, 如果该命令是集群可以执行的命令, 那么节点会查找这个命令所要处理的键所在的槽。如果要查找的哈希槽正好就由接收到命令的节点负责处理,那么节点就直接执行这个命令。另一方面, 如果所查找的槽不是由该节点处理的话, 节点将查看自身内部所保存的哈希槽到节点 ID 的映射记录,并向客户端回复一个 MOVED 错误。

 

即使客户端在重新发送 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 转向错误。

 

集群在线重配置:

 

Redis 集群支持在集群运行的过程中添加或者移除节点。实际上, 节点的添加操作和节点的删除操作可以抽象成同一个操作,那就是, 将哈希槽从一个节点移动到另一个节点:添加一个新节点到集群, 等于将其他已存在节点的槽移动到一个空白的新节点里面。从集群中移除一个节点, 等于将被移除节点的所有槽移动到集群的其他节点上面去。

 

因此, 实现Redis 集群在线重配置的核心就是将槽从一个节点移动到另一个节点的能力。 因为一个哈希槽实际上就是一些键的集合, 所以 Redis 集群在重哈希(rehash)时真正要做的, 就是将一些键从一个节点移动到另一个节点。

 

要理解Redis 集群如何将槽从一个节点移动到另一个节点, 我们需要对 CLUSTER 命令的各个子命令进行介绍,这些命理负责管理集群节点的槽转换表(slots translation table)。

 

以下是CLUSTER 命令可用的子命令:

 

 

向节点 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 。

 

键的移动操作由以下两个命令执行:

 

 

CLUSTERGETKEYSINSLOT slot count

上面的命令会让节点返回 count 个 slot 槽中的键, 对于命令所返回的每个键, redis-trib 都会向节点 A 发送一条 MIGRATE 命令, 该命令会将所指定的键原子地(atomic)从节点 A 移动到节点 B (在移动键期间,两个节点都会处于阻塞状态,以免出现竞争条件)。

 

以下为MIGRATE 命令的运作原理:

 

MIGRATEtarget_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)如下:

 

1. 如果客户端接收到 ASK 转向, 那么将命令请求的发送对象调整为转向所指定的节点。

2. 先发送一个 ASKING 命令,然后再发送真正的命令请求。

3. 不必更新客户端所记录的槽 8 至节点的映射: 槽 8 应该仍然映射到节点 A , 而不是节点 B 。

 

一旦节点 A 针对槽 8 的迁移工作完成, 节点 A 在再次收到针对槽 8 的命令请求时, 就会向客户端返回 MOVED 转向, 将关于槽 8 的命令请求长期地转向到节点 B 。

 

注意, 即使客户端出现 Bug , 过早地将槽 8 映射到了节点 B 上面, 但只要这个客户端不发送 ASKING 命令, 客户端发送命令请求的时候就会遇上 MOVED 错误, 并将它转向回节点 A 。



容错:

节点失效检测,以下是节点失效检查的实现方法:

 

1. 当一个节点向另一个节点发送 PING 命令, 但是目标节点未能在给定的时限内返回 PING 命令的回复时, 那么发送命令的节点会将目标节点标记为 PFAIL(possible failure,可能已失效)。等待 PING 命令回复的时限称为“节点超时时限(node timeout)”, 是一个节点选项(node-wise setting)。

 

2. 每次当节点对其他节点发送 PING 命令的时候,它都会随机地广播三个它所知道的节点的信息, 这些信息里面的其中一项就是说明节点是否已经被标记为 PFAIL或者 FAIL 。

 

当节点接收到其他节点发来的信息时, 它会记下那些被其他节点标记为失效的节点。这称为失效报告(failure report)。

 

3. 如果节点已经将某个节点标记为 PFAIL , 并且根据节点所收到的失效报告显式,集群中的大部分其他主节点也认为那个节点进入了失效状态, 那么节点会将那个失效节点的状态标记为 FAIL 。

 

4. 一旦某个节点被标记为 FAIL , 关于这个节点已失效的信息就会被广播到整个集群,所有接收到这条信息的节点都会将失效节点标记为 FAIL 。

 

简单来说, 一个节点要将另一个节点标记为失效, 必须先询问其他节点的意见, 并且得到大部分主节点的同意才行。因为过期的失效报告会被移除,所以主节点要将某个节点标记为 FAIL 的话, 必须以最近接收到的失效报告作为根据。

 

从节点选举:一旦某个主节点进入 FAIL 状态, 如果这个主节点有一个或多个从节点存在,那么其中一个从节点会被升级为新的主节点, 而其他从节点则会开始对这个新的主节点进行复制。

新的主节点由已下线主节点属下的所有从节点中自行选举产生,以下是选举的条件:

 

1. 这个节点是已下线主节点的从节点。

2. 已下线主节点负责处理的槽数量非空。

3. 从节点的数据被认为是可靠的, 也即是, 主从节点之间的复制连接(replication link)的断线时长不能超过节点超时时限(nodetimeout)乘以REDIS_CLUSTER_SLAVE_VALIDITY_MULT 常量得出的积。

 

如果一个从节点满足了以上的所有条件, 那么这个从节点将向集群中的其他主节点发送授权请求, 询问它们,是否允许自己(从节点)升级为新的主节点。

 

如果发送授权请求的从节点满足以下属性, 那么主节点将向从节点返FAILOVER_AUTH_GRANTED 授权, 同意从节点的升级要求:

 

 

1. 发送授权请求的是一个从节点, 并且它所属的主节点处于 FAIL状态。

2. 在已下线主节点的所有从节点中, 这个从节点的节点 ID 在排序中是最小的。

3. 这个从节点处于正常的运行状态: 它没有被标记为 FAIL 状态,也没有被标记为 PFAIL 状态。

 

一旦某个从节点在给定的时限内得到大部分主节点的授权,它就会开始执行以下故障转移操作:

 

 

1. 通过 PONG 数据包(packet)告知其他节点, 这个节点现在是主节点了。

2. 通过 PONG 数据包告知其他节点, 这个节点是一个已升级的从节点(promoted slave)。

3. 接管(claiming)所有由已下线主节点负责处理的哈希槽。

4. 显式地向所有节点广播一个 PONG 数据包, 加速其他节点识别这个节点的进度,而不是等待定时的 PING / PONG 数据包。

 

所有其他节点都会根据新的主节点对配置进行相应的更新:

 

  • 所有被新的主节点接管的槽会被更新。

  • 已下线主节点的所有从节点会察觉到 PROMOTED 标志,并开始对新的主节点进行复制。

  • 如果已下线的主节点重新回到上线状态, 那么它会察觉到PROMOTED 标志, 并将自身调整为现任主节点的从节点。

 

在集群的生命周期中, 如果一个带有 PROMOTED 标识的主节点因为某些原因转变成了从节点,那么该节点将丢失它所带有的 PROMOTED 标识。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!