Redis 笔记十

Posted by MatthewHan on 2021-07-22

Q:Redis主从同步与故障切换,有哪些坑?

故障一:主从数据不一致

因为主从库间的命令复制是异步进行的。

一方面,主从库间的网络可能会有传输延迟,所以从库不能及时地收到主库发送的命令,从库上执行同步命令的时间就会被延后。

另一方面,即使从库及时收到了主库的命令,但是,也可能会因为正在处理其它复杂度高的命令(例如集合操作命令)而阻塞。

解决方法

首先,**在硬件环境配置方面,我们要尽量保证主从库间的网络连接状况良好。**例如,我们要避免把主从库部署在不同的机房,或者是避免把网络通信密集的应用(例如数据分析应用)和 Redis 主从库部署在一起。

另外,我们还可以开发一个外部程序来监控主从库间的复制进度。

如果某个从库的进度差值大于我们预设的阈值,我们可以让客户端不再和这个从库连接进行数据读取,这样就可以减少读到不一致数据的情况。不过,为了避免出现客户端和所有从库都不能连接的情况,我们需要把复制进度差值的阈值设置得大一些。

故障二:读取过期数据

这个问题主要发生在从库读取数据。

首先,我们都知道 Redis 同时使用了两种策略来删除过期的数据,分别是惰性删除策略和定期删除策略。

其中定期删除策略是指,Redis 每隔一段时间(默认 100ms),就会随机选出一定数量的数据,检查它们是否过期,并把其中过期的数据删除,这样就可以及时释放一些内存。

因为从库无法删除数据,所以读到过期的数据之后,不会采用惰性删除,会导致返回过期的数据。但是只是 Redis 3.2 之前的版本,在 3.2 版本后,Redis 做了改进,如果读取的数据已经过期了,从库虽然不会删除,但是会返回空值。

即使使用了 Redis 3.2 后的版本,还是会出现读到过期数据的情况

因为 Redis 的过期分为两种

  • EXPIRE 和 PEXPIRE:它们给数据设置的是从命令执行时开始计算的存活时间;
  • EXPIREAT 和 PEXPIREAT:它们会直接把数据的过期时间设置为具体的一个时间点。

我们分开来看,**如果设定的是 xx 秒/天 之类的过期,就会因为网络拥塞、延迟导致过期时间延后。**主从库全量同步花费了 2 分钟才完成。等从库开始执行这条命令时,时间已经是 9 点 2 分了。而 EXPIRE 命令是把 testkey 的过期时间设置为当前时间的 60s 后,也就是 9 点 3 分。如果客户端在 9 点 2 分 30 秒时在从库上读取 testkey,仍然可以读到 testkey 的值。但是,testkey 实际上已经过期了。

这也太拉胯了。。。讲道理从库同步主库数据(有需要过期的 kv)应该改成同步他的过期时间,而不是时长。

如果设定的是具体的过期日期,也会因为和主库的时钟不一致,从而产生问题。

所以应对过期 key 的问题,需要:

  1. 保证良好网络环境,以及使用程序监控从库复制进度,一旦从库复制进度超过阈值,不让客户端连接从库。
  2. 使用 Redis 3.2 及以上版本;尽量使用 EXPIREAT/PEXPIREAT 命令设置过期时间,避免从库上的数据过期时间滞后。
  3. 另外,主从节点上的时钟要保持一致,具体的做法是,让主从节点和相同的 NTP 服务器(时间服务器)进行时钟同步。

故障三:不合理配置项导致的服务挂掉

protected-mode 配置项

这个配置项的作用是限定哨兵实例能否被其他服务器访问。当这个配置项设置为 yes 时,哨兵实例只能在部署的服务器本地进行访问。当设置为 no 时,其他服务器也可以访问这个哨兵实例。

正因为这样,如果 protected-mode 被设置为 yes,而其余哨兵实例部署在其它服务器,那么,这些哨兵实例间就无法通信。当主库故障时,哨兵无法判断主库下线,也无法进行主从切换,最终 Redis 服务不可用。

**所以,我们在应用主从集群时,要注意将 protected-mode 配置项设置为 no,并且将 bind 配置项设置为其它哨兵实例的 IP 地址。**这样一来,只有在 bind 中设置了 IP 地址的哨兵,才可以访问当前实例,既保证了实例间能够通信进行主从切换,也保证了哨兵的安全性。

cluster-node-timeout 配置项

这个配置项设置了 Redis Cluster 中实例响应心跳消息的超时时间。

如果执行主从切换的实例超过半数,而主从切换时间又过长的话,就可能有半数以上的实例心跳超时,从而可能导致整个集群挂掉。所以,我建议你将 cluster-node-timeout 调大些(例如 10 到 20 秒)。

小结

img

Q:我们把 slave-read-only 设置为 no,让从库也能直接删除数据,以此来避免读到过期数据,你觉得,这是一个好方法吗?

A:不太好,因为增加一个特性是为了解决 bug,但是采用从库也能写数据的方式无疑是会增加 bug,还是会产生数据不一致的情况。。

Q:Redis支撑秒杀场景的关键技术和实践都有哪些

秒杀场景的负载特征对支撑系统的要求

第一个特征是瞬时并发访问量非常高。

第二个特征是读多写少,而且读操作是简单的查询操作。

Redis 可以在秒杀场景的哪些环节发挥作用?

我们一般可以把秒杀活动分成三个阶段。在每一个阶段,Redis 所发挥的作用也不一样。第一阶段是秒杀活动前。在这个阶段,用户会不断刷新商品详情页,这会导致详情页的瞬时请求量剧增。这个阶段的应对方案,一般是尽量把商品详情页的页面元素静态化,然后使用 CDN 或是浏览器把这些静态化的元素缓存起来。

秒杀开始什么环节可以在 Redis 中进行?

  1. 大量高并发的库存查验请求,这个环节使用 Redis 保存库存量,请求可以直接从 Redis 中读取库存并进行查验。
  2. 库存扣减操作,不能交给后端数据库处理。
    1. 额外的开销。Redis 中保存了库存量,而库存量的最新值又是数据库在维护,所以数据库更新后,还需要和 Redis 进行同步,这个过程增加了额外的操作逻辑,也带来了额外的开销。
    2. 下单量超过实际库存量,出现超售。由于数据库的处理速度较慢,不能及时更新库存余量,这就会导致大量库存查验的请求读取到旧的库存值,并进行下单。此时,就会出现下单数量大于实际的库存量,导致出现超售,这就不符合业务层的要求了。
  3. 库存的扣减在 Redis 中进行。而且,为了避免请求查询到旧的库存值,库存查验和库存扣减这两个操作需要保证原子性。
img

Redis 的哪些方法可以支撑秒杀场景?

  • 支持高并发。这个很简单,Redis 本身高速处理请求的特性就可以支持高并发。而且,如果有多个秒杀商品,我们也可以使用切片集群,用不同的实例保存不同商品的库存,这样就避免,使用单个实例导致所有的秒杀请求都集中在一个实例上的问题了。不过,需要注意的是,当使用切片集群时,我们要先用 CRC 算法计算不同秒杀商品 key 对应的 Slot,然后,我们在分配 Slot 和实例对应关系时,才能把不同秒杀商品对应的 Slot 分配到不同实例上保存。
  • 保证库存查验和库存扣减原子性执行。针对这条要求,我们就可以使用 Redis 的原子操作或是分布式锁这两个功能特性来支撑了。

基于原子操作支撑秒杀场景

虽然 Redis 具有原子性的加减,但是还有一步是查验操作,需要【查询 + 加减库存】,所以需要搞个 lua 脚本实现原子性操作。

基于分布式锁来支撑秒杀场景

先让客户端向 Redis 申请分布式锁,只有拿到锁的客户端才能执行库存查验和库存扣减。这样一来,大量的秒杀请求就会在争夺分布式锁时被过滤掉。而且,库存查验和扣减也不用使用原子操作了,因为多个并发客户端只有一个客户端能够拿到锁,已经保证了客户端并发访问的互斥性。

所以,可以使用切片集群中的不同实例来分别保存分布式锁和商品库存信息。使用这种保存方式后,秒杀请求会首先访问保存分布式锁的实例。如果客户端没有拿到锁,这些客户端就不会查询商品库存,这就可以减轻保存库存信息的实例的压力了。

小结

  1. 前端静态页面的设计。秒杀页面上能静态化处理的页面元素,我们都要尽量静态化,这样可以充分利用 CDN 或浏览器缓存服务秒杀开始前的请求。
  2. 请求拦截和流控。在秒杀系统的接入层,对恶意请求进行拦截,避免对系统的恶意攻击,例如使用黑名单禁止恶意 IP 进行访问。如果 Redis 实例的访问压力过大,为了避免实例崩溃,我们也需要在接入层进行限流,控制进入秒杀系统的请求数量。
  3. 库存信息过期时间处理。Redis 中保存的库存信息其实是数据库的缓存,为了避免缓存击穿问题,我们不要给库存信息设置过期时间。
  4. 数据库订单异常处理。如果数据库没能成功处理订单,可以增加订单重试功能,保证订单最终能被成功处理。

Q:如何应对数据倾斜

数据倾斜有两类。

  • 数据量倾斜:在某些情况下,实例上的数据分布不均衡,某个实例上的数据特别多。
  • 数据访问倾斜:虽然每个集群实例上的数据量相差不大,但是某个实例上的数据是热点数据,被访问得非常频繁。

数据量倾斜的成因和应对方法

那么,数据量倾斜是怎么产生的呢?这主要有三个原因,分别是某个实例上保存了 bigkeySlot 分配不均衡以及 Hash Tag

bigkey 导致倾斜

第一个原因是,某个实例上正好保存了 bigkey。bigkey 的 value 值很大(String 类型),或者是 bigkey 保存了大量集合元素(集合类型),会导致这个实例的数据量增加,内存资源消耗也相应增加。

解决:

一个根本的应对方法是,我们在业务层生成数据时,要尽量避免把过多的数据保存在同一个键值对中。

此外,如果 bigkey 正好是集合类型,我们还有一个方法,就是把 bigkey 拆分成很多个小的集合类型数据,分散保存在不同的实例上。

Slot 分配不均衡导致倾斜

如果集群运维人员没有均衡地分配 Slot,就会有大量的数据被分配到同一个 Slot 中,而同一个 Slot 只会在一个实例上分布(该实例配置较高),这就会导致,大量数据被集中到一个实例上,造成数据倾斜。

在 Redis Cluster 中,我们可以使用 3 个命令完成 Slot 迁移。

  • CLUSTER SETSLOT:使用不同的选项进行三种设置,分别是设置 Slot 要迁入的目标实例,Slot 要迁出的源实例,以及 Slot 所属的实例。
  • CLUSTER GETKEYSINSLOT:获取某个 Slot 中一定数量的 key。
  • MIGRATE:把一个 key 从源实例实际迁移到目标实例。

Hash Tag 导致倾斜

Hash Tag 是指加在键值对 key 中的一对花括号{}。这对括号会把 key 的一部分括起来,客户端在计算 key 的 CRC16 值时,只对 Hash Tag 花括号中的 key 内容进行计算。如果没用 Hash Tag 的话,客户端计算整个 key 的 CRC16 的值。

img

其中,user:profile:{3231}和 user:order:{3231}的 Hash Tag 一样,都是 3231,它们的 CRC16 计算值对 16384 取模后的值也是一样的,所以就对应映射到了相同的 Slot 1024 中。user:profile:{5328}和 user:order:{5328}也是相同的映射结果。

小结

通常来说,热点数据以服务读操作为主,在这种情况下,我们可以采用热点数据多副本的方法来应对。

这个方法的具体做法是,我们把热点数据复制多份,在每一个数据副本的 key 中增加一个随机前缀,让它和其它副本数据不会被映射到同一个 Slot 中。

热点数据多副本方法只能针对只读的热点数据。如果热点数据是有读有写的话,就不适合采用多副本方法了,因为要保证多副本间的数据一致性,会带来额外的开销。

img