一般我们在使用 Redis 时,鉴于单机存在的单点故障,容量有限,高并发压力问题,都不会采用单机模式,那么该如何设计 Redis 的部署方式来解决诸如单点故障,容量有限,高并发压力这样的问题呢?
首先来看下单点故障的问题,单点故障一般就是指提供服务的节点或实例只有一个,当这个节点出现故障就导致这个服务不可用。解决这种问题一般会引入主备或主从的概念。主备模式就是主机向外提供服务,备机从主机同步数据,只有当主机出现故障时,备机才代替主机向外提供服务。主从模式下,从节点从主节点同步数据,同时也提供部分服务(对于主从模式的 Redis,从节点一般只提供读服务 )。这两种方案都可以很好的解决单点故障的问题,主从模式甚至还可以解决访问压力的问题。
上面提到的方案,都没有解决容量受限的问题,当系统的数据随着业务的发展快速增长的时候,我们必须解决容量的问题,那么提升单机的内存容量是否可行呢?一般来说,我们不会采用这种方式,因为单机内存的提升毕竟有限,另外单节点数据量的提升,势必会降低 Redis 的性能。既然这种方式并不是很好的方案,那只能一变多,单实例拆分成多实例了,到底该如何进行拆分呢?这里就要了解一下微服务的拆分原则之 AKF 原则。上面提到的主备或者主从其实就对应 AKF 拆分原则的 X 轴方向上的拆分。对 Redis 来说,X 轴方向的拆分只能解决单点故障和访问压力的问题。要解决容量有限的问题,必须进行 Y 轴及 Z 轴方向的拆分。先按照 Y 轴方向拆分,我们可以根据业务模块对数据进行划分,每个模块的数据放在一个 Redis 实例中,客户端可以根据要查询的数据所属的业务模块进行路由查询。
按照 Y 轴方向拆分还存在一个问题,如果某个业务模块的数据随着业务的发展而成比例或者指数级增长,存放这部分业务数据的实例也会出现容量受限的问题,这时我们就要进行 Z 轴方向的拆分,即数据分片。X,Y,Z 轴方向的拆分还可以进行随意的组合来解决单机故障,容量及访问压力的问题。X 方向–数据镜像解决单点故障;Y&Z 或者 Y/Z 方向–数据拆分解决访问压力及容量问题。
接下来我们重点聊一下 Redis 的数据分片(分区)。Redis 的数据分区整体上可以分为两种,一种范围分区,另一种散列分区。范围分区,举个例子,可以根据用户的 id 所在区间,将用户所关联的数据分配到不同的 Redis 实例,比如用户 id 在 0-9999 之间的用户关联的数据分配到 Redis 实例 1 上,用户 id 在 10000-19999 之间的用户所关联的数据分配到实例 2 上,以此类推。这种分区方案的维护成本比较高,需要专门的张表来维护映射关系,而且维护的成本随着业务的复杂度成比例增加。散列分区要比范围分区高效得多,它只需要对数据的 key 进行一次哈希运算,根据具体方案的不同进行不同的计算即可得到 key 应在的实例。常用的散列分区方案一般有两种:哈希取模和一致性哈希。
哈希取模就是将前面提到的哈希运算得到的数值,对 Redis 实例数取模,便得到数据应在的节点位置。比如一个 key 哈希运算后得到的值是 93024922,Redis 实例数为 4,那么这个 key 应在的节点位置为 93024922 % 4 = 2,即第三个节点上。了解了哈希取模的实现之后,我们可以发现,这种方案分布式下的扩展性不好,如果最初我们采用 5 个实例进行数据分区,后面需要增加实例数,就需要将所有的数据进行重新分区,在数据量大的情况下,这将是非常耗时的操作,这样也同时破坏了可用性。
一致性哈希同样是对 key 先进性哈希运算得到一个散列值 h0,同时对每个节点节点的唯一标识(如 ip)也采用相同的散列函数,得到哈希值 hx,将 h0 依次同每个节点得到的哈希值进行比较,得到哈希值大于 h0 的最小的节点位置。比如 h0 为 5,hx 的值依次为 1,3,6,8,10,最终这个 key 会被分配到 hx 为 6 的实例。一致性哈希是分布式应用常用的算法,这里不做过多详细介绍。在增加节点时,一致性哈希不需要将全部数据重新计算,分区,增加节点只会影响到与新节点相邻的下一个节点的数据分布。
介绍完了数据分区的方案之后,还有一个问题需要考虑:数据分区的实现方式。一种比较原始的方式就是客户端自己取实现分区逻辑,即客户端分区。这种方式增加了客户端代码的复杂度,不过却方便开发人员进行调试与调优。通过下图我们可以发现,每个客户端都要与每个 Redis 实例至少建立一个连接,不管查询的数据在那个实例。这样当大量并发请求过来时,很容易导致服务端压力过大而影响性能。
要解决客户端分区方式存在的问题,只需要在客户端与服务端之间增加一个代理层,这种方式被称为代理分区,采用这种方式,后端服务层的架构对客户端来说是透明的,客户端只需要与代理层建立连接即可,分片的逻辑由代理层取实现。目前有较多开源的解决方案,比如推特的 twemproxy,国内开源的 predixy,豌豆荚的 codis。
除了上面两种分区的方式,第三种是查询路由方式。采用这种方式时,客户端可以与集群中的任意一个节点建立连接并操作数据,如果要操作的数据碰巧就在这个节点上,直接处理并返回即可,如果不在这个节点,则将此数据所在的节点信息返回客户端,客户端重定向到目标节点进行数据操作(如下图)。这便是 Redis-Cluster 的工作方式。Redis-Cluster 采用预分区的方式降低了数据迁移的成本。Redis-Cluster 将数据提前分到 16384 个槽位中,数据对应的槽位是固定的,每个节点管理的槽位可以进行动态分配,这样很方便的实现了在线扩缩容。
由于哈希取模和一致性哈希的方案在扩缩容时,存在数据丢失或者数据迁移的成本较高,一般我们只有在使用 Redis 作为缓存时才采用,增加节点时不进行数据迁移,出现击穿时从数据库查询一下再写入缓存即可,对于应该迁移走的数据,可以采用淘汰策略进行冷数据的淘汰。在使用 Redis 作为数据库时,一般只能选择 Redis-Cluster 作为分区方式,这样可以保证运行时的数据再平衡。
参考资料:分区:怎样将数据分布到多个redis实例