Day748.Redis常见问题② -Redis 核心技术与实战
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day748.Redis常见问题② -Redis 核心技术与实战相关的知识,希望对你有一定的参考价值。
Redis常见问题②
Hi,我是阿昌
,今天记录统计聚合一下前面学习过来的一系列Redis常见问题②
。
1、除了 String 类型和 Hash 类型,还有什么类型适合保存在【String数据结构内存空间消耗问题】所说的图片吗?
除了 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 类型虽然可以用来保存,但并不是最优选项。
2、在【亿个keys数据统计方案】中4 种典型的统计模式,分别是聚合统计、排序统计、二值状态统计和基数统计,以及它们各自适合的集合类型。还遇到过其他的统计场景吗?用的是什么集合类型呢?
一种场景:使用 List+Lua 统计
最近 200 个客户的触达率。
具体做法是,每个 List 元素表示一个客户,元素值为 0,代表触达;元素值为 1,就代表未触达。
在进行统计时,应用程序会把代表客户的元素写入队列中。
当需要统计触达率时,就使用 LRANGE key 0 -1
取出全部元素,计算 0 的比例,这个比例就是触达率
。
这个例子需要获取全部元素,不过数据量只有 200 个,不算大,所以,使用 List,在实际应用中也是可以接受的。
但是,如果数据量很大,又有其他查询需求的话(例如查询单个元素的触达情况),List 的操作复杂度较高
,就不合适了,可以考虑使用 Hash 类型
。
3、【GEO经纬度数据结构&自定义数据结构】在日常的实践过程中,还用过 Redis 的其他数据类型吗?
除了介绍的 5 大基本数据类型,以及 HyperLogLog、Bitmap、GEO,Redis 还有一种数据类型,叫作布隆过滤器
。
它的查询效率很高,经常会用在缓存场景中,可以用来判断数据是否存在缓存中。
4、【如何保存时间序列数据】在用 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 的内在功能实现
的。
5、【Redis消息队列】如果一个生产者发送给消息队列的消息,需要被多个消费者进行读取和处理(例如,一个消息是一条从业务系统采集的数据,既要被消费者 1 读取并进行实时计算,也要被消费者 2 读取并留存到分布式文件系统 HDFS 中,以便后续进行历史查询),你会使用 Redis 的什么数据类型来解决这个问题呢?
使用 Streams 数据类型
的消费组,同时消费生产者的数据。
但是,有个地方需要注意,如果只是使用一个消费组的话,消费组内的多个消费者在消费消息时是互斥
的,换句话说,在一个消费组内,一个消息只能被一个消费者消费。
希望消息既要被消费者 1 读取,也要被消费者 2 读取,是一个多消费者的需求。
所以,如果使用消费组模式
,需要让消费者 1 和消费者 2 属于不同的消费组,这样它们就能同时消费了。
另外,Redis 基于字典
和链表数据结构
,实现了发布和订阅功能
,这个功能可以实现一个消息被多个消费者消费使用,可以满足问题中的场景需求。
6、【Redis阻塞主线程的问题】Redis 的写操作(例如 SET、HSET、SADD 等)是在关键路径上吗?
Redis 本身是内存数据库
,所以,写操作都需要在内存上完成执行后才能返回,这就意味着,如果这些写操作处理的是大数据集,例如 1 万个数据,那么,主线程需要等这 1 万个数据都写完,才能继续执行后面的命令。
所以说,Redis 的写操作
也是在关键路径上
的。
这个问题是希望把面向内存
和面向磁盘
的写操作区分开。
当一个写操作需要把数据写到磁盘时,一般来说,写操作只要把数据写到操作系统的内核缓冲区就行
。不过,如果执行了同步写操作,那就必须要等到数据写回磁盘。所以,面向磁盘的写操作一般不会在关键路径上。根据写操作命令的返回值来决定是否在关键路径上,如果返回值是 OK,或者客户端不关心是否写成功,那么,此时的写操作就不算在关键路径上。这个思路不错,不过,需要注意的是,客户端经常会阻塞等待发送的命令返回结果,在上一个命令还没有返回结果前,客户端会一直等待,直到返回结果后,才会发送下一个命令。此时,即使我们不关心返回结果,客户端也要等到写操作执行完成才行。
所以,在不关心写操作返回结果的场景下,可以对 Redis 客户端做异步改造
。
具体点说,就是使用异步线程发送这些不关心返回结果的命令,而不是在 Redis 客户端中等待这些命令的结果。
7、【CPU结构影响Redis的性能问题】在一台有两个 CPU Socket(每个 Socket 8 个物理核)的服务器上,我们部署了一个有着 8 个实例的 Redis 切片集群(8 个实例都为主节点,没有主备关系),现在有两个方案:
- 在同一个 CPU Socket 上运行 8 个实例,并和 8 个 CPU 核绑定;
- 在两个 CPU Socket 上各运行 4 个实例,并和相应 Socket 上的核绑定。
如果不考虑网络数据读取的影响,你会选择哪个方案呢?
建议使用第二个方案
,主要有两方面的原因。
- 同一个 CPU Socket 上的进程,会
共享 L3 缓存
。如果把 8 个实例都部署在同一个 Socket 上,它们会竞争 L3 缓存
,这就会导致它们的 L3 缓存命中率降低,影响访问性能。 - 同一个 CPU Socket 上的进程,会使用同一个 Socket 上的内存空间。8 个实例共享同一个 Socket 上的内存空间,肯定会
竞争内存资源
。如果有实例保存的数据量大,其他实例能用到的内存空间可能就不够了,此时,其他实例就会跨 Socket 申请内存,进而造成跨 Socket 访问内存,造成实例的性能降低
。
另外,在切片集群中,不同实例间通过网络进行消息通信和数据迁移,并不会使用共享内存空间进行跨实例的数据访问。
所以,即使把不同的实例部署到不同的 Socket 上,它们之间也不会发生跨 Socket 内存的访问,不会受跨 Socket 内存访问的负面影响。
8、【如何应对变慢的Redis①】在 Redis 中,还有哪些命令可以代替 KEYS 命令,实现对键值对的 key 的模糊查询呢?这些命令的复杂度会导致 Redis 变慢吗?
Redis 提供的 SCAN 命令
,以及针对集合类型数据提供的 SSCAN、HSCAN 等,可以根据执行时设定的数量参数,返回指定数量的数据,这就可以避免像 KEYS 命令一样同时返回所有匹配的数据,不会导致 Redis 变慢。
以 HSCAN 为例,可以执行下面的命令,从 user 这个 Hash 集合中返回 key 前缀以 103 开头的 100 个键值对。
HSCAN user 0 match "103*" 100
9、【如何应对变慢的Redis②】你遇到过 Redis 变慢的情况吗?如果有的话,你是怎么解决的呢?
- 使用复杂度过高的命令或一次查询全量数据;
- 操作 bigkey;
- 大量 key 集中过期;
- 内存达到 maxmemory;
- 客户端使用短连接和 Redis 相连;
- 当 Redis 实例的数据量大时,无论是生成 RDB,还是 AOF 重写,都会导致 fork 耗时严重;
- AOF 的写回策略为 always,导致每个操作都要同步刷回磁盘;
- Redis 实例运行机器的内存不足,导致 swap 发生,Redis 需要到 swap 分区读取数据;
- 进程绑定 CPU 不合理;
- Redis 实例运行机器上开启了透明内存大页机制;
- 网卡压力过大。
10、【Redis内存碎片问题】我们可以使用 mem_fragmentation_ratio 来判断 Redis 当前的内存碎片率是否严重,我给出的经验阈值都是大于 1 的。我想请你思考一下,如果 mem_fragmentation_ratio 小于 1,Redis 的内存使用是什么情况呢?会对 Redis 的性能和内存空间利用率造成什么影响呢?
如果 mem_fragmentation_ratio
小于 1,就表明,操作系统分配给 Redis 的内存空间已经小于 Redis 所申请的空间大小了,此时,运行 Redis 实例的服务器上的内存已经不够
用了,可能已经发生 swap
了。
这样一来,Redis 的读写性能也会受到影响,因为 Redis 实例需要在磁盘上的 swap 分区中读写数据,速度较慢
。
11、【Redis缓冲区】在和 Redis 实例交互时,应用程序中使用的客户端需要使用缓冲区吗?如果使用的话,对 Redis 的性能和内存使用会有影响吗?
应用程序中使用的 Redis 客户端,需要把要发送的请求暂存在缓冲区。这有两方面的好处。
- 一方面,可以在客户端
控制发送速率
,避免把过多的请求一下子全部发到 Redis 实例,导致实例因压力过大而性能下降。不过,客户端缓冲区不会太大,所以,对 Redis 实例的内存使用没有什么影响。 - 另一方面,在应用 Redis 主从集群时,主从节点进行故障切换是需要一定时间的,此时,主节点无法服务外来请求。如果客户端有缓冲区
暂存请求
,那么,客户端仍然可以正常接收业务应用的请求,这就可以避免直接给应用返回无法服务的错误
。
12、如何使用慢查询日志和 latency monitor 排查执行慢的操作?
可以使用 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 //最大的超过阈值的延迟
13、如何排查 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。
以上是关于Day748.Redis常见问题② -Redis 核心技术与实战的主要内容,如果未能解决你的问题,请参考以下文章