Redis|进阶

01:redis 有哪些慢操作

redis为什么快

  • 它是内存数据库,存取快

  • 具有优秀的底层数据结构

    string->简单动态字符串

    List-> 压缩列表或双向链表

    Hash:压缩类表或字典

    有序集合:压缩列表或跳表+字典 集合:字典或整数集合

键和值用什么结构组织

  • Redis 使用了一个字典来保存所有键值对,哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。

为什么哈希表操作变慢了和rehash

  • 拉链法解决哈希冲突,指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。如果链过长,需要顺序查找,耗时长。

    解决办法:rehash,增大哈希桶的数量,使拉链变短。

  • 为了使 rehash 操作更高效,Redis 存储键值对的字典里保存了两个全局哈希表。rehash过程:

    1. 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
    2. 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
    3. 释放哈希表 1 的空间。
    4. 将哈希表1和哈希表2交换
  • 渐进式 rehash

    为了减少阻塞,使用了渐进式rehash。每次对字典进行添加,删除,查找,修改,更新时,将一个桶上的键进行rehash。

    (1)删除和查找:在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找、更新等操作会在两个哈希表上进行。比如说,要在字典里面查找一个键的话,程序会先在ht[0]里面进行查找,如果没找到的话,就会继续到ht[1]里面进行查找,诸如此类。

    (2)新增数据:在渐进式 rehash 执行期间,新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作。这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。

集合数据操作效率

  • 一个集合类型的值,第一步是通过全局哈希表找到对应的哈希桶位置,第二步是在集合中再增删改查。

有哪些底层数据结构?

  • 集合类型的底层数据结构主要有 5 种:整数数组、双向链表、字典(哈希表)、压缩列表和跳表。

  • 压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。

    在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。

  • 跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位

    过程就是在多级索引上跳来跳去,最后定位到元素。这也正好符合“跳”表的叫法。当数据量很大时,跳表的查找复杂度就是 O(logN)。

不同操作的复杂度

  • 单元素操作是基础。单元素操作,是指每一种集合类型对单个数据实现的增删改查操作
  • 范围操作非常耗时;范围操作,是指集合类型中的遍历操作,可以返回集合中的所有数据这类操作的复杂度一般是 O(N),比较耗时,应该尽量避免
  • 统计操作通常高效;统计操作,是指集合类型对集合中所有元素个数的记录。当集合类型采用压缩列表、双向链表、整数数组,字典这些数据结构时,这些结构中专门记录了元素的个数统计,因此可以高效地完成相关操作。
  • 例外情况只有几个。是指某些数据结构的特殊记录,例如压缩列表和双向链表都会记录表头和表尾的偏移量。这样一来,对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂度也只有 O(1),可以实现快速操作。

02:高性能IO模型:为什么单线程Redis能那么快

Redis 是单线程,主要是指 Redis 的网络 IO和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程

但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。 严格来说,Redis 并不是单线程。

多线程的开销

  • 多线程编程模式面临的共享资源的并发访问控制问题,多线程编程模式面临的共享资源的并发访问控制问题。
  • 采用多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。为了避免这些问题,Redis 直接采用了单线程模式。

单线程 Redis 为什么那么快?

  • Redis 的大部分操作在内存上完成,它采用了高效的数据结构。
  • Redis 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率

基于多路复用的高性能 I/O 模型

  • Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的select/epoll 机制。
  • 在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。
  • 内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个IO 流的效果

03: AOF日志

目前,Redis 的持久化主要有两大机制,即 AOF 日志和 RDB 快照。

AOF 日志是如何实现的?

  • Redis 是先执行命令,把数据写入内存,然后才记录日志到硬盘。

  • 为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。

  • AOF 里记录的是 Redis 收到的每一条命令语句。以文本形式保存。AOF 日志由主线程完成

  • 它是在命令执行后才记录日志,所以不会阻塞当前的写操作

  • 有两个潜在的风险

    1. 如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。
    2. AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。
  • 三种写回策略

    1. appendfsync = Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘。可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能;

    appendfsync = Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。

    appendfsync = No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不在 Redis 手中了,一旦宕机对应的数据就丢失了

    想要获得高性能,就选择 No 策略;如果想要得到高可靠性保证,就选择Always 策略;如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择Everysec 策略。

AOF 重写机制

  • AOF 是以文件的形式在记录接收到的所有写命令。随着接收的写命令越来越多,AOF 文件会越来越大。

    一是,文件系统本身对文件大小有限制,无法保存过大的文件

    二是,如果文件太大,之后再往里面追加命令记录的话,效率也会变低;

    三是,如果发生宕机,AOF 中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到 Redis 的正常使用。

  • Redis 根据数据库的现状创建一个新的 AOF 文件,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入,记录的等价命令。要把整个数据库的最新数据的操作日志都写回磁盘,仍然是一个非常耗时的过程。

  • AOF 重写会阻塞主线程吗?

    和 AOF 日志由主线程写回不同,重写过程是由后台线程 bgrewriteaof 来完成的。避免阻塞主线程,导致数据库性能下降。

    每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。

    同时把操作日志写到AOF日志缓冲区和重写AOF日志的缓冲区。当重写AOF日志完成后,把缓冲区中的日志写到重写后的AOF日志后面。

04:RDB日志

综述

用 AOF 方法进行故障恢复的时候,需要逐一把操作日志都执行一遍。如果操作日志非常多,Redis 就会恢复得很缓慢,影响到正常使用。

把某一时刻的状态以文件的形式写到磁盘上,也就是快照。这样一来,即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。这个快照文件就称为 RDB 文件

和 AOF 相比,RDB 记录的是某一时刻的数据,并不是操作,所以,在做数据恢复时,可以直接把 RDB 文件读入内存,很快地完成恢复。

给哪些内存数据做快照?

  • Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中,给内存的全量数据做快照,把它们全部写入磁盘也会花费很多时间。而且,全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大

会阻塞主线程吗?

  • save:在主线程中执行,会导致阻塞;
  • bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是Redis RDB 文件生成的默认配置。Redis 会 fork 子进程来完成,fork 操作的用时和 Redis 的数据量是正相关的而 fork 在执行时会阻塞主线程。数据量越大,fork 操作造成的主线程阻塞的时间越长。

快照时数据能修改吗?

  • Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW)(类似于undolog),在执行快照的同时,正常处理写操作。
  • bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。如果在读取过程中,有对数据修改的操作,操作系统会复制出一个副本,对该副本执行修改操作。bgsave所关联的内存并没有修改。

可以每秒做一次快照吗?

  • 虽然 bgsave 执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销。

    一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。

    bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。

混合使用 AOF 日志和内存快照

  • 内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
  • 快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。

05: 数据同步:主从库如何实现数据一致

Redis 具有高可靠性,这里有两层含义:一是数据尽量少丢失,二是服务尽量少中断。AOF 和 RDB 保证了前者,主从复制和哨兵机制保证后者。

数据如何保持一致呢?

  • Redis 提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式。

    读操作:主库、从库都可以接收;

    写操作:首先到主库执行,然后,主库将写操作同步给从库

  • 主从库模式一旦采用了读写分离,所有数据的修改只会在主库上进行,不用协调多个实例。主库有了最新的数据后,会同步给从库,这样,主从库的数据就是一致的。

主从库间如何进行第一次同步?

  • 可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系

    第一阶段是主从库间建立连接、协商同步的过程,主要是为全量复制做准备。在这一步,从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。

    在第二阶段,主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。**主库执行 bgsave 命令,生成 RDB 文件,**接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录RDB 文件生成后收到的所有写操作。

    第三阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。

  • 主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。

主从级联模式分担全量复制时的主库压力

  • 如果从库数量很多,而且都要和主库进行全量复制的话,就会导致主库忙于 fork 子进程生成 RDB 文件,进行数据全量同步。fork 这个操作会阻塞主线程处理正常请求,从而导致主库响应应用程序的请求速度变慢。此外,传输 RDB 文件也会占用主库的网络带宽,同样会给主库的资源使用带来压力。

    可以通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。在部署主从集群的时候,可以手动选择一个从库(比如选择内存资源配置较高的从库),用于级联其他的从库。其他从库级联这个从库。

主从库间网络断了怎么办?

  • 在 Redis 2.8 之前,如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。
  • 从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。增量复制只会把主从库网络断连期间主库收到的命令,同步给从库。通过从复制积压缓冲区内读入没有完成的命令。如果缓冲区内没有包含全部缺失命令,就进行全量复制。
  • repl_backlog_buffer:repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。则会进行全量复制。

06: 哨兵机制:主库挂了,如何不间断服务?

在 Redis 主从集群中,哨兵机制是实现主从库自动切换的关键机制,哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。

哨兵机制的基本流程

  • 哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。

    监控:哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行

    选主:主库挂了以后,哨兵就需要从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库

    通知:哨兵会把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。

主观下线和客观下线

  • 主观下线

    哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”。

  • 客观下线

    哨兵机制通常会采用多实例组成的集群模式进行部署,这也被称为哨兵集群。引引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。

    “客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”。这个所需的赞成票数是通过哨兵配置文件中的 quorum 配置项设定的。

如何选定新主库?

  • 筛选:多个从库中,先按照一定的筛选条件,把不符合条件的从库去掉。当前在线状态,判断它之前的网络连接状态

  • 打分:再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库。从库优先级、从库复制进度以及从库 ID 号

    07:哨兵集群

一旦多个实例组成了哨兵集群,即使有哨兵实例出现故障挂掉了,其他哨兵还能继续协作完成主从库切换的工作,包括判定主库是不是处于下线状态,选择新主库,以及通知从库和客户端。

基于 pub/sub 机制的哨兵集群组成

  • 哨兵实例之间可以相互发现,要归功于 Redis 提供的 pub/sub 机制,也就是发布 / 订阅机制
  • 哨兵订阅主库的频道,特定频率向该频道发送消息,其他哨兵可以收到,这样哨兵之间就发现了彼此,从而建立连接。

哨兵是如何知道从库的 IP 地址和端口的呢?

  • 哨兵向主库发送 INFO 命令来完成的。主库接受到这个命令后,就会把从库列表返回给哨兵。哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。

基于 pub/sub 机制的客户端事件通知

  • 哨兵就是一个运行在特定模式下的 Redis 实例,每个哨兵实例也提供 pub/sub 机制,客户端可以从哨兵订阅消息。哨兵提供的消息订阅频道有很多,不同频道包含了主从库切换过程中的不同关键事件。

由哪个哨兵执行主从切换?

  1. 投票仲裁,标记客观下线:拿到半数以上的赞成票,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
  2. 投片选举leader:主观认为下线的哨兵就可以再给其他哨兵发送命令,表明希望由自己来执行主从切换,并让所有其他哨兵进行投票。这个投票过程称为“Leader 选举”。在投票过程中,任何一个想成为 Leader 的哨兵,要满足两个条件:第一,拿到半数以上的赞成票;第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
  3. 如果没有满足上述两个条件,不会产生 Leader。哨兵集群会等待一段时间(也就是哨兵故障转移超时时间的 2 倍),再重新选举。
  4. 注意:如果只有 2 个实例,一个哨兵要想成为 Leader,必须获得2 票,而不是 1 票。如果有个哨兵挂掉了,则会导致集群是无法进行主从库切换的。因此,至少配置 3 个哨兵实例。

08:切片集群

切片集群,也叫分片集群,就是指启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。

如何保存更多数据?

  • 纵向扩展:升级单个 Redis 实例的资源配置,让它变硬变强。

    优点:实施起来简单、直接

    缺点:当使用 RDB 对数据进行持久化时,如果数据量增加,主线程 fork 子进程时就可能会阻塞。

    纵向扩展会受到硬件和成本的限制

  • 横向扩展:横向增加当前 Redis 实例的个数,人多力量大。

    优点:成本低,不用担心单个实例的硬件和成本限制。在面向百万、千万级别的用户规模时,横向扩展的 Redis 切片集群会是一个非常好的选择。

    缺点:复杂性提高

数据切片和实例的对应分布关系

  • 哈希槽:一个切片集群共有 16384个哈希槽,每个键值对都会根据它的 key,被映射到一个哈希槽中。
  • 先根据键值对的 key,按照CRC16 算法计算一个 16 bit的值;
  • 然后对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
  • 在部署 Redis Cluster 方案时,可以使用 cluster create 命令创建集群,Redis会自动把这些槽平均分布在集群实例上。
  • 在手动分配哈希槽时,需要把 16384 个槽都分配完,否则Redis 集群无法正常工作。

客户端如何定位数据?

  • key到哈希槽:可以通过计算得到的,这个计算可以在客户端发送请求时,由客户端计算出来。

  • 哈希槽到实例:客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端。

  • Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。

  • 客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。

  • 实例和哈希槽的对应关系并不是一成不变的

    在集群中,实例有新增或删除,Redis 需要重新分配哈希槽; 为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍

  • 重定向机制:当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,这个实例就会给客户端返回下 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。客户端会根据moved信息更新本地缓存

  • 迁移一半

    • 在原来实例中:直接执行

    • 不在原来实例中:返回ASK命令

    • 和 MOVED 命令不同,ASK 命令并不会更新客户端缓存的哈希槽分配信息。

    • 客户端收到ask命令后,会先向目服务器发送asking命令,再发送语句,强制目标服务器执行下该命令。

09: string对象的特性

它有一个明显的短板,就是它保存数据时额外所消耗的内存空间较多。

为什么 String 类型内存开销大?

  • 底层是SDS两个额外信息,额外消耗8字节
    1. len:4字节,已经使用的长度
    2. free:4字节,剩余空间
    3. 真实数据:保存在数组中。(指针指向数组)
  • string对象:额外16字节
    1. 元数据:8字节
    2. 指针:8字节
  • 数据库保存键值对,一个键值对有一个这样的结构 value指针:指向value,8字节 next指针:指向下一个键值对,拉链发的next,8字节 共24字节,实际分配32字节。

用什么数据结构可以节省内存?

  • 压缩列表这是一种非常节省内存的结构。

    entry 会挨个儿放置在内存中,不需要再用额外的指针进行连接,这样就可以节省指针所占用的空间。

如何用集合类型保存单值的键值对?

  • 在保存单值的键值对时,可以采用基于 Hash 类型的二级编码方法。

  • 这里说的二级编码,就是把一个单值的数据拆分成两部分,前一部分作为 Hash 集合的 key,后一部分作为Hash 集合的 value,这样一来,我们就可以把单值数据保存到 Hash 集合中了。

  • 二级编码方法中采用的 ID 长度

    Hash类型设置了用压缩列表保存数据时的两个阈值,一旦超过了阈值,Hash 类型就会用哈希表来保存数据

    hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。

    hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度

    为了能充分使用压缩列表的精简内存布局,我们一般要控制保存在 Hash 集合中的元素个数。

10:大量数据的统计

聚合统计

  • 多个集合元素的聚合结果:交集,差集,并集。
  • Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞
  • 可以从主从集群中选择一个从库,让它专门负责聚合计算,或者是把数据读取到客户端,在客户端来完成聚合统计

排序统计

  • List:List 是按照元素进入 List 的顺序进行排序的。

    List 是通过元素在 List 中的位置来排序的,当有一个新元素插入时,原先的元素在 List 中的位置都后移了一位,对比新元素插入前后,List 相同位置上的元素就会发生变化,用LRANGE 读取时,就会读到旧元素。

  • Sorted Set:Sorted Set 可以根据元素的权重来排序

    即使集合中的元素频繁更新,Sorted Set 也能通过 ZRANGEBYSCORE 命令准确地获取到按序排列的数据

  • 在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,建议你优先考虑使用 Sorted Set。

二值状态统计

  • 二值状态统计:二值状态就是指集合元素的取值就只有 0 和 1 两种。

  • Bitmap

    Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型

    GETBIT/SETBIT 操作,使用一个偏移值 offset 对 bit 数组的某一个 bit 位进行读和写

    BITCOUNT 操作,用来统计这个 bit 数组中所有“1”的个数

    BITOP 命令对多个 Bitmap 按位做“与”“或”“异或”的操作,操作的结果会保存到一个新的 Bitmap 中。

基数统计

  • 基数统计就是指统计一个集合中不重复的元素个数。

  • Set 类型默认支持去重。元素很多时,会消耗很大的内存空间

  • HyperLogLog

    HyperLogLog 是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小。

    PFADD 命令用于向 HyperLogLog 中添加新元素

    PFCOUNT 命令的作用就是返回 HyperLogLog 的统计结果。

    它给出的统计结果是有一定误差的,标准误算率是 0.81%

11:GEO是什么

面向 LBS 应用的 GEO 数据类型

LBS 应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO 就非常适合应用在LBS 服务的场景中

GEO 的底层结构

  • 需要实现的功能:

    保存位置

    查询附近的位置

  • GEO 类型的底层数据结构就是用 Sorted Set 来实现的,Sorted Set 的元素是车辆 ID,元素的权重分数是经纬度信息.

GeoHash 的编码方法

  • 基本原理就是“二分区间,区间编码”,先对经度和纬度分别编码,然后再把经纬度各自的编码组合成一个最终编码。
  • 分别对经纬度不断二分区间,左边的记做0, 右边记做1,最后得到精度合适的 0 1编码的经纬度。最终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值,其中,偶数位从 0 开始,奇数位从 1 开始。
  • 使用 Sorted Set 范围查询得到的相近编码值,在实际的地理空间上,也是相邻的方格,这就可以实现 LBS 应用“搜索附近的人或物”的功能了。
  • 但是,有的编码值虽然在大小上接近,但实际对应的方格却距离比较远。为了避免查询不准确问题,我们可以同时查询给定经纬度所在的方格周围的 4 个或8 个方格。

如何操作 GEO 类型?

  • GEOADD 命令:用于把一组经纬度信息和相对应的一个 ID 记录到 GEO 类型集合中;
  • GEORADIUS 命令:会根据输入的经纬度位置,查找以这个经纬度为中心的一定范围内的其他元素。当然,我们可以自己定义这个范围

12:如何自定义数据类型

RedisObject 包括元数据和指针。其中,元数据的一个功能就是用来区分不同的数据类型,指针用来指向具体的数据类型的值。

Redis 的基本对象结构

  • RedisObject 的内部组成包括了 type,、encoding,、lru 和 refcount 4 个元数据,以及 1个*ptr指针。

    type:表示值的类型,涵盖了我们前面学习的五大基本类型;

    encoding:是值的编码方式,用来表示 Redis 中实现各个基本类型的底层数据结构, 例如 SDS、压缩列表、哈希表、跳表等;

    lru:记录了这个对象最后一次被访问的时间,用于淘汰过期的键值对;

    refcount:记录了对象的引用计数;

    *ptr:是指向数据的指针。

开发一个新的数据类型

  • 需要为新数 据类型定义好它的底层结构、type 和 encoding 属性值,然后再实现新数据类型的创建、

    释放函数和基本命令。

  • 第一步:定义新数据类型的底层结构

  • 第二步:在 RedisObject 的 type 属性中,增加这个新类型的定义

  • 第三步:开发新类型的创建和释放函数

  • 第四步:开发新类型的命令操作

13:如何在Redis中保存时间序列数据?

时间序列数据些与发生时间相关的一组数据,特点是没有严格的关系模型,记录的信息可以表示成键和值的关系。

时间序列数据的读写特点

  • 持续高并发的插入数据
  • 根据时间进行单条查询和范围查询,聚合查询等。
  • 这种数据的写入特点很简单,就是插入数据快,这就要求我们选择的数据类型,在进行数据插入时,复杂度要低,尽量不要阻塞

基于 Hash 和 Sorted Set 保存时间序列数据

  • Hash 类型:实现对单键的快速查询,把时间戳作为 Hash 集合的 key,把记录的设备状态值作为 Hash 集合的 value。(ID:温度 t1 25.3),应对单值查询。

  • Sorted Set类型:支持按时间戳范围的查询,因为它能够根据元素的权重分数来排序。我们可以把时间戳作为 Sorted Set 集合的元素分数,把时间点上记录的数据作为元素本身。

  • 如何对时间序列数据进行聚合计算?

    只能先把时间范围内的数据取回到客户端,然后在客户端自行完成聚合计算。

    大量数据在 Redis 实例和客户端间频繁传输,这会和其他操作命令竞争网络资源,导致其他操作变慢。

基于 RedisTimeSeries 模块保存时间序列数据

  • 这是专门为存取时间序列数据而设计的扩展模块。和第一种方案相比,RedisTimeSeries 能支持直接在 Redis 实例上进行多种数据聚合计算,避免了大量数据在实例和客户端间传输。不过,RedisTimeSeries 的底层数据结构使用了链表,它的范围查询的复杂度是 O(N) 级别的,同时,它的 TS.GET 查询只能返回最新的数据,没有办法像第一种方案的 Hash 类型一样,可以返回任一时间点的数据。

14:redis实现消息队列

消息队列要能支持组件通信消息的快速读写,而 Redis 本身支持数据的高速访问,正好可以满足消息队列的读写性能需求。

消息队列的消息存取需求

  • 在分布式系统中,当两个组件要基于消息队列进行通信时,一个组件会把要处理的数据以消息的形式传递给消息队列,然后,这个组件就可以继续执行其他操作了;远端的另一个组件从消息队列中把消息读取出来,再在本地进行处理。也就是生产者,消费者模型。
  • 消息保序:虽然消费者是异步处理消息,但是,消费者仍然需要按照生产者发送消息的顺序来处理消息,避免后发送的消息被先处理了。
  • 重复消息处理:消费者从消息队列读取消息时,有时会因为网络堵塞而出现消息重传的情况。消费者可能会收到多条重复的消息。对于重复的消息,消费者如果多次处理的话,就会出现错误
  • 消息可靠性保证:消费者在处理消息的时候,还可能出现因为故障或宕机导致消息没有处理完成的情况。消息队列需要能提供消息可靠性的保证,当消费者重启后,可以重新读取消息再次进行处理。

基于 List 的消息队列解决方案

  • 有序性:List 本身就是按先进先出的顺序对数据进行存取的,所以,如果使用 List 作为消息队列保存消息的话,就已经能满足消息保序的需求了。Redis 提供了 BRPOP 命令。BRPOP 命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。

  • 重复消息处理:要求消费者程序本身能对重复消息进行判断,消息队列要能给每一个消息提供全局唯一的 ID 号;另一方面,消费者程序要把已经处理过的消息的 ID 号记录下来。

  • 可靠性:当消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。为了留存消息,List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份List)留存。

  • 存在问题:

    生产者消息发送很快,而消费者处理消息的速度比较慢,这就导致 List 中的消息越积越多,给 Redis 的内存带来很大压力

    List 类型并不支持消费组的实现。

基于 Streams 的消息队列解决方案

Streams 是Redis 5.0 专门针对消息队列场景设计的数据类型,如果你的 Redis 是 5.0 及 5.0 以后的版本,就可以考虑把 Streams 用作消息队列了

  • Streams 是 Redis 专门为消息队列设计的数据类型,它提供了丰富的消息队列操作命令。

    XADD 命令可以往消息队列中插入新消息,消息的格式是键 - 值对形式。对于插入的每一 条消息,Streams 可以自动为其生成一个全局唯一的 ID。

    XREAD 在读取消息时,可以指定一个消息 ID,并从这个消息 ID 的下一条消息开始进行读取。消费者也可以在调用 XRAED 时设定 block 配置项,实现类似于 BRPOP 的阻塞读取操作。当消息队列中没有消息时,一旦设置了 block 配置项,XREAD 就会阻塞,阻塞的时长可以在 block 配置项进行设置。

  • Streams 本身可以使用 XGROUP 创建消费组,创建消费组之后,Streams 可以使用 XREADGROUP 命令让消费组内的消费者读取消息

  • 为了保证消费者在发生故障或宕机再次重启后,仍然可以读取未处理完的消息,Streams会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息,直到消费者使用 XACK 命令通知 Streams“消息已经处理完成”。如果消费者没有成功处理消息,它就不会给 Streams 发送 XACK 命令,消息仍然会留存。此时,消费者可以在重启后,用 XPENDING 命令查看已读取、但尚未确认处理完成的消息。

15:如何避免单线程模型的阻塞?

集合全量查询和聚合操作;

bigkey 删除;

清空数据库;

AOF 日志同步写;

从库加载 RDB 文件

和客户端交互时的阻塞点

  • 网络 IO 有时候会比较慢,但是 Redis 使用了 IO 多路复用机制,避免了主线程一直处在等待网络连接或请求到来的状态,所以,网络 IO 不是导致 Redis 阻塞的因素。
  • 键值对的增删改查操作是 Redis 主线程执行的主要任务。复杂度高的增删改查操作肯定会阻塞 Redis。
    • 操作的复杂度是否为 O(N)。
    • 集合全量查询聚合操作
    • bigkey 删除:集合自身的删除操作一下子释放了大量内存,例如删除大量键值对数据的时候,最典型的就是删除包含了大量元素的集合。
    • 清空数据库
  • 和磁盘交互时的阻塞点
  • **AOF 日志同步写:**Redis 直接记录 AOF 日志时,会根据不同的写回策略对数据做落盘保存。一个同步写磁盘的操作的耗时大约是 1~2ms,如果有大量的写操作需要记录在 AOF 日志中,并同步写回的话,就会阻塞主线程了。
  • Redis 采用子进程的方式生成 RDB 快照文件,以及执行 AOF 日志重写操作。这两个操作由子进程负责执行,慢速的磁盘 IO 不会阻塞主线程。

主从节点交互时的阻塞点

  • 从库来在接收了RDB 文件后,需要使用 FLUSHDB 命令清空当前数据库。阻塞
  • 从库在清空当前数据库后,需要把 RDB 文件加载到内存,RDB 文件越大,加载过程越慢。阻塞

切片集群实例交互时的阻塞点

  • 每个 Redis 实例上分配的哈希槽信息需要在不同实例间进行传递,同时,当需要进行负载均衡或者有实例增删时,数据会在不同的实例间进行迁移。哈希槽的信息量不大,而数据迁移是渐进式执行的,这两类操作对 Redis 主线程的阻塞风险不大。
  • 使用了 Redis Cluster 方案,而且同时正好迁移的是 bigkey 的话,就会造成主线程的阻塞

哪些阻塞点可以异步执行?

  • 对于 Redis 的五大阻塞点来说,除了“集合全量查询和聚合操作”和“从库加载 RDB 文 件”, bigkey 删除 清空数据 AOF 日志同步****写都不在关键路径上,所以,我们可以使用 Redis 的异步 子线程机制来实现 bigkey 删除,清空数据库,以及 AOF 日志同步写

异步的子线程机制

  • Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别负责 AOF 日志写操作、键值对删除以及文件关闭的异步执行。

  • 主线程通过一个链表形式的任务队列和子线程进行交互。当收到键值对删除和清空数据库的操作时,主线程会把这个操作封装成一个任务,放入到任务队列中,然后给客户端返回一个完成信息,表明删除已经完成。 但实际上,这个时候删除还没有执行,等到后台子线程从任务队列中读取任务后,才开始实际删除键值对,并释放相应的内存空间。因此,我们把这种异步删除也称为惰性删除 (lazy free)。此时,删除或清空操作不会阻塞主线程,这就避免了对主线程的性能影响。

  • 和惰性删除类似,当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到任务队列中。后台子线程读取任务后,开始自行写入 AOF 日志,这样主线程就不用一直等待 AOF 日志写完了。

  • 这里有个地方需要你注意一下,异步的键值对删除和数据库清空操作是 Redis 4.0 后提供的功能,Redis 也提供了新的命令来执行这两个操作。

    键值对删除:当你的集合类型中有大量元素(例如有百万级别或千万级别元素)需要删除时,我建议你使用 UNLINK 命令。

    清空数据库:可以在 FLUSHDB 和 FLUSHALL 命令后加上 ASYNC 选项,这样就可以让后台子线程异步地清空数据库。

集合全量查询和聚合操作、从库加载 RDB 文件是在关键路径上,无法使用异步操作来完成。

集合全量查询和聚合操作:可以使用 SCAN 命令,分批读取数据,再在客户端进行聚合计算;

从库加载 RDB 文件:把主库的数据量大小控制在 2~4GB 左右,以保证 RDB 文件能以较快的速度加载。

真正会阻塞主线程的有:****

  1. 复杂度大的查询操作
  2. RDB文件加载
  3. AOF的同步写入
  4. BGRDB子线程生成

16:如何应对变慢的Redis上

Redis 真的变慢了吗?

大部分时候,Redis 延迟很低,但是在某些时刻,有些 Redis 实例会出现很高的响应延迟,甚至能达到几秒到十几秒,不过持续时间不长,这也叫延迟“毛刺”。

  • 查看 Redis 的响应延迟。当发现Redis 命令的执行时间突然就增长到了几秒,基本就可以认定 Redis 变慢了。

  • 查看 Redis 基线性能

    基线性能:一个系统在低压力、无干扰下的基本性能,这个性能只由当前的软硬件配置决定。

    怎么确定基线性能:redis-cli 命令提供了–intrinsic-latency 选项,可以用来监测和统计测试期间内的最大延迟,这个延迟可以作为 Redis 的基线性能。

  • 结合Redis 的响应延迟和Redis 基线性能综合判断

    把运行时延迟和基线性能进行对比,如果观察到的 Redis 运行时延迟是其基线性能的 2 倍及以上,就可以认定 Redis 变慢了。

慢查询命令

  • 采用了复杂度高的慢查询命令,对集合的遍历,聚合操作等,一个比较容易忽略的慢查询命令,就是 KEYS它用于返回和输入模式匹配的所有key。发现 Redis 性能变慢时,可以通过 Redis 日志,或者是 latency monitor 工具,查询变慢的请求,根据请求对应的具体命令以及官方文档,确认下是否采用了复杂度高的慢查询命令。
  • 决办法:用其他高效命令代替,例如用SSCAN 代替 SMEMBERS。需要执行排序、交集、并集操作时,可以在客户端完成,而不要用 SORT、SUNION、SINTER 这些命令,以免拖慢 Redis 实例。

过期 key 操作

  • 过期 key 的自动删除机制。Redis 键值对的 key 可以设置过期时间。默认情况下,Redis 每 100 毫秒会删除一些过期key。
    1. 采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个数的 key,并将其中过期的key 全部删除;
    2. 如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下。
  • 触发了上面这个算法的第二条,Redis 就会一直删除以释放内存空间,删除操作是阻塞的(Redis 4.0 后可以用异步线程机制来减少阻塞影响)。
  • 算法的第二条是怎么被触发的呢?其中一个重要来源,就是频繁使用带有相同时间参数的 EXPIREAT 命令设置过期 key,这就会导致,在同一秒内有大量的 key 同时过期。
  • 解决办法:不要把大量key设置成同时过期。如果实在需要,就加上一个一定范围内的随机数。

17:如何应对redis变慢下

如果上节课的方法不管用,那就说明,要关注影响性能的其他机制了,也就是文件系统和操作系统。

文件系统:AOF 模式

  • Redis 会采用 AOF 日志或 RDB 快照。其中,AOF日志提供了三种日志写回策略:no、everysec、always。这三种写回策略依赖文件系统的两个系统调用完成,也就是 write 和 fsync。

  • 当写回策略配置为 everysec 和 always 时,Redis 需要调用 fsync 把日志写回磁盘。

    在使用 everysec 时,Redis 允许丢失一秒的操作记录,所以,Redis 主线程并不需要确保 每个操作记录日志都写回磁盘。而且,fsync 的执行时间很长,如果是在 Redis 主线程中 执行 fsync,就容易阻塞主线程。所以,当写回策略配置为 everysec 时,Redis 会使用后 台的子线程异步完成 fsync 的操作。

    而对于 always 策略来说,Redis 需要确保每个操作记录日志都写回磁盘,如果用后台子线 程异步完成,主线程就无法及时地知道每个操作是否已经完成了,这就不符合 always 策略 的要求了。所以**,always 策略并不使用后台子线程来执行。**

  • 在使用 AOF 日志时,为了避免日志文件不断增大,Redis 会执行 AOF 重写,生成 体量缩小的新的 AOF 日志文件。AOF 重写本身需要的时间很长,也容易阻塞 Redis 主线 程,所以,Redis 使用子进程来进行 AOF 重写。

  • 这里有一个潜在的风险点:AOF 重写会对磁盘进行大量 IO 操作,同时,fsync 又需要等到数据写到磁盘后才能返回,所以,当 AOF 重写的压力比较大时,就会导致 fsync 被阻塞。虽然 fsync 是由后台子线程负责执行的,但是,主线程会监控 fsync 的执行进度。

    当主线程使用后台子线程执行了一次 fsync,需要再次把新接收的操作记录写回磁盘时,如果主线程发现上一次的 fsync 还没有执行完,那么它就会阻塞。

  • 如果在重写日志时,AOF 重写子进程的写入量比较大,fsync 线程也会被阻塞,进而阻塞主线程,导致延迟增加。

  • 如果业务应用对延迟非常敏感,但同时允许一定量的数据丢失,那么,可以把配置项 no-appendfsync-on-rewrite 设置为 yes。这个配置项设置为 yes 时,表示在 AOF 重写时,不进行 fsync 操作。也就是说,Redis 实例把写命令写到内存后,不调用后台线程进行 fsync 操作,就可以直接返回了。当然,如果此时实例发生宕机,就会导致数据丢失

  • 如果的确需要高性能,同时也需要高可靠数据保证,我建议你考虑采用高速的固态硬盘作为 AOF 日志的写入设备。

操作系统:swap

  • 正常情况下,Redis 的操作是直接通过访问内存就能完成,一旦 swap 被触发了,Redis 的请求操作需要等到磁盘数据读写完成才行。而且,和我刚才说的 AOF 日志文件读写使用 fsync 线程不同,swap 触发后影响的是 Redis 主 IO 线程,这会极大地增加 Redis 的响应时间。

  • 通常,触发 swap 的原因主要是物理机器内存不足,对于 Redis 而言,有两种常见的情 况:

    1. Redis 实例自身使用了大量的内存,导致物理机器的可用内存不足;
    2. 和 Redis 实例在同一台机器上运行的其他进程,在进行大量的文件读写操作。文件读写 本身会占用系统内存,这会导致分配给 Redis 实例的内存量变少,进而触发 Redis 发生 swap。

    增加机器的内存或者使用 Redis 集群

操作系统:内存大页

内存大页机制(Transparent Huge Page, THP),也会影响 Redis 性能。

  • Redis 为了提供 数据可靠性保证,需要将数据做持久化保存。这个写入过程由额外的线程执行,所以,此 时,Redis 主线程仍然可以接收客户端写请求。客户端的写请求可能会修改正在进行持久 化的数据。在这一过程中,Redis 就会采用写时复制机制,也就是说,一旦有数据要被修改,Redis 并不会直接修改内存中的数据,而是将这些数据拷贝一份,然后再进行修改。
  • 如果采用了内存大页,那么,即使客户端请求只修改 100B 的数据,Redis 也需要拷贝 2MB 的大页。相反,如果是常规内存页机制,只用拷贝 4KB。
  • 很简单,关闭内存大页,就行了。

18:redis删除键命令的策略

在使用 Redis 时,我们经常会遇到这样一个问题:明明做了数据删除,数据量已经不大了,为什么使用 top 命令查看时,还会发现 Redis 占用了很多内存呢?

当数据删除后,Redis 释放的内存空间会由内存分配器管理,并不会立即返回给操作系统。所以,操作系统仍然会记录着给 Redis 分配了大量内存。

Redis 释放的内存空间可能并不是连续的,那么,这些不连续的内存空间很有可能处于一种闲置的状态。这就会导致一个问题:虽然有空闲空间,Redis 却无法用来保存数据,不仅会减少 Redis 能够实际保存的数据量,还会降低 Redis 运行机器的成本回报率。

什么是内存碎片?

  • 虽然操作系统的剩余内存空间总量足够,但是,应用申请的是一块连续地址空间的 N 字节,但在剩余的内存空间中,没有大小为 N 字节的连续空间了,那么,这些剩余空间就是内存碎片。
  • 虽然操作系统的剩余内存空间总量足够,但是,应用申请的是一块连 续地址空间的 N 字节,但在剩余的内存空间中,没有大小为 N 字节的连续空间了,那么, 这些剩余空间就是内存碎片

内存碎片是如何形成的?

  • 内因:内存分配器的分配策略

    一般是按固定大小来分配内存,而不是完全按照应用程序申请的内存空间大小给程序分配。

    emalloc 的分配策略之一,是按照一系列固定的大小划分内存空间,当程序申请的内存最接近某个固定值时,会给它分配相应大小的空间。

    好处:减少分配次数,坏处:形成碎片的风险

  • 外因:键值对大小不一样和删改操作

    不同业务应用的数据都可能保存在 Redis 中,这就会带来不同大小的键值对,内存分配器只能按固定大小分配内存,所以,分配的内存空间一般都会比申请的空间大一些

如何判断是否有内存碎片?

  • Redis 自身提供了 INFO 命令,可以用来查询内存使用的详细信息
  • mem_fragmentation_ratio 的指标,它表示的就是 Redis 当前的内存碎片率,mem_fragmentation_ratio 大于 1 但小于 1.5。这种情况是合理的

如何清理内存碎片?

  • 重启 Redis 实例

    如果 Redis 中的数据没有持久化,那么,数据就会丢失;

    即使 Redis 数据持久化了,我们还需要通过 AOF 或 RDB 进行恢复,恢复时长取决于AOF 或 RDB 的大小,如果只有一个 Redis 实例,恢复阶段无法提供服务。

  • 从 4.0-RC3 版本以后,Redis 自身提供了一种内存碎片自动清理的方法

    可以把 activedefrag 配置项设置为 yes,同时设置下面参数

    1. active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到 100MB 时,开始清理;

    2. active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10% 时,开始清理。

    3. active-defrag-cycle-min 25: 表示自动清理过程所用 CPU 时间的比例不低于25%,保证清理能正常开展

    4. active-defrag-cycle-max 75:表示自动清理过程所用 CPU 时间的比例不高于

      75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致

      响应延迟升高。

19: redis 的redis缓冲区

缓冲区的功能其实很简单,主要就是用一块内存空间来暂时存放命令数据,以免出现因为数据和命令的处理速度慢于发送速度而导致的数据丢失和性能问题。但因为缓冲区的内存空间有限,如果往里面写入数据的速度持续地大于从里面读取数据的速度,就会导致缓冲区需要越来越多的内存来暂存数据。当缓冲区占用的内存超出了设定的上限阈值时,就会出现缓冲区溢出。

客户端输入和输出缓冲区

为了避免客户端和服务器端的请求发送和处理速度不匹配,服务器端给每个连接的客户端都设置了一个输入缓冲区和输出缓冲区.输入缓冲区会先把客户端发送过来的命令暂存起来,Redis 主线程再从输入缓冲区中读取命令,进行处理。当 Redis 主线程处理完数据后,会把结果写入到输出缓冲区,再通过输出缓冲区返回给客户端,

如何应对输入缓冲区溢出?

  • 可能导致溢出的情况主要是下面两种:写入了 bigkey,服务器端处理请求的速度过慢

  • 如何查看输入缓冲区的内存使用情况:

    使用 CLIENTLIST 命令

    qbuf 很大,而同时 qbuf-free 很小,就要引起注意了,因为这时候输入缓冲区已经占用了很多内存,而且没有什么空闲空间了

  • 解决方法:从两个角度去考虑

    把缓冲区调大:没有,除非修改源码

    数据命令的发送和处理速度:避免客户端写入 bigkey,以及避免 Redis 主线程阻塞。

如何应对输出缓冲区溢出?

  • Redis 为每个客户端设置的输出缓冲区也包括两部分:一部分,是一个大小为 16KB的固定缓冲空间,用来暂存 OK 响应和出错信息;另一部分,是一个可以动态增加的缓冲空间,用来暂存大小可变的响应结果

  • 什么情况下会发生输出缓冲区溢出

    1. 服务器端返回 bigkey 的大量结果; bigkey 原本就会占用大量的内存空间
    2. 执行了 MONITOR 命令; MONITOR 命令是用来监测 Redis 执行的。执行这个命令之后,就会持续输出监测到的各个命令操作。MONITOR 的输出结果会持续占用输出缓冲区,并越占越多,最后的结果就是发生溢出,因此,MONITOR 命令主要用在调试环境中,不要在线上生产环境中持续使用 MONITOR。
    3. 缓冲区大小设置得不合理。和输入缓冲区不同,可以通过 clientoutput-buffer-limit 配置项,来设置缓冲区的大小。设置输出缓冲区持续写入数据的数量上限阈值,和持续写入数据的时间的上限阈值
  • 解决方法

    避免 bigkey 操作返回大量数据结果;

    避免在线上环境中持续使用 MONITOR 命令。

    使用 client-output-buffer-limit 设置合理的缓冲区大小上限,或是缓冲区连续写入时间和写入量上限。

复制缓冲区的溢出问题

  • 全量复制时,主节点在传输RDB文件时,会继续接受客户端命令,为了保证数据同步,主节点会为每个从节点建立一个复制缓冲区,把这个期间的命令保存起来,等RDB文件传输完成后,再把复制缓冲区的命令发送给从节点取执行,保正主从一致。
  • 如果在全量复制时,从节点接收和加载 RDB 较慢,同时主节点接收到了大量的写命 令,写命令在复制缓冲区中就会越积越多,最终导致溢出。复制缓冲区一旦发生溢出,主节点也会直接关闭和从节点进行复制操作的连接,导致全量复制失败。
  • 如何避免
    1. 控制主节点保存的数据量大小,按通常的使用经验,我们会把主节点的数据量控制在 2~4GB,这样可以让全量同步执行得更快些,避免复制缓冲区累积过多命令
    2. 使用 client-output-buffer-limit 配置项,来设置合理的复制缓冲区大小。设置的依据,就是主节点的数据量大小、主节点的写负载压力和主节点本身的内存大小。
    3. 主节点上复制缓冲区的内存开销,会是每个从节点客户端输出缓冲区占用内存的总和。们还必须得控制和主节点连接的从节点个数,不要使用大规模的主从集群。

复制积压缓冲区的溢出问题

  • 主节点在把接收到的写命令同步给从节点时,同时会把这些写命令写入复制积压缓冲区。一旦从节点发生网络闪断,再次和主节点恢复连接后,从节点就会从复制积压缓冲区中,读取断连期间主节点接收到的写命令,进而进行增量同步
  • 复制积压缓冲区是一个大小有限的环形缓冲区。当主节点把复制积压缓冲区写满后,会覆盖缓冲区中的旧命令数据。如果从节点还没有同步这些旧命令数据,就会造成主从节点间重新开始执行全量复制。
  • 为了应对复制积压缓冲区的溢出问题,可以调整复制积压缓冲区的大小,也就是设置 repl_backlog_size 这个参数的值。

20:redis 做缓存

缓存的特征

  • 第一个特征:在一个层次化的系统中,缓存一定是一个快速子系统,数据存在缓存中时,能避免每次从慢速子系统中存取数据。
  • 第二个特征:缓存系统的容量大小总是小于后端慢速系统的,不可能把所有数据都放在缓存系统中。所以,缓存和后端慢速系统之间,必然存在数据写回和再读取的交互过程。缓存中的数据需要按一定规则淘汰出去,写回后端系统,而新的数据又要从后端系统中读取进来,写入缓存。

Redis 缓存处理请求的两种情况

  • 把 Redis 用作缓存时,会把 Redis 部署在数据库的前端,业务应用在访问数据时,会先查询 Redis 中是否保存了相应的数据。
  • 缓存命中:Redis 中有相应数据,就直接读取 Redis,性能非常快。
  • 缓存缺失:Redis 中没有保存相应数据,就从后端数据库中读取数据,性能就会变慢。

Redis 作为旁路缓存的使用操作

  • 旁路缓存:如果应用程序想要使用Redis 缓存,就要在程序中增加相应的缓存操作代码。所以, Redis 称为旁路缓存,读取缓存、读取数据库和更新缓存的操作都需要在应用程序中来完成。
  • Redis 并不适用于那些无法获得源码的应用。
  • 可以单独对 Redis缓存进行扩容或性能优化。而且,只要保持操作接口不变,我们在应用程序中增加的代码就不用再修改了。

缓存的类型

  • 只读缓存:应用要读取数据的话,会先调用 Redis GET 接口,查询数据是否存在。而所有的数据修改请求,会直接发往后端的数据库,在数据库中增删改。对于删改的数据来说,如果 Redis 已经缓存了相应的数据,还应用需要把这些缓存的数据删除,Redis中就没有这些数据了。当应用再次读取这些数据时,会发生缓存缺失,应用会把这些数据从数据库中读出来,并写到缓存中。这样一来,这些数据后续再被读取时,就可以直接从缓存中获取了,能起到加速访问的效果。

    当我们需要缓存图片、短视频这些用户只读的数据时,就可以使用只读缓存这个类型了。

  • 读写缓存:对于读写缓存来说,除了读请求会发送到缓存进行处理(直接在缓存中查询数据是否存在),所有的写请求也会发送到缓存,在缓存中直接对数据进行增删改操作。

    同步直写:写请求发给缓存的同时,也会发给后端数据库进行处理,等到缓存和数据库都写完数据,才给客户端返回。这样,即使缓存宕机或发生故障,最新的数据仍然保存在数据库中,这就提供了数据可靠性保证。优先保证数据可靠性。同步直写会降低缓存的访问性能。

    异步写回:所有写请求都先在缓存中处理。等到这些增改的数据要被从缓存中淘汰出来时,缓存将它们写回后端数据库。优先提供快速响应。如果发生了掉电,而它们还没有被写回数据库,就会有丢失的风险了。

21:redis缓存替换策略

设置多大的缓存容量合适?

  • 根据数据访问的特性,建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销。

Redis 缓存有哪些淘汰策略?

  • 不进行数据淘汰的策略: noeviction,Redis 在使用的内存空间超过 maxmemory 值时,并不会淘汰数据一旦缓存被写满了,再有写请求来时,Redis 不再提供服务,而是直接返回错误。

  • 进行数据淘汰的策略

    1. 在所有数据范围内进行淘汰

      allkeys-lru:使用 LRU 算法在所有数据中进行筛选

      allkeys-random:从所有键值对中随机选择并删除数据;

      allkeys-lfu:使用 LFU 算法在所有数据中进行筛选。

    2. 在设置了过期时间的数据中进行淘汰

      volatile-random:在设置了过期时间的键值对中,进行随机删除。

      volatile-ttl:会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除

      volatile-lru:使用 LRU 算法筛选设置了过期时间的键值对。

      volatile-lfu:使用 LFU 算法选择设置了过期时间的键值对。

LRU:按照最近最少使用的原则来筛选数据,最不常用的数据会被筛选出来

  • 思路:链表+移动。复杂度较高。

  • LRU 算法在实际实现时,需要用链表管理所有的缓存数据,这会带来额外的空间开销

    redis改进:在 Redis 中,Redis 默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构RedisObject 中的 lru 字段记录)。然后,Redis 在决定淘汰的数据时**,第一次会随机选出N 个数据**,把它们作为一个候选集合。Redis 会比较这 N 个数据的 lru 字段,把lru 字段值最小的数据从缓存中淘汰出去。

如何选择淘汰策略

  • 优先使用 allkeys-lru 策略。如果业务数据中有明显的冷热数据区分,我建议你使用allkeys-lru 策略
  • 如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用allkeys-random 策略,随机选择淘汰的数据。
  • 如果业务中有置顶的需求,可以使用 volatile-lru策略,同时不给这些置顶数据设置过期时间。这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据 LRU 规则进行筛选。

22:redis 缓存一致性保证

缓存中有数据,那么,缓存的数据值需要和数据库中的值相同;缓存中本身没有数据,那么,数据库中的值必须是最新值

同步直写策略:写缓存时,也同步写数据库,缓存和数据库中的数据一致;

异步写回策略:写缓存时不同步写数据库,等到数据从缓存中淘汰时,再写回数据库。 使用这种策略时,如果数据还没有写回数据库,缓存就发生了故障,那么,此时,数据库就没有最新的数据了。

所以,对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策 略。不过,需要注意是,如果采用这种策略,就需要同时更新缓存和数据库。所以,我们要在业务应用中使用事务机制,来保证缓存和数据库的更新具有原子性

读写缓存

  • 要想保证缓存和数据库中的数据一致,就要采用同步直写策略。
  • 在业务应用中使用事务机制,来保证缓存和数据库的更新具有原子性。
  • 在数据一致性要求不高的情景下,可以使用异步写回策略。

只读缓存

  • 新增数据,数据会直接写到数据库中,不用对缓存做任何操作,此时,缓存中本身就没有新增数据,而数据库中是最新值。缓存和数据库的数据是一致的

  • 删改数据:如果发生删改操作,应用既要更新数据库,也要在缓存中删除数据。这两个操作如果无法保证原子性,就会出现数据不一致问题。

    先删除缓存,出现故障,无法更新数据库:应用再访问数据时,缓存中没有数据,发生缓存缺失。应用访问数据库,但是数据库中的值为旧值,应用就访问到旧值了。

    先更新了数据库,出现故障,无法删除缓存:数据库中的值是新值,而缓存中的是旧值,这肯定是不一致的。如果有其他的并发请求来访问数据,按照正常的缓存访问流程,就会先在缓存中查询,就会读到旧值。

如何解决数据不一致问题?

  • 重试机制

    把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka 消息队列)。当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。

    如果能够成功地删除或更新,就要把这些值从消息队列中去除,以免重复操作,可以保证数据库和缓存的数据一致了。否则的话,还需要再次进行重试。如果重试超过的一定次数,还是没有成功,就需要向业务层发送报错信息了。

大量并发请求有可能读到不一致的数据

在更新数据库和删除缓存值的过程中,即使这两个操作第一次执行时都没有失败,当有大量并发请求时,应用还是有可能读到不一致的数据

  • 先删除缓存,再更新数据库。

    完成第一步后,其他线程来都数据,发现缓存没有,重新从数据库加到到redis,相当于没删。后来再来线程都数据,就会读到旧数据。

    解决方法:延迟双删在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作。

  • 先更新数据库值,再删除缓存值

    如果线程 A 删除了数据库中的值,但还没来得及删除缓存值,线程 B 就开始读取数据了,那么此时,线程 B 查询缓存时,发现缓存命中,就会直接从缓存中读取旧值。

删除缓存值或更新数据库失败而导致数据不一致,你可以使用重试机制确保删除或更新操作成功。

在删除缓存值、更新数据库的这两步操作中,有其他线程的并发读操作,导致其他线程读取到旧值,应对方案是延迟双删。

23:如何解决缓存雪崩、击穿、穿透难题

缓存雪崩、缓存击穿和缓存穿透。这三问题一旦发生,会导致大量的请求积压到数据库层。如果请求的并发量很大,就会导致数据库宕机或是故障,这就是很严重的生产事故了。

缓存雪崩

  • 缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。
  • 导致雪崩的原因:
    • 缓存中有大量数据同时过期,导致大量请求无法得到处理。
    • Redis缓存实例发生故障宕机了,无法处理请求。
  • 解决方法:
    • 避免给大量的数据设置相同的过期时间。如果业务层的确要求有些数据同时失效,你可以在用 EXPIRE 命令给每个数据设置过期时间时,给这些数据的过期时间增加一个较小的随机数.
    • 过服务降级。发生缓存雪崩时,针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据(例如电商商品属性)时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或是错误信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。
    • 服务熔断机制。在发生缓存雪崩时,为了防止引发连锁的数据库雪崩,甚至是整个系统的崩溃,我们暂停业务应用对缓存系统的接口访问。
    • 请求限流机制。在业务系统的请求入口前端控制每秒进入系统的请求数,避免过多的请求被发送到数据库。
    • 事前预防。从节点的方式构建 Redis 缓存高可靠集群。如果 Redis 缓存的主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务

缓存击穿

  • 针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。
  • 发生场景:热点数据过期失效时。
  • 如何避免:对于访问特别频繁的热点数据,不设置过期时间。

缓存穿透

指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。

  • 产生的原因:
    1. 缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据;
    2. 恶意攻击:专门访问数据库中没有的数据。
  • 如何避免:
    1. 缓存空值或缺省值。一旦发生缓存穿透,可以针对查询的数据,在 Redis 中缓存一个空值或是和业务层协商确定的缺省值
    2. 使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。布隆过滤器可以使用 Redis 实现,本身就能承担较大的并发访问压力。
    3. 在请求入口的前端进行请求检测。一个有效的应对方案是在请求入口前端,对业务系统接收到的请求进行合法性检测,把恶意的请求(例如请求参数不合理、请求参数是非法值、请求字段不存在)直接过滤掉,不让它们访问后端缓存和数据库。

24:缓存污染

Job-Hunter 文章被收录于专栏

2024年最新整理的八股文。 包括计算机网络,操作系统,MySQL,linux,设计模式,数据结构和算法,等等。 题目来源于网友爆料,GZH摘录,CSDN等等。 根据考察知识点,将题目进行分类,方便背诵。

全部评论
AOF里的进程写成线程了
点赞 回复 分享
发布于 2024-01-23 14:04 山东

相关推荐

06-12 11:51
已编辑
门头沟学院 Java
1. 反射的原理是什么?2. 类加载时,类和实例分别加载到哪里?3. 什么是好的编程方法或方案?4. SQL 语句优化有哪些方法?5. 订单生成 ID 的实现方式是什么?6. 如何处理时间回拨问题?7. 雪花算法的基本原理是什么?8. 如何设计一个简单的 IOC 容器?9. 自动装箱和拆箱的底层实现是什么?10. Java 中的线程安全是什么意思?11. 什么是 TLAB(Thread Local Allocation Buffer)?12. NIO 的基本概念是什么?13. ConcurrentHashMap 中锁的机制是什么?14. 一条 SQL 语句在 MySQL 中的执行过程是怎样的?15. MySQL 中事务的日志记录是如何实现的?16. 如何通过代码实现死锁?17. 接口和抽象类的区别是什么?18. 接口和抽象类在设计模式中的体现有哪些?19. String、StringBuilder 和 StringBuffer 的区别是什么?20. final 关键字的作用是什么?21. THREAD LOCAL 可能引发的内存泄漏问题是什么?22. THREAD LOCAL 的应用场景有哪些?23. 如何将父线程的 THREAD LOCAL 值传递给子线程?24. 对 Java 面向对象的理解是什么?25. 面向对象和面向过程的区别是什么?26. Java 中创建对象的方式有哪些?27. Java 中序列化与反序列化的概念是什么?28. Redis 中使用哈希存储对象和直接使用字符串存储对象的区别是什么?29. Java 中参数传递是按值传递还是按引用传递?30. 多继承可能带来的问题是什么?31. 方法重载与方法重写的区别是什么?32. Java 内部类的概念和作用是什么?33. JDK 8 的新特性有哪些?34. JRE 和 JDK 的区别是什么?35. JDK 中常用的工具类有哪些?36. Hashcode 的作用是什么?37. TreeSet 集合在加入一个对象时如何判断该对象是否存在?38. 是否可能两个不相等的对象有相同的哈希值?39. Java 中的 hashCode 和 equals 方法有什么区别?40. 什么是 Java 中的动态代理?41. 动态代理与静态代理的区别是什么?42. 注解的原理是什么?43. SPI 机制的作用是什么?44. 泛型的作用是什么?45. 什么是泛型擦除?46. 深拷贝和浅拷贝的区别是什么?47. Integer 类型的缓存池是如何工作的?48. Java 程序的运行过程是怎样的?49. new 一个 String 类型的对象时会创建多少个对象?50. final、finally 和 finalize 的区别是什么?51. Java 的基本类型有哪些?52. 静态方法和实例方法的区别是什么?53. for-each 循环实现的接口或集合类型是什么?54. RandomAccess 接口的作用是什么?55. 迭代器(Iterator)的工作原理是什么?56. ArrayList 和 LinkedList 的区别是什么?57. 为什么需要 ArrayList 而不是直接使用数组?58. 数组和链表在 Java 中的区别是什么?59. ArrayList 的扩容机制是怎样的?60. ArrayList 的缺点有哪些?如何使其线程安全?61. 如何使一个集合不可修改?62. 使用 HashMap 时有哪些提升性能的技巧?63. 什么是哈希碰撞?在 HashMap 中如何解决哈希碰撞?64. HashMap 的 put 方法执行流程是怎样的?65. 为什么 HashMap 在扩容时总是以 2 的 n 次方倍增长?66. ConcurrentHashMap 在 get 方法中是否需要加锁?67. 为什么 HashMap 不支持 key 或 value 为 null?68. Java 中如何创建多线程?69. 线程池的工作原理是什么?70. Java 中的线程是如何进行通信的?71. join 方法的原理是什么?72. 主线程如何知道子线程创建成功?73. 反射的原理是什么?74. Java 内存模型是什么?如何保证可见性和有序性?75. final 关键字是否能够保证变量的可见性?
点赞 评论 收藏
分享
评论
2
10
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务