redis核心知识点

1,162 阅读17分钟

数据结构与对象

对象与编码

  • ziplist

  • skiplist

    • 跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。每个跳跃表节点的层高都是1至32之间的随机数
    • 按照scope排序

渐进式 rehash

Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。

单机数据库的实现

持久化

AOF(写后日志)

三种写回策略

风险:

  1. 数据丢失:刚执行完一个命令,宕机。
  2. AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。

策略:

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

重写机制

一个拷贝(redis数据拷贝,两处日志(AOF缓冲区,AOF重写缓冲区)

  • AOF 重写机制:为了避免日志文件过大,从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。 AOF重写并不需要对原有AOF文件进行任何的读取,写入,分析等操作,这个功能是通过读取服务器当前的数据库状态来实现的。
  • 和 AOF 日志由主线程写回不同,每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程,这也是为了避免阻塞主线程,导致数据库性能下降。
  • 解决重写过程中新数据写入,导致数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区,

RDB(内存快照)

  • 避免阻塞:

    • save:在主线程中执行,会导致阻塞;
    • bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。
  • 写时复制技术(Copy-On-Write, COW)

    • COW:Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。
    • 如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本(键值对 C’)。然后,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据(键值对 C)写入 RDB 文件。

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

  • 一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
  • 另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了(所以,在 Redis 中如果有一个 bgsave 在运行,就不会再启动第二个 bgsave 子进程)。
  • 此时,我们可以做增量快照,所谓增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。

增量快照

混合

Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。

事件

单线程,多路复用

Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。

多机数据库的实现

复制

全量复制,基于长连接的命令传播,增量复制。

主从复制

主库的 runID 和复制进度 offset 两个参数。

runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”。offset,此时设为 -1,表示第一次复制。

一主多从,主-从-从

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

增量复制 repl_backlog_buffer。

为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。

repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区。

Sentinel哨兵机制

功能:监控,选主,通知

监控

主观下线,客观下线。

监控是指哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。如果从库没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”;

用于判断主库下线:引入多个哨兵实例(哨兵集群)一起来判断,只有大多数的哨兵实例,都判断主库已经“主观下线”了,主库才会被标记为“客观下线”

选主

  • 筛掉不合格的:下线,总是断联

    • down-after-milliseconds * 10。其中,down-after-milliseconds 是我们认定主从库断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库。
  • 顺序筛选:

      1. 优先级最高的从库得分高
    • 2.和旧主库同步程度最接近 (repl offset) 的从库得分高
    • 3.ID 号小的从库得分高

通知

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

哨兵 -哨兵,哨兵-主库联系 pub/ sub****

主库上有一个名为“sentinel:hello”的频道,不同哨兵就是通过它来相互发现,实现互相通信的。

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

由哨兵向主库发送 INFO 命令来完成的。

哨兵 2 给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵。

哨兵 - 客户端 :客户端订阅哨兵 频道 图为客户端可以订阅的频道

哨兵 主从切换

任何一个实例只要自身判断主库“主观下线”后,就会给其他实例发送 is-master-down-by-addr 命令。接着,其他实例会根据自己和主库的连接情况,做出 Y 或 N 的响应,Y 相当于赞成票,N 相当于反对票。

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

集群

切片集群

哈希槽 16384,CRC16算法。 在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。

客户端 如何定位数据?

客户端为什么可以在访问任何一个实例时,都能获得所有的哈希槽信息呢?这是因为,Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。 客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。

实例和 哈希 槽的对应关系发生改变?

  1. MOVED 重定向命令

由于负载均衡,Slot 2 中的数据已经从实例 2 迁移到了实例 3,但是,客户端缓存仍然记录着“Slot 2 在实例 2”的信息,所以会给实例 2 发送命令。实例 2 给客户端返回一条 MOVED 命令,把 Slot 2 的最新位置(也就是在实例 3 上),返回给客户端,客户端就会再次向实例 3 发送请求,同时还会更新本地缓存,把 Slot 2 与实例的对应关系更新过来。

  1. ASK命令

在实际应用时,如果 Slot 2 中的数据比较多,就可能会出现一种情况:客户端向实例 2 发送请求,但此时,Slot 2 中的数据只有一部分迁移到了实例 3,还有部分数据没有迁移。在这种迁移部分完成的情况下,客户端就会收到一条 ASK 报错信息。

ASK 命令表示两层含义:第一,表明 Slot 数据还在迁移中;第二,ASK 命令把客户端所请求数据的最新实例地址返回给客户端,此时,客户端需要给实例 3 发送 ASKING 命令,然后再发送操作命令。

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

脑裂

脑裂,导致数据丢失的问题。

如何应对脑裂问题? 问题是出在原主库发生假故障后仍然能接收请求上,我们就开始在主从集群机制的配置项中查找是否有限制主库接收请求的设置。

通过查找,我们发现,Redis 已经提供了两个配置项来限制主库的请求处理,分别是 min-slaves-to-write 和 min-slaves-max-lag。

  • min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;
  • min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。

有了这两个配置项后,我们就可以轻松地应对脑裂问题了。具体咋做呢?

我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的请求了。

假设我们将 min-slaves-to-write 设置为 1,把 min-slaves-max-lag 设置为 12s,把哨兵的 down-after-milliseconds 设置为 10s,主库因为某些原因卡住了 15s,导致哨兵判断主库客观下线(15s超过了10s的阈值),开始进行主从切换。同时,因为原主库卡住了 15s,没有一个从库能和原主库在 12s 内进行数据复制,原主库也无法接收客户端请求了。这样一来,主从切换完成后,也只有新主库能接收请求,不会发生脑裂,也就不会发生数据丢失的问题了。

假设我们将 min-slaves-to-write 设置为 1,min-slaves-max-lag 设置为 15s,哨兵的 down-after-milliseconds 设置为 10s,哨兵主从切换需要 5s。主库因为某些原因卡住了 12s,此时,还会发生脑裂吗?主从切换完成后,数据会丢失吗?

主库卡住 12s,达到了哨兵设定的切换阈值,所以哨兵会触发主从切换。但哨兵切换的时间是 5s,也就是说哨兵还未切换完成,主库就会从阻塞状态中恢复回来,而且也没有触发 min-slaves-max-lag 阈值,所以主库在哨兵切换剩下的 3s 内,依旧可以接收客户端的写操作,如果这些写操作还未同步到从库,哨兵就把从库提升为主库了,那么此时也会出现脑裂的情况,之后旧主库降级为从库,重新同步新主库的数据,新主库也会发生数据丢失。 由此也可以看出,即使 Redis 配置了 min-slaves-to-write 和 min-slaves-max-lag,当脑裂发生时,还是无法严格保证数据不丢失,它只能是尽量减少数据的丢失。

我给你的建议是,假设从库有 K 个,可以将 min-slaves-to-write 设置为 K/2+1(如果 K 等于 1,就设为 1),将 min-slaves-max-lag 设置为十几秒(例如 10~20s),在这个配置下,如果有一半以上的从库和主库进行的 ACK 消息延迟超过十几秒,我们就禁止主库接收客户端写请求。

事务

实现事务

MULTI(事务开启)、EXEC(事务提交执行)

ACID

  1. 原子性

    1. 命令入队时就报错(客户端发送的操作命令本身就有错误,比如语法错误,使用了不存在的命令),会放弃事务执行,保证原子性;
    2. 命令入队时没报错(命令和操作的数据类型不匹配等),实际执行时报错,不保证原子性;DISCARD只能清除队列,不能回滚。
    3. EXEC 命令执行时实例故障,如果开启了 AOF 日志(redis-check-aof 这个工具可以把未完成的事务操作从 AOF 文件中去除 ),可以保证原子性。
  2. 一致性

    1. 情况一:命令入队时就报错在这种情况下,事务本身就会被放弃执行,所以可以保证数据库的一致性。

    2. 情况二:命令入队时没报错,实际执行时报错在这种情况下,有错误的命令不会被执行,正确的命令可以正常执行,也不会改变数据库的一致性。

    3. 情况三:EXEC 命令执行时实例发生故障在这种情况下,实例故障后会进行重启,这就和数据恢复的方式有关了,我们要根据实例是否开启了 RDB 或 AOF 来分情况讨论下。

      • 如果我们没有开启 RDB 或 AOF,那么,实例故障重启后,数据都没有了,数据库是一致的。
      • 如果我们使用了 RDB 快照,因为 RDB 快照不会在事务执行时执行,所以,事务命令操作的结果不会被保存到 RDB 快照中,使用 RDB 快照进行恢复时,数据库里的数据也是一致的。
      • 如果我们使用了 AOF 日志,而事务操作还没有被记录到 AOF 日志时,实例就发生了故障,那么,使用 AOF 日志恢复的数据库数据是一致的。如果只有部分操作被记录到了 AOF 日志,我们可以使用 redis-check-aof 清除事务中已经完成的操作,数据库恢复后也是一致的。
  3. 隔离性

    1. 并发操作在 EXEC 命令前执行,此时,隔离性的保证要使用 WATCH 机制来实现,否则隔离性无法保证。
    2. 并发操作在 EXEC 命令后执行,此时,隔离性可以保证。这是因为 ****Redis 是用单线程执行命令,而且,EXEC 命令执行后,Redis 会保证先把命令队列中的所有命令执行完再执行之后的指令。

WATCH 机制的作用是,在事务执行前,监控一个或多个键的值变化情况,当事务调用 EXEC 命令执行时,WATCH 机制会先检查监控的键是否被其它客户端修改了。如果修改了,就放弃事务执行,避免事务的隔离性被破坏。然后,客户端可以再次执行事务,此时,如果没有并发修改事务数据的操作了,事务就能正常执行,隔离性也得到了保证。

  1. 持久性

不管 Redis 采用什么持久化模式,事务的持久性属性是得不到保证的。

事务一致性的理解

redian.news/wxnews/3526…

我感觉可以从两个方面来看:单机版本的一致性和分布式版本的一致性。

分布式的话主要依赖主从同步,这块是能有一个最终一致的。

单机的话,主要看事务里边的操作,是不是客户端执行完成后,他能一直看到提交成功后的结果(或者是提交前的,因为是个内存数据库,重启的话会丢数据)。

只要能保证看到的一定是提交前,或者提交后的,那他就是一致的。

缓存雪崩击穿穿透

缓存雪崩

缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。

缓存击穿

缓存击穿是指,针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。缓存击穿的情况,经常发生在热点数据过期失效时。

缓存穿透

缓存穿透 是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。此时,应用也无法从数据库中读取数据再写入缓存,来服务后续请求,这样一来,缓存也就成了“摆设”,如果应用持续有大量请求访问数据,就会同时给缓存和数据库带来巨大压力。