You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

260 lines
21 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 22 | 第1121讲课后思考题答案及常见问题答疑
你好,我是蒋德钧。
咱们的课程已经更新到第21讲了今天我们来进行一场答疑。
前半部分我会给你讲解第1121讲的课后思考题。在学习这部分内容时可以和你的答案进行对照看看还有哪里没有考虑到。当然有些问题不一定有标准答案我们还可以继续讨论。
后半部分我会围绕着许多同学都很关注的如何排查慢查询命令和bigkey的问题重点解释一下希望可以解答你的困惑。
好了,我们现在开始。
## 课后思考题答案
### [第11讲](https://time.geekbang.org/column/article/279649)
**问题除了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类型虽然可以用来保存但并不是最优选项。
### [第12讲](https://time.geekbang.org/column/article/280680)
问题我在第12讲中介绍了4种典型的统计模式分别是聚合统计、排序统计、二值状态统计和基数统计以及它们各自适合的集合类型。你还遇到过其他的统计场景吗用的是什么集合类型呢
答案:@海拉鲁同学在留言中提供了一种场景他们曾使用List+Lua统计最近200个客户的触达率。具体做法是每个List元素表示一个客户元素值为0代表触达元素值为1就代表未触达。在进行统计时应用程序会把代表客户的元素写入队列中。当需要统计触达率时就使用LRANGE key 0 -1 取出全部元素计算0的比例这个比例就是触达率。
这个例子需要获取全部元素不过数据量只有200个不算大所以使用List在实际应用中也是可以接受的。但是如果数据量很大又有其他查询需求的话例如查询单个元素的触达情况List的操作复杂度较高就不合适了可以考虑使用Hash类型。
### [第13讲](https://time.geekbang.org/column/article/281745)
问题你在日常的实践过程中还用过Redis的其他数据类型吗
答案除了我们课程上介绍的5大基本数据类型以及HyperLogLog、Bitmap、GEORedis还有一种数据类型叫作布隆过滤器。它的查询效率很高经常会用在缓存场景中可以用来判断数据是否存在缓存中。我会在后面第25讲具体地介绍一下它。
### [第14讲](https://time.geekbang.org/column/article/282478)
问题在用Sorted Set保存时间序列数据时如果把时间戳作为score把实际的数据作为member这样保存数据有没有潜在的风险另外如果你是Redis的开发维护者你会把聚合计算也设计为Sorted Set的一个内在功能吗
答案Sorted Set和Set一样都会对集合中的元素进行去重也就是说如果我们往集合中插入的member值和之前已经存在的member值一样那么原来member的score就会被新写入的member的score覆盖。相同member的值在Sorted Set中只会保留一个。
对于时间序列数据来说这种去重的特性是会带来数据丢失风险的。毕竟某一时间段内的多个时间序列数据的值可能是相同的。如果我们往Sorted Set中写入的数据是在不同时刻产生的但是写入的时刻不同Sorted Set中只会保存一份最近时刻的数据。这样一来其他时刻的数据就都没有保存下来。
举个例子在记录物联网设备的温度时一个设备一个上午的温度值可能都是26。在Sorted Set中我们把温度值作为member把时间戳作为score。我们用ZADD命令把上午不同时刻的温度值写入Sorted Set。由于member值一样所以只会把score更新为最新时间戳最后只有一个最新时间戳例如上午12点下的温度值。这肯定是无法满足保存多个时刻数据的需求的。
关于是否把聚合计算作为Sorted Set的内在功能考虑到Redis的读写功能是由单线程执行在进行数据读写时本身就会消耗较多的CPU资源如果再在Sorted Set中实现聚合计算就会进一步增加CPU的资源消耗影响到Redis的正常数据读取。所以如果我是Redis的开发维护者除非对Redis的线程模型做修改比如说在Redis中使用额外的线程池做聚合计算否则我不会把聚合计算作为Redis的内在功能实现的。
### [第15讲](https://time.geekbang.org/column/article/284291)
问题如果一个生产者发送给消息队列的消息需要被多个消费者进行读取和处理例如一个消息是一条从业务系统采集的数据既要被消费者1读取并进行实时计算也要被消费者2读取并留存到分布式文件系统HDFS中以便后续进行历史查询你会使用Redis的什么数据类型来解决这个问题呢
答案有同学提到可以使用Streams数据类型的消费组同时消费生产者的数据这是可以的。但是有个地方需要注意如果只是使用一个消费组的话消费组内的多个消费者在消费消息时是互斥的换句话说在一个消费组内一个消息只能被一个消费者消费。我们希望消息既要被消费者1读取也要被消费者2读取是一个多消费者的需求。所以如果使用消费组模式需要让消费者1和消费者2属于不同的消费组这样它们就能同时消费了。
另外Redis基于字典和链表数据结构实现了发布和订阅功能这个功能可以实现一个消息被多个消费者消费使用可以满足问题中的场景需求。
### [第16讲](https://time.geekbang.org/column/article/285000)
问题Redis的写操作例如SET、HSET、SADD等是在关键路径上吗
答案Redis本身是内存数据库所以写操作都需要在内存上完成执行后才能返回这就意味着如果这些写操作处理的是大数据集例如1万个数据那么主线程需要等这1万个数据都写完才能继续执行后面的命令。所以说Redis的写操作也是在关键路径上的。
这个问题是希望你把面向内存和面向磁盘的写操作区分开。当一个写操作需要把数据写到磁盘时,一般来说,写操作只要把数据写到操作系统的内核缓冲区就行。不过,如果我们执行了同步写操作,那就必须要等到数据写回磁盘。所以,面向磁盘的写操作一般不会在关键路径上。
我看到有同学说根据写操作命令的返回值来决定是否在关键路径上如果返回值是OK或者客户端不关心是否写成功那么此时的写操作就不算在关键路径上。
这个思路不错不过需要注意的是客户端经常会阻塞等待发送的命令返回结果在上一个命令还没有返回结果前客户端会一直等待直到返回结果后才会发送下一个命令。此时即使我们不关心返回结果客户端也要等到写操作执行完成才行。所以在不关心写操作返回结果的场景下可以对Redis客户端做异步改造。具体点说就是使用异步线程发送这些不关心返回结果的命令而不是在Redis客户端中等待这些命令的结果。
### [第17讲](https://time.geekbang.org/column/article/286082)
问题在一台有两个CPU Socket每个Socket 8个物理核的服务器上我们部署了一个有着8个实例的Redis切片集群8个实例都为主节点没有主备关系现在有两个方案
1. 在同一个CPU Socket上运行8个实例并和8个CPU核绑定
2. 在两个CPU Socket上各运行4个实例并和相应Socket上的核绑定。
如果不考虑网络数据读取的影响,你会选择哪个方案呢?
答案:建议使用第二个方案,主要有两方面的原因。
1. 同一个CPU Socket上的进程会共享L3缓存。如果把8个实例都部署在同一个Socket上它们会竞争L3缓存这就会导致它们的L3缓存命中率降低影响访问性能。
2. 同一个CPU Socket上的进程会使用同一个Socket上的内存空间。8个实例共享同一个Socket上的内存空间肯定会竞争内存资源。如果有实例保存的数据量大其他实例能用到的内存空间可能就不够了此时其他实例就会跨Socket申请内存进而造成跨Socket访问内存造成实例的性能降低。
另外在切片集群中不同实例间通过网络进行消息通信和数据迁移并不会使用共享内存空间进行跨实例的数据访问。所以即使把不同的实例部署到不同的Socket上它们之间也不会发生跨Socket内存的访问不会受跨Socket内存访问的负面影响。
### [第18讲](https://time.geekbang.org/column/article/286549)
问题在Redis中还有哪些命令可以代替KEYS命令实现对键值对的key的模糊查询呢这些命令的复杂度会导致Redis变慢吗
答案Redis提供的SCAN命令以及针对集合类型数据提供的SSCAN、HSCAN等可以根据执行时设定的数量参数返回指定数量的数据这就可以避免像KEYS命令一样同时返回所有匹配的数据不会导致Redis变慢。以HSCAN为例我们可以执行下面的命令从user这个Hash集合中返回key前缀以103开头的100个键值对。
```
HSCAN user 0 match "103*" 100
```
### [第19讲](https://time.geekbang.org/column/article/287819)
问题你遇到过Redis变慢的情况吗如果有的话你是怎么解决的呢
答案:@Kaito同学在留言区分享了他排查Redis变慢问题的Checklist而且还提供了解决方案非常好我把Kaito同学给出的导致Redis变慢的原因汇总并完善一下分享给你
1. 使用复杂度过高的命令或一次查询全量数据;
2. 操作bigkey
3. 大量key集中过期
4. 内存达到maxmemory
5. 客户端使用短连接和Redis相连
6. 当Redis实例的数据量大时无论是生成RDB还是AOF重写都会导致fork耗时严重
7. AOF的写回策略为always导致每个操作都要同步刷回磁盘
8. Redis实例运行机器的内存不足导致swap发生Redis需要到swap分区读取数据
9. 进程绑定CPU不合理
10. Redis实例运行机器上开启了透明内存大页机制
11. 网卡压力过大。
### [第20讲](https://time.geekbang.org/column/article/289140)
问题我们可以使用mem\_fragmentation\_ratio来判断Redis当前的内存碎片率是否严重我给出的经验阈值都是大于1的。我想请你思考一下如果mem\_fragmentation\_ratio小于1Redis的内存使用是什么情况呢会对Redis的性能和内存空间利用率造成什么影响呢
答案如果mem\_fragmentation\_ratio小于1就表明操作系统分配给Redis的内存空间已经小于Redis所申请的空间大小了此时运行Redis实例的服务器上的内存已经不够用了可能已经发生swap了。这样一来Redis的读写性能也会受到影响因为Redis实例需要在磁盘上的swap分区中读写数据速度较慢。
### [第21讲](https://time.geekbang.org/column/article/291277)
问题在和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有两个不足的地方
1. 这个方法只能返回每种类型中最大的那个bigkey无法得到大小排在前N位的bigkey
2. 对于集合类型来说,这个方法只统计集合元素个数的多少,而不是实际占用的内存量。但是,一个集合中的元素个数多,并不一定占用的内存就多。因为,有可能每个元素占用的内存很小,这样的话,即使元素个数有很多,总内存开销也不大。
所以如果我们想统计每个数据类型中占用内存最多的前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性能变慢。
希望这节课的答疑,能帮助你更加深入地理解前面学过的内容。通过这节课,你应该也看到了,课后思考题是一种很好地梳理重点内容、拓展思路的方式,所以,在接下来的课程里,希望你能多留言聊一聊你的想法,这样可以进一步巩固你所学的知识。而且,还能在和其他同学的交流中,收获更多东西。好了,这节课就到这里,我们下节课见。