1、11 讲:用于存储图片 ID 及其图片存储对象 ID 这样的键值对数据除了使用 String 类型还有 Hash 类型以外,还可以使用什么数据类型进行存储?
- 使用 String 类型进行存储的时候需要大量额外的空间用于存储元数据,因此会消耗大量的内存,并不是最优选择,因此选择将图片 ID 的前 7 位作为 Hash 数据类型的 key,其中 Hash 集合类型中的键值对就使用图片 ID 的后三位作为 key,用图片存储对象 ID 作为 value。就这样使用二级编码的方式用 Redis 中的集合数据类型来存储键值对数据,并且控制 Hash 类型中的两个阈值,避免 Hash 类型将压缩列表的底层结构变成哈希表的为底层实现的数据结构,分别是 Hash-max-zplist-entries 和 Hash-max-zplist-value Hash 中使用压缩列表作为底层结构的 entries 最大个数及其最大单个 entry 的长度。使用压缩列表为底层实现结构就是为了利用好压缩列表节省内存空间的好处。
- 其中除了可以使用 Hash 这样的集合类型来存储这些键值对以外,还可以使用 Sorted Set 集合数据类型,将图片 ID 作为存储的 key 的权重值,用图片存储对象 ID 作为 Sorted Set 中的 key。但是由于 Sorted Set 在插入数据的时候需要根据数据的权重进行有序的插入,因此相对于 Hash 插入数据,需要额外的排序存储的消耗,性能较差。
2、14 讲:我们可以使用 Sorted Set 保存时间序列数据,把时间戳作为score(数据权重),把实际的数据作为 member(value),你觉得这样保存数据有没有潜在的风险?另外,如果你是 Redis 的开发维护者,你会把聚合计算也设计为 Sorted Set 的一个内在功能吗?
- 使用 Sorted Set 或者是 Set 用于存储时间戳数据本身就是存在数据丢失的风险的,由于这两种数据类型在存储数据的时候会对数据进行去重操作,然而不同时间戳下记录的状态可能是相同的,如果之后又状态相同的状态数据存储到 Sorted Set 中就会对原先存储的数据进行覆盖,因此就会丢失被覆盖的时间戳状态数据。
- 对于单线程模型的 Redis 本身对数据进行读写的操作就需要消耗大量的 CPU 资源,虽然 Redis 中某些比较消耗 CPU 资源的任务选择采用 Redis 的子进程或者是后台线程来负责执行,但是对于键值对的增删操作仍然是由 Redis 主线程负责执行,而对于集合的聚合操作本身就是时间复杂度比较高的操作,为了避免对主线程造成阻塞,影响到 Redis 进行数据的正常的读写操作,除非改变 Redis 的线程模型,即使用额外的线程池来负责聚合操作,否则不会将 Sorted Set 中的聚合操作作为一个内在的功能。
3、15讲:如果一个生产者发送给消息队列的消息,需要被多个消费者进行读取和处理(例如,一个消息是一条从业务系统采集的数据,既要被消费者 1 读取进行实时计算,也要被消费者 2 读取并留存到分布式文件系统 HDFS 中,以便后续进行历史查询),你会使用 Redis 的什么数据类型来解决这个问题呢?
- 对于生产者生产出来的消息要被多个消费者进行消费,可能通过学习了 15 讲之后第一个想到的就是使用 Streams 作为消息队列,让生产者讲生产出来的消息放到消息队列中供消费者或者是消费组中的消费者获取并处理。但是需要注意的是,同一个消费组中的消费者在获取消息的时候是具有排斥性的,同一个消费组中的消费者只能由一个消费者可以获取到消息,因此如果是使用 Streams 数据类型,让生产者生成的消息放入到该消息队列中,之后讲两个消费者放到不同的消费组中,这样它们都可以从消息队列中获取消息。
- 在 Redis 中的字典和链表数据结构中,实现了发布和订阅的功能,同样也可以满足上面多个消费者的需求。
4、16 讲:Redis 的写操作(例如 SET、HSET、SADD 等)是在关键路径上吗?
- Redis 中的写操作都需要在内存中执行完毕之后才能返回响应结果,因此这些写操作就属于关键路径上的操作。这里需要注意的是,Redis 在执行 SET,HSET,SADD 这类的写操作的时候与进行磁盘写是不一样的,磁盘写通常只需要将数据写入到所属操作系统的内核缓冲区中即可返回,不需要等到将数据在磁盘中完成写命令执行完毕,因此不属于关键路径上的操作,但是对于 AOF 日志的 always 同步写策略需要由子进程调用 fsync 将数据写入到磁盘中之后才能返回,该磁盘写操作属于关键路径上的操作。
- 其中关于如果 Redis 的内存的写操作的返回仅仅只是个 OK 或者是客户端并不在乎其返回的结果,那么可以将这些内存写操作视作为非关键路径上的操作嘛?答案是不能的,因为即使客户端可能并不在于其写操作的返回值,但是一般对于客户端而言每次发送一个命令之后都会等到返回结果之后才会向服务器继续发送任务,除非 Redis 使用异步线程用于发送和接收请求数据及其无关紧要的响应数据。
5、17讲:在一台有 2 个 CPU Socket(每个 Socket 8 个物理核)的服务器上,我们部署了有 8 个实例的 Redis 切片集群(8 个实例都为主节点,没有主备关系),现在有两个方案:
- 在同一个 CPU Socket 上运行 8 个实例,让他们分别绑定该 CPU Socket 下的不同的物理核。
- 在 2 个 CPU Socket 上各运行 4 个实例,并和相应 Socket 上的核绑定。
- 会选择使用方案二,由于部署在同一个 CPU Socket 下的物理核会共享使用当前 CPU Socket 中的三级缓存,当单个CPU Socket 下运行的 Redis 实例个数过多,就会使得 Redis 实例之间使用三级缓存竞争压力比较大,使得其三级缓存中的单个 Redis 实例需要频繁使用的命令数据的命中率下降,使得降低了 Redis 的性能。
- 同时单个 CPU Socket 中的直接的内存的容量是有限的,当单个 CPU Socket 中运行的 Redis 实例过多,那么每个 Redis 实例能使用的内存的容量变得非常有限,如果切片集群中某个 Redis 实例中需要保存的数据量比较大,就很容易造成其他的 Redis 实例在运行过程中因此内存空间不足,进而需要跨 Socket 申请内存。导致使用跨 Socket 访问内存,从而降低了 Redis 的性能。
- 同时切片集群中 Redis 实例之间进行信息传递及其数据迁移都是采用网络通信的方式,即使切片集群中 Redis 实例运行在不同的 Socket 上,各个实例之间也不需要采用跨 Socket 访问内存,因此不受跨 Socket 访问内存的负面影响。
6、18讲:在 Redis 中,还有哪些命令可以代替 KEYS 命令,实现对键值对的 key 的模糊查询呢?这些命令的复杂度会导致 Redis 变慢吗?
使用 SCAN 命令进行键的模糊查询,SCAN 采用的是可以指定返回数据的数量,因此对 Redis 的性能影响较低。
HSCAN user 0 match "103*" 100
7、19讲:Redis 变慢的情况汇总
- 使用了复杂度过高命令操作或者是一次性返回大量数据的全量查询操作
- 操作了 bigkey
- 在同一时刻有大量的 key 过期
- 内存达到了 maxmemory
- 客户端使用短连接与 Redis 服务器连接
- 当 Redis 中存储的数据量过大,都会导致无论是生成 RDB 文件还是 AOF 重写,其中需要主线程 fark 子进程的耗时严重。
- AOF 文件的写回策略使用了 Alwarys 同步写策略,使得每次执行依次写操作都需要等待将该写操作记录刷磁盘的时间消耗
- Redis 实例运行的内存不足,导致操作系列执行 swap 操作,而 Redis 就需要到 swap 磁盘分区读取数据。
- Redis 实例中进程绑定的 CPU 不合理,使得与 Redis 主线程竞争 CPU 资源。
- Redis 实例运行的机器使用的内存大页机制
- 网卡压力过大,使得网络应用程序再从网卡读取网络数据耗时时间拉长,增加了 Redis 实例从网络应用程序获取网络信息的延迟增加。
8、20讲:我们可以使用 mem_fragmentation_ratio 来判断 Redis 当前的内存碎片率是否严重,我给出的经验阈值都是大于 1 的。我想请你思考一下,如果mem_fragmentation_ratio 小于 1,Redis 的内存使用是什么情况呢?会对 Redis 的性能和内存空间利用率造成什么影响呢?
说明此时系统分配给 Redis 实例的实际内存小于 Redis 存储数据的基本内存大小,说明此时 Redis 已经执行了 swap 了,此时 Redis 的读写性能大大降低了,因此 Redis 中的部分数据存储到了 swap 的磁盘分区中,当对这些数据进行读写操作的时候就需要涉及到慢速的磁盘 IO 操作了。
9、21讲:在和 Redis 实例交互时,应用程序中使用的客户端需要使用缓冲区吗?如果使用的话,对 Redis 的性能和内存使用会有影响吗?
Redis 的客户端使用缓冲区有两点好处:
- 由于 Redis 的客户端有发送请求的缓冲区,那么就可以在客户端进行对发送给服务器的请求并发数量进行控制,避免大量的请求一下子发送到 Redis 实例中,使得 Redis 实例由于压力过大,使其性能降低了。
- 当 Redis 主库发生了宕机的时候,需要进行主从切换的过程, 此时由于 Redis 集群不能执行写命令就会导致客户端发送的请求后得到的响应是无法服务的错误,但是由于客户端有了缓冲区,那么客户端可以正常的发送请求,避免直接请求返回无法服务的错误。