【Redis】中的一些坑(一)——「常用命令」篇

本文需要对 Redis 常用命令、底层数据结构、内存管理以及集群等相关概念有一定了解,推荐查看 Redis 面试题总结 快速学习相关知识点。

1 前言

想必大家在使用 Redis 时,或多或少遇到了一些「诡异」的场景,那很大概率是踩到「坑」:

  • 一个设置了过期时间的 key 最后没有过期;
  • 使用 O(1) 复杂度的 SETBIT 命令,竟然 OOM 了;
  • 使用 RANDOMKEY 随机读取一个 key,主线程发生阻塞;
  • 相同命令主库查不到数据,从库却可以查到;
  • 从库使用内存为什么比主库使用的多;

熟悉和了解一些 Redis 中的常见问题,可可以方便我们快速的定位和解决问题,经过归类整理,笔者把这些问题大致划分成三大部分:

  1. 常见命令有哪些坑?
  2. 数据持久化有哪些坑?
  3. 主从库同步有哪些坑?

2 常用命令中的坑

首先让我们看看哪些平常再熟悉不过的命令可能会产生「意料之外」的结果。

2.1 过期时间意外丢失

SET 命令一定是 Redis 使用最多的命令,它不仅可以设置 key-value 之外,还可以设置 key 的过期时间,就像下面这样:

127.0.0.1:6379> SET testkey val1 EX 60
OK
127.0.0.1:6379> TTL testkey
(integer) 59

但需要注意的是,如果你从代码其它地方修改了这个 key,并且没有加上「过期时间」参数,那这个 key 的过期时间将会被「擦除」,即永不过期

127.0.0.1:6379> SET testkey val2
OK
127.0.0.1:6379> TTL testkey  // key永远不过期了!
(integer) -1

如果你发现 Redis 的内存持续增长,而且很多 key 原来设置了过期时间,后来发现过期时间丢失了,很有可能是因为这个原因导致的。

2.2 DEL 阻塞 Redis

当需要删除一个 key 时你也许立马就想到了 DEL 命令,但你是否想过它的时间复杂度?

Redis 官方文档在介绍 DEL 命令时,是这样描述的:删除一个 key 的耗时与其类型有关。

  • key 是 String 类型,DEL 时间复杂度是 O(1);
  • key 是 List、Hash、Set、ZSet 类型,DEL 时间复杂度是 O(M),M 为元素数量。

如果你了解 Redis 的底层数据结构就不难理解,所以,当删除 List、Hash、Set、ZSet 类型的 key 时,一定要格外注意,不能无脑执行 DEL,而是应该用以下方式删除**:**

1)查询 key 中元素数量:执行 LLEN HLEN SCARD ZCARD 命令;

2)判断 key 中元素数量:如果较少可直接删除,否则分批删除;

3)分批删除:执行 LRANGE/HSCAN/SSCAN/ZSCAN + LPOP/RPOP/HDEL/SREM/ZREM 删除。

上面说的是集合类型,如果直接删除一个 String 类型就不会出现这种情况吗?

显然也会阻塞 Redis 主线程,我们知道,一个 String 类型的 key 默认最大可以存储 512M 的数据,当 Redis 释放一个如此大的内存给操作系统时,必然也是耗时的。

如果打开 lazy-free 机制是否就不会阻塞主线程?

即使 Redis 打开了 lazy-free,在删除一个 String 类型的 bigkey 时,它仍旧是在主线程中处理,而不是放到后台线程中执行。所以依旧无法解决阻塞 Redis 的风险。

2.3 RANDOMKEY 阻塞 Redis

Redis 提供了一个随机查看内存中一个 key 的命令: RANDOMKEY。这个命令会从 Redis 中「随机」取出一个 key,虽名为随机,但执行速度也并非我们想象中的快。

要解释清楚这个问题,就要结合 Redis 的过期策略来讲。Redis 采用定时清理 + 懒惰清理两种结合方式来清理过期 key。

再来了解一下 RANDOMKEY 的执行过程:Redis 在随机拿出一个 key 后,首先会先检查这个 key 是否已过期。如果该 key 已经过期,那么 Redis 会删除它,这个过程就是懒惰清理。但清理完了还不能结束,Redis 还要以此循环往复,直到找出一个「不过期」的 key 返回给客户端。

但这里就有一个问题:如果此时 Redis 中,有大量 key 已经过期,但还未来得及被清理掉,那这个循环就会持续很久才能结束,而且,这个耗时都花费在了清理过期 key + 寻找不过期 key 上。导致的结果就是,RANDOMKEY 执行耗时变长,影响 Redis 性能。

以上流程主要针对的是 Matser 节点,如果是 Slave 节点执行 RANDOMKEY 命令那么产生的问题将更加严重。主要原因就在于,Slave 是不会自己清理过期 key:

当一个 key 要过期时,Master 会先清理删除它,之后 Master 向 Slave 发送一个 DEL 命令,告知 Slave 也删除这个 key,以此达到主从库的数据一致性。

还是在同样 Redis 中存在大量已过期但还未被清理的 key 的场景中, Slave 上执行 RANDOMKEY 时就会发生以下问题:

1)Slave 随机取出一个 key,判断是否已过期;

2)key 已过期,但 Slave 不会删除它,而是继续随机寻找不过期的 key;

3)由于大量 key 都已过期,那 Slave 就会寻找不到符合条件的 key,此时就会陷入「死循环」中。

也就是说,在 Slave 上执行 RANDOMKEY,有可能会造成整个 Redis 实例卡死。

这其实是 Redis 的一个 Bug,并一直持续到 5.0 才被修复。Redis 给出的解决方案是

在 Slave 上执行 RANDOMKEY 时,先判断整个实例所有 key 是否都设置了过期时间,如果是,为了避免长时间找不到符合条件的 key,Slave 最多只会在哈希表中寻找 100 次,无论是否能找到,都会退出循环。即增加了一个最大重试次数。

在这里插入图片描述

所以,如果你在使用 RANDOMKEY 时发现 Redis 产生了「抖动」,很有可能是因为这个原因导致。

2.4 SETBIT 导致 OOM

String 类型除了存储一串字符串外,我们经常还可以将其当做 bitmap 位图来使用。换句话说将 value 拆成一个一个的 bit 位来使用:

127.0.0.1:6379> SETBIT testkey 10 1
(integer) 1
127.0.0.1:6379> GETBIT testkey 10
(integer) 1

但是这里有一个坑:如果这个 key 不存在,或者 key 的内存使用很小,此时你要操作的 offset 非常大,那么 Redis 就需要分配「更大的内存空间」,这个操作耗时就会变长,影响性能。

在这里插入图片描述

所以,当你在使用 SETBIT 时,也一定要注意 offset 的大小,操作过大的 offset 会引发 Redis 卡顿。当然,也要注意删除时的影响。

2.5 MONITOR 导致 OOM

当你在执行 MONITOR 命令时,Redis 会把每一条命令写到客户端的「输出缓冲区」中,然后客户端从这个缓冲区读取服务端返回的结果。

在这里插入图片描述

但如果 Redis 对应的 QPS 很高时,这将会导致这个输出缓冲区内存持续增长,占用 Redis 大量的内存资源,如果恰好机器的内存资源不足,那 Redis 实例就会面临被 OOM 的风险。

猜你喜欢

转载自blog.csdn.net/adminpd/article/details/127342475