21 缓冲区

缓冲区就是用一块内存空间来暂时存放命令数据,以免出现因为数据和命令的处理速度慢于发送速度而导致的数据丢失和性能问题。

缓冲器溢出:
缓冲区空间有限,当写入速度持续大于读取速度,占用内存超出设定上限时,发生缓冲区溢出。

使用场景:

  1. 在客户端和服务端通信时,暂存客户端命令数据,和服务端返回结果
  2. 在主从同步时,用来暂存主节点接收的写命令和数据

客户端

服务器端给每个连接的客户端都设置了一个输入缓冲区和输出缓冲区,我们称之为客户端输入缓冲区和输出缓冲区。

  1. 输入缓冲区会先把客户端发送过来的命令暂存起来,Redis 主线程再从输入缓冲区中读取命令,进行处理。
    2.当 Redis 主线程处理完数据后,会把结果写入到输出缓冲区,再通过输出缓冲区返回给客户端。

输入缓冲区

Redis 的客户端输入缓冲区大小的上限阈值,在代码中就设定为了 1GB,无法调整。

溢出原因:

  1. 写入了 bigkey,比如一下子写入了多个百万级别的集合类型数据;
  2. 服务器端处理请求的速度过慢,例如,Redis 主线程出现了间歇性阻塞,无法及时处理正常发送的请求,导致客户端发送的请求在缓冲区越积越多。

查看使用情况:

CLIENT LIST

id=5 addr=127.0.0.1:50487 fd=9 name= age=4 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
  • qbuf,表示输入缓冲区已经使用的大小。
  • qbuf-free,表示输入缓冲区尚未使用的大小。

输出缓冲区

Redis 为每个客户端设置的输出缓冲区也包括两部分:

  • 一部分,是一个大小为 16KB 的固定缓冲空间,用来暂存 OK 响应和出错信息;
  • 另一部分,是一个可以动态增加的缓冲空间,用来暂存大小可变的响应结果。

溢出原因:

  1. 服务器端返回 bigkey 的大量结果;
  2. 执行了 MONITOR 命令;
  3. 缓冲区大小设置得不合理。

MONITOR 命令是用来监测 Redis 执行的,输出会持续占用缓冲区,不要在生产环境使用:

MONITOR

设置缓冲区大小:client-output-buffer-limit 配置项

client-output-buffer-limit normal 0 0 0

normal 表示当前设置的是普通客户端,
第 1 个 0 设置的是缓冲区大小限制,
第 2 个 0 和第 3 个 0 分别表示缓冲区持续写入量限制和持续写入时间限制。

客户端类型:

  • 常规和 Redis 服务器端进行读写命令交互的普通客户端
  • 订阅了 Redis 频道的订阅客户端。

对于普通客户端来说,它每发送完一个请求,会等到请求结果返回后,再发送下一个请求,这种发送方式称为阻塞式发送。
所以,我们通常把普通客户端的缓冲区大小限制,以及持续写入量限制、持续写入时间限制都设置为 0,也就是不做限制。

订阅客户端和服务器间的消息发送方式,不属于阻塞式发送。
因此,我们会给订阅客户端设置缓冲区大小限制、缓冲区持续写入量限制,以及持续写入时间限制,可以在 Redis 配置文件中这样设置:

client-output-buffer-limit pubsub 8mb 2mb 60

pubsub 参数表示当前是对订阅客户端进行设置;8mb 表示输出缓冲区的大小上限为 8MB,一旦实际占用的缓冲区大小要超过 8MB,服务器端就会直接关闭客户端的连接;2mb 和 60 表示,如果连续 60 秒内对输出缓冲区的写入量超过 2MB 的话,服务器端也会关闭客户端连接。

主从集群缓冲区

主从节点的全量和增量复制都会用到缓冲区。

全量复制

在全量复制过程中,主节点在向从节点传输 RDB 文件的同时,会继续接收客户端发送的写命令请求。这些写命令就会先保存在复制缓冲区中,等 RDB 文件传输完成后,再发送给从节点去执行。主节点上会为每个从节点都维护一个复制缓冲区,来保证主从节点间的数据同步。

问题:
在全量复制时,从节点接收和加载 RDB 较慢,同时主节点接收到了大量的写命令,写命令在复制缓冲区中就会越积越多,最终导致溢出。
复制缓冲区一旦发生溢出,主节点也会直接关闭和从节点进行复制操作的连接,导致全量复制失败。

优化:

  1. 主节点的数据量控制在 2~4GB,这样可以让全量同步执行得更快些,避免复制缓冲区累积过多命令。
  2. 控制和主节点连接的从节点个数,不要使用大规模的主从集群。

设置复制缓冲区:

config set client-output-buffer-limit slave 512mb 128mb 60

slave 参数表明该配置项是针对复制缓冲区的。512mb 代表将缓冲区大小的上限设置为 512MB;128mb 和 60 代表的设置是,如果连续 60 秒内的写入量超过 128MB 的话,也会触发缓冲区溢出。

增量复制

增量复制时使用的缓冲区,称为复制积压缓冲区,英文名repl_backlog_buffer。

主节点在把接收到的写命令同步给从节点时,同时会把这些写命令写入复制积压缓冲区。一旦从节点发生网络闪断,再次和主节点恢复连接后,从节点就会从复制积压缓冲区中,读取断连期间主节点接收到的写命令,进而进行增量同步。

复制积压缓冲区是一个大小有限的环形缓冲区。当主节点把复制积压缓冲区写满后,会覆盖缓冲区中的旧命令数据。如果从节点还没有同步这些旧命令数据,就会造成主从节点间重新开始执行全量复制。

优化:
为了应对复制积压缓冲区的溢出问题,我们可以调整复制积压缓冲区的大小,也就是设置 repl_backlog_size 这个参数的值。

22 常见问题答疑

  1. 如何使用慢查询日志和 latency monitor 排查执行慢的操作?
    配置参数:
  2. slowlog-log-slower-than:慢查询日志对执行时间大于多少微秒的命令进行记录。
  3. slowlog-max-len:慢查询日志最多能记录多少条命令记录,默认128,建议1000。

查看慢日志:

SLOWLOG GET 1
1) 1) (integer) 33           //每条日志的唯一ID编号
   2) (integer) 1600990583   //命令执行时的时间戳
   3) (integer) 20906        //命令执行的时长,单位是微秒
   4) 1) "keys"               //具体的执行命令和参数
      2) "abc*"
   5) "127.0.0.1:54793"      //客户端的IP和端口号
   6) ""                     //客户端的名称,此处为空

设置峰值延迟(1000毫秒):

config set latency-monitor-threshold 1000

查看峰值延迟:

latency latest
1) 1) "command"
   2) (integer) 1600991500    //命令执行的时间戳
   3) (integer) 2500           //最近的超过阈值的延迟
   4) (integer) 10100          //最大的超过阈值的延迟
  1. 如何排查Redis的bigkey?

    ./redis-cli  --bigkeys
    
    -------- summary -------
    Sampled 32 keys in the keyspace!
    Total key length in bytes is 184 (avg len 5.75)
    
    //统计每种数据类型中元素个数最多的bigkey
    Biggest   list found 'product1' has 8 items
    Biggest   hash found 'dtemp' has 5 fields
    Biggest string found 'page2' has 28 bytes
    Biggest stream found 'mqstream' has 4 entries
    Biggest    set found 'userid' has 5 members
    Biggest   zset found 'device:temperature' has 6 members
    
  2. lists with 15 items (12.50% of keys, avg size 3.75)
  3. hashs with 14 fields (15.62% of keys, avg size 2.80)
  4. strings with 68 bytes (31.25% of keys, avg size 6.80)
  5. streams with 4 entries (03.12% of keys, avg size 4.00)
  6. sets with 19 members (21.88% of keys, avg size 2.71)
  7. zsets with 17 members (15.62% of keys, avg size 3.40)

    注意:扫描数据库会对Redis性能产生影响,建议在从节点执行,使用-i控制扫描间隔。

    ./redis-cli --bigkeys -i 0.1

  8. 只能返回每种类型最大的bigkey,无法得到多个
  9. 只能统计集合元素个数,而不是实际占用内存

扫描工具:

  1. 使用 SCAN 命令对数据库扫描,然后用 TYPE 命令获取返回的每一个 key 的类型。
  2. 对于 String 类型,使用 STRLEN 命令获取字符串的长度,也就是占用的内存空间字节数。
  3. 集合类型获取元素个数,乘以平均大小就是占用内存大小。

    • List 类型:LLEN 命令;
    • Hash 类型:HLEN 命令;
    • Set 类型:SCARD 命令;
    • Sorted Set 类型:ZCARD 命令;
  4. 把每种数据类型占用内存前N位的key统计出来,就是bigkey

23 旁路缓存

缓存特征:

  1. 在分层系统中,数据暂存在快速子系统中有助于加速访问。
  2. 缓存容量有限,缓存写满时,数据需要被淘汰。

计算机系统缓存(两种):

  • LLC:CPU中的末级缓存,用来缓存内存中的数据,避免每次从内存中存取数据;
  • page cache:内存中的告诉页缓存,用来缓存磁盘中的数据,避免每次从磁盘中存取数据。

构建计算机硬件系统时,已经把 LLC 和 page cache 放在了应用程序的数据访问路径上,应用程序访问数据时直接就能用上缓存。

旁路缓存

在应用程序中新增缓存逻辑处理的代码,读取缓存、读取数据库和更新缓存的操作都在应用程序中来完成。

旁路缓存是一个独立的系统,我们可以单独对 Redis 缓存进行扩容或性能优化。而且,只要保持操作接口不变,我们在应用程序中增加的代码就不用再修改了。

缓存类型

Redis 缓存的两种类型:只读缓存和读写缓存。

  • 只读缓存能加速读请求
  • 读写缓存可以同时加速读写请求,有两种数据写回策略:

    • 同步直写保证数据可靠性
    • 异步回写低延迟

只读缓存

当 Redis 用作只读缓存时,应用要读取数据的话,会先调用 Redis GET 接口,查询数据是否存在。而所有的数据写请求,会直接发往后端的数据库,在数据库中增删改。

对于删改的数据来说,如果 Redis 已经缓存了相应的数据,应用需要把这些缓存的数据删除,Redis 中就没有这些数据了。当应用再次读取这些数据时,会发生缓存缺失,应用会把这些数据从数据库中读出来,并写到缓存中。

只读缓存直接在数据库中更新数据的好处是,所有最新的数据都在数据库中,而数据库是提供数据可靠性保障的,这些数据不会有丢失的风险。当我们需要缓存图片、短视频这些用户只读的数据时,就可以使用只读缓存这个类型了。

读写缓存

读请求会发送到缓存进行处理(直接在缓存中查询数据是否存在),所有的写请求也会发送到缓存,在缓存中直接对数据进行增删改操作。

在使用读写缓存时,最新的数据是在 Redis 中,而 Redis 是内存数据库,一旦出现掉电或宕机,内存中的数据就会丢失。这也就是说,应用的最新数据可能会丢失,给应用业务带来风险。

写回策略:同步直写,异步写回。

同步直写:
写请求发给缓存的同时,也会发给后端数据库进行处理,等到缓存和数据库都写完数据,才给客户端返回。
优点:即使缓存宕机或发生故障,最新的数据仍然保存在数据库中,这就提供了数据可靠性保证。
缺点:降低访问性能。

异步写回:
所有写请求都先在缓存中处理。等到这些增改的数据要被从缓存中淘汰出来时,缓存将它们写回后端数据库。
优点:性能高。
缺点:可能会丢失。

举例:在商品大促的场景中,商品的库存信息会一直被修改。如果每次修改都需到数据库中处理,就会拖慢整个应用,此时,我们通常会选择读写缓存的模式。

24 缓存淘汰

建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销。

确定了缓存最大容量,就可以设定缓存的大小了:

CONFIG SET maxmemory 4gb

淘汰策略(8种)

不进行数据淘汰:noeviction(默认)
过期淘汰:volatile-random、volatile-ttl、volatile-lru、volatile-lfu
全范围淘汰:allkeys-lru、allkeys-random、allkeys-lfu

noeviction:缓存写满,再请求直接返回错误。
volatile-ttl:根据过期时间先后进行删除
volatile-random、allkeys-random:随机算法删除
volatile-lru、allkeys-lru:LRU算法删除
volatile-lfu、allkeys-lfu:LFU算法删除

LRU算法

Least Recently Used
把所有的数据组织成一个链表,链表的头和尾分别表示 MRU 端和 LRU 端,分别代表最近最常使用的数据和最近最不常用的数据。

缺点:需要用链表管理所有的缓存数据,这会带来额外的空间开销。而且,当有数据被访问时,需要在链表上把该数据移动到 MRU 端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。

Redis优化:
Redis 默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构 RedisObject 中的 lru 字段记录)。然后,Redis 在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。

Redis 提供了一个配置参数 maxmemory-samples,这个参数就是 Redis 选出的数据个数 N。

CONFIG SET maxmemory-samples 100

当需要再次淘汰数据时,Redis 需要挑选数据进入第一次淘汰时创建的候选集合。这儿的挑选标准是:能进入候选集合的数据的 lru 字段值必须小于候选集合中最小的 lru 值。候选集是一个链表,当有新数据进入候选数据集后,如果候选数据集中的数据个数达到了 maxmemory-samples,Redis 就把候选数据集中 lru 字段值最小的数据淘汰出去。

使用建议:

  • 数据有明显的冷热区分,优先使用 allkeys-lru 策略。
  • 数据访问频率相差不到,建议allkeys-random
  • 有置顶需求的数据,使用volatile-lru,同时置顶数据不设置过期时间,其他数据过期后进行LRU筛选

处理淘汰数据

一旦被淘汰的数据选定后,如果这个数据是干净数据,那么我们就直接删除;如果这个数据是脏数据,我们需要把它写回数据库

干净数据和脏数据的区别就在于,和最初从后端数据库里读取时的值相比,有没有被修改过。干净数据一直没有被修改,所以后端数据库里的数据也是最新值。在替换时,它可以被直接删除。

对于 Redis 来说,它决定了被淘汰的数据后,会把它们删除。即使淘汰的数据是脏数据,Redis 也不会把它们写回数据库。

所以,我们在使用 Redis 缓存时,如果数据被修改了,需要在数据修改时就将它写回数据库。否则,这个脏数据被淘汰时,会被 Redis 删除,而数据库里也没有最新的数据了。

25 缓存异常

缓存中有数据,那么,缓存的数据值需要和数据库中的值相同;缓存中本身没有数据,那么,数据库中的值必须是最新值。不符合这两种情况的,就属于缓存和数据库的数据不一致问题了。

读写缓存:

  • 对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略。同时更新缓存和数据库,需要我们在业务应用中使用事务机制,来保证缓存和数据库的更新具有原子性。
  • 对于数据一直性要求不高的数据,如商品的非关键属性,可使用异步写回策略。

只读缓存:
如果有数据新增,会直接写入数据库;而有数据删改时,就需要把只读缓存中的数据标记为无效。

重试机制

把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用 Kafka 消息队列)。当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。

如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了。否则的话,我们还需要再次进行重试。如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。

分布式锁

对于写请求,需要配合分布式锁使用。

写请求进来时,针对同一个资源的修改操作,先加分布式锁,这样同一时间只允许一个线程去更新数据库和缓存,没有拿到锁的线程把操作放入到队列中,延时处理。用这种方式保证多个线程操作同一资源的顺序性,以此保证一致性。

小结

缓存和数据库的数据不一致一般是由两个原因导致的,我给你提供了相应的解决方案。

  1. 删除缓存值或更新数据库失败而导致数据不一致,你可以使用重试机制确保删除或更新操作成功。
  2. 在删除缓存值、更新数据库的这两步操作中,有其他线程的并发操作,应对方案是分布式锁。

总结:
使用读写缓存同时操作数据库和缓存时,因为其中一个操作失败导致不一致的问题,可以通过消息队列重试来解决。

而在并发的场景下,读+写并发对业务没有影响或者影响较小,而写+写并发时需要配合分布式锁的使用,才能保证缓存和数据库的一致性。

参考

缓存和一致性问题


IT小马
1.2k 声望166 粉丝

Php - Go - Vue - 云原生