咱们的课程已经更新到第 21 讲了,今天,我们来进行一场答疑。
前半部分,我会给你讲解第 11 ~ 21 讲的课后思考题。在学习这部分内容时,可以和你的答案进行对照,看看还有哪里没有考虑到。当然,有些问题不一定有标准答案,我们还可以继续讨论。
后半部分,我会围绕着许多同学都很关注的如何排查慢查询命令和 bigkey 的问题,重点解释一下,希望可以解答你的困惑。
好了,我们现在开始。
课后思考题答案
第 11 讲
问题:除了 String 类型和 Hash 类型,还有什么类型适合保存第 11 讲中所说的图片吗?
答案:除了 String 和 Hash,我们还可以使用 Sorted Set 类型进行保存。Sorted Set 的元素有 member 值和 score 值,可以像 Hash 那样,使用二级编码进行保存。具体做法是,把图片 ID 的前 7 位作为 Sorted Set 的 key,把图片 ID 的后 3 位作为 member 值,图片存储对象 ID 作为 score 值。
Sorted Set 中元素较少时,Redis 会使用压缩列表进行存储,可以节省内存空间。不过,和 Hash 不一样,Sorted Set 插入数据时,需要按 score 值的大小排序。当底层结构是压缩列表时,Sorted Set 的插入性能就比不上 Hash。所以,在我们这节课描述的场景中,Sorted Set 类型虽然可以用来保存,但并不是最优选项。
第 14 讲
问题:在用 Sorted Set 保存时间序列数据时,如果把时间戳作为 score,把实际的数据作为 member,这样保存数据有没有潜在的风险?另外,如果你是 Redis 的开发维护者,你会把聚合计算也设计为 Sorted Set 的一个内在功能吗?
第 15 讲
问题:如果一个生产者发送给消息队列的消息,需要被多个消费者进行读取和处理(例如,一个消息是一条从业务系统采集的数据,既要被消费者 1 读取并进行实时计算,也要被消费者 2 读取并留存到分布式文件系统 HDFS 中,以便后续进行历史查询),你会使用 Redis 的什么数据类型来解决这个问题呢?
答案:有同学提到,可以使用 Streams 数据类型的消费组,同时消费生产者的数据,这是可以的。但是,有个地方需要注意,如果只是使用一个消费组的话,消费组内的多个消费者在消费消息时是互斥的,换句话说,在一个消费组内,一个消息只能被一个消费者消费。我们希望消息既要被消费者 1 读取,也要被消费者 2 读取,是一个多消费者的需求。所以,如果使用消费组模式,需要让消费者 1 和消费者 2 属于不同的消费组,这样它们就能同时消费了。
另外,Redis 基于字典和链表数据结构,实现了发布和订阅功能,这个功能可以实现一个消息被多个消费者消费使用,可以满足问题中的场景需求。
第 20 讲
问题:我们可以使用 mem_fragmentation_ratio 来判断 Redis 当前的内存碎片率是否严重,我给出的经验阈值都是大于 1 的。我想请你思考一下,如果 mem_fragmentation_ratio 小于 1,Redis 的内存使用是什么情况呢?会对 Redis 的性能和内存空间利用率造成什么影响呢?
答案:如果 mem_fragmentation_ratio 小于 1,就表明,操作系统分配给 Redis 的内存空间已经小于 Redis 所申请的空间大小了,此时,运行 Redis 实例的服务器上的内存已经不够用了,可能已经发生 swap 了。这样一来,Redis 的读写性能也会受到影响,因为 Redis 实例需要在磁盘上的 swap 分区中读写数据,速度较慢。
第 21 讲
问题:在和 Redis 实例交互时,应用程序中使用的客户端需要使用缓冲区吗?如果使用的话,对 Redis 的性能和内存使用会有影响吗?
答案:应用程序中使用的 Redis 客户端,需要把要发送的请求暂存在缓冲区。这有两方面的好处。
一方面,可以在客户端控制发送速率,避免把过多的请求一下子全部发到 Redis 实例,导致实例因压力过大而性能下降。不过,客户端缓冲区不会太大,所以,对 Redis 实例的内存使用没有什么影响。
另一方面,在应用 Redis 主从集群时,主从节点进行故障切换是需要一定时间的,此时,主节点无法服务外来请求。如果客户端有缓冲区暂存请求,那么,客户端仍然可以正常接收业务应用的请求,这就可以避免直接给应用返回无法服务的错误。
代表性问题
在前面的课程中,我重点介绍了避免 Redis 变慢的方法。慢查询命令的执行时间和 bigkey 操作的耗时都很长,会阻塞 Redis。很多同学学完之后,知道了要尽量避免 Redis 阻塞,但是还不太清楚,具体应该如何排查阻塞的命令和 bigkey 呢。
所以,接下来,我就再重点解释一下,如何排查慢查询命令,以及如何排查 bigkey。
问题 1:如何使用慢查询日志和 latency monitor 排查执行慢的操作?
在第 18 讲中,我提到,可以使用 Redis 日志(慢查询日志)和 latency monitor 来排查执行较慢的命令操作,那么,我们该如何使用慢查询日志和 latency monitor 呢?
Redis 的慢查询日志记录了执行时间超过一定阈值的命令操作。当我们发现 Redis 响应变慢、请求延迟增加时,就可以在慢查询日志中进行查找,确定究竟是哪些命令执行时间很长。
在使用慢查询日志前,我们需要设置两个参数。
- slowlog-log-slower-than:这个参数表示,慢查询日志对执行时间大于多少微秒的命令进行记录。
- slowlog-max-len:这个参数表示,慢查询日志最多能记录多少条命令记录。慢查询日志的底层实现是一个具有预定大小的先进先出队列,一旦记录的命令数量超过了队列长度,最先记录的命令操作就会被删除。这个值默认是 128。但是,如果慢查询命令较多的话,日志里就存不下了;如果这个值太大了,又会占用一定的内存空间。所以,一般建议设置为 1000 左右,这样既可以多记录些慢查询命令,方便排查,也可以避免内存开销。
设置好参数后,慢查询日志就会把执行时间超过 slowlog-log-slower-than 阈值的命令操作记录在日志中。
我们可以使用 SLOWLOG GET 命令,来查看慢查询日志中记录的命令操作,例如,我们执行如下命令,可以查看最近的一条慢查询的日志信息。
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) "" //客户端的名称,此处为空可以看到,KEYS “abc*“这条命令的执行时间是 20906 微秒,大约 20 毫秒,的确是一条执行较慢的命令操作。如果我们想查看更多的慢日志,只要把 SLOWLOG GET 后面的数字参数改为想查看的日志条数,就可以了。
好了,有了慢查询日志后,我们就可以快速确认,究竟是哪些命令的执行时间比较长,然后可以反馈给业务部门,让业务开发人员避免在应用 Redis 的过程中使用这些命令,或是减少操作的数据量,从而降低命令的执行复杂度。
除了慢查询日志以外,Redis 从 2.8.13 版本开始,还提供了 latency monitor 监控工具,这个工具可以用来监控 Redis 运行过程中的峰值延迟情况。
和慢查询日志的设置相类似,要使用 latency monitor,首先要设置命令执行时长的阈值。当一个命令的实际执行时长超过该阈值时,就会被 latency monitor 监控到。比如,我们可以把 latency monitor 监控的命令执行时长阈值设为 1000 微秒,如下所示:
config set latency-monitor-threshold 1000设置好了 latency monitor 的参数后,我们可以使用 latency latest 命令,查看最新和最大的超过阈值的延迟情况,如下所示:
latency latest
1) 1) "command"
2) (integer) 1600991500 //命令执行的时间戳
3) (integer) 2500 //最近的超过阈值的延迟
4) (integer) 10100 //最大的超过阈值的延迟问题 2:如何排查 Redis 的 bigkey?
在应用 Redis 时,我们要尽量避免 bigkey 的使用,这是因为,Redis 主线程在操作 bigkey 时,会被阻塞。那么,一旦业务应用中使用了 bigkey,我们该如何进行排查呢?
Redis 可以在执行 redis-cli 命令时带上–bigkeys 选项,进而对整个数据库中的键值对大小情况进行统计分析,比如说,统计每种数据类型的键值对个数以及平均大小。此外,这个命令执行后,会输出每种数据类型中最大的 bigkey 的信息,对于 String 类型来说,会输出最大 bigkey 的字节长度,对于集合类型来说,会输出最大 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
//统计每种数据类型的总键值个数,占所有键值个数的比例,以及平均大小
4 lists with 15 items (12.50% of keys, avg size 3.75)
5 hashs with 14 fields (15.62% of keys, avg size 2.80)
10 strings with 68 bytes (31.25% of keys, avg size 6.80)
1 streams with 4 entries (03.12% of keys, avg size 4.00)
7 sets with 19 members (21.88% of keys, avg size 2.71)
5 zsets with 17 members (15.62% of keys, avg size 3.40)不过,在使用–bigkeys 选项时,有一个地方需要注意一下。这个工具是通过扫描数据库来查找 bigkey 的,所以,在执行的过程中,会对 Redis 实例的性能产生影响。如果你在使用主从集群,我建议你在从节点上执行该命令。因为主节点上执行时,会阻塞主节点。如果没有从节点,那么,我给你两个小建议:第一个建议是,在 Redis 实例业务压力的低峰阶段进行扫描查询,以免影响到实例的正常运行;第二个建议是,可以使用 -i 参数控制扫描间隔,避免长时间扫描降低 Redis 实例的性能。例如,我们执行如下命令时,redis-cli 会每扫描 100 次暂停 100 毫秒(0.1 秒)。
./redis-cli --bigkeys -i 0.1当然,使用 Redis 自带的–bigkeys 选项排查 bigkey,有两个不足的地方:
- 这个方法只能返回每种类型中最大的那个 bigkey,无法得到大小排在前 N 位的 bigkey;
- 对于集合类型来说,这个方法只统计集合元素个数的多少,而不是实际占用的内存量。但是,一个集合中的元素个数多,并不一定占用的内存就多。因为,有可能每个元素占用的内存很小,这样的话,即使元素个数有很多,总内存开销也不大。
所以,如果我们想统计每个数据类型中占用内存最多的前 N 个 bigkey,可以自己开发一个程序,来进行统计。
我给你提供一个基本的开发思路:使用 SCAN 命令对数据库扫描,然后用 TYPE 命令获取返回的每一个 key 的类型。接下来,对于 String 类型,可以直接使用 STRLEN 命令获取字符串的长度,也就是占用的内存空间字节数。
对于集合类型来说,有两种方法可以获得它占用的内存大小。
如果你能够预先从业务层知道集合元素的平均大小,那么,可以使用下面的命令获取集合元素的个数,然后乘以集合元素的平均大小,这样就能获得集合占用的内存大小了。
- List 类型:LLEN 命令;
- Hash 类型:HLEN 命令;
- Set 类型:SCARD 命令;
- Sorted Set 类型:ZCARD 命令;
如果你不能提前知道写入集合的元素大小,可以使用 MEMORY USAGE 命令(需要 Redis 4.0 及以上版本),查询一个键值对占用的内存空间。例如,执行以下命令,可以获得 key 为 user:info 这个集合类型占用的内存空间大小。
MEMORY USAGE user:info
(integer) 315663239这样一来,你就可以在开发的程序中,把每一种数据类型中的占用内存空间大小排在前 N 位的 key 统计出来,这也就是每个数据类型中的前 N 个 bigkey。
总结
从第 11 讲到第 21 讲,我们重点介绍的知识点比较多,也比较细。其实,我们可以分成两大部分来掌握:一个是多种多样的数据结构,另一个是如何避免 Redis 性能变慢。
希望这节课的答疑,能帮助你更加深入地理解前面学过的内容。通过这节课,你应该也看到了,课后思考题是一种很好地梳理重点内容、拓展思路的方式,所以,在接下来的课程里,希望你能多留言聊一聊你的想法,这样可以进一步巩固你所学的知识。而且,还能在和其他同学的交流中,收获更多东西。好了,这节课就到这里,我们下节课见。