Redis知识点汇总

Redis 简介

Redis本质上是一个Key-Value类型的内存数据库,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。

因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。可用于缓存、事件发布或订阅、高速队列等场景。

该数据库使用ANSI C语言编写,支持网络,提供字符串、哈希、列表、队列、集合结构直接存取,基于内存,可持久化。Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。

在项目中使用 Redis,主要考虑两个角度:性能和并发。

Redis 的线程模型

文件事件处理器

redis基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)

这个文件事件处理器是单线程的,所以redis才叫做单线程模型,采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器来处理这个事件。

如果被监听的socket准备好执行accept、read、write、close等操作的时候,跟操作对应的文件事件就会产生,这个时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件。

文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了redis内部的线程模型的简单性。

文件事件处理器的结构包含4个部分:

  • 多个socket
  • IO多路复用程序
  • 文件事件分派器
  • 事件处理器(命令请求处理器、命令回复处理器、连接应答处理器,等等)

多个socket可能并发的产生不同的操作,每个操作对应不同的文件事件,IO多路复用程序会监听多个socket,然后将socket放入一个队列中排队,每次从队列中取出一个socket给事件分派器,事件分派器把socket给对应的事件处理器。

一个socket的事件处理完之后,IO多路复用程序才会将队列中的下一个socket给事件分派器。文件事件分派器会根据每个socket当前产生的事件,来选择对应的事件处理器来处理。

文件事件

  • 当socket变得可读时(比如客户端对redis执行write操作,或者close操作),或者有新的可以应答的sccket出现时(客户端对redis执行connect操作),socket就会产生一个AE_READABLE事件。

  • 当socket变得可写的时候(客户端对redis执行read操作),socket会产生一个AE_WRITABLE事件。

  • IO多路复用程序可以同时监听AE_REABLEAE_WRITABLE两种事件,要是一个socket同时产生了AE_READABLE和AE_WRITABLE两种事件,那么文件事件分派器优先处理AE_REABLE事件,然后才是AE_WRITABLE事件。

文件事件处理器

  • 如果是客户端要连接redis,那么会为socket关联连接应答处理器。
  • 如果是客户端要写数据到redis,那么会为socket关联命令请求处理器。
  • 如果是客户端要从redis读数据,那么会为socket关联命令回复处理器。

客户端与Redis通信的一次流程

客户端与redis通信的一次流程

  1. redis 服务端进程初始化的时候,会将 server socket 的 AE_READABLE 事件与连接应答处理器关联。

  2. 客户端 socket01 向 redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。文件事件分派器从队列中获取 socket,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。

  3. 假设此时客户端发送了一个 set key value 请求,此时 redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将 socket01 压入队列,此时事件分派器从队列中获取到 socket01 产生的 AE_READABLE 事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。

  4. 如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。

为啥 Redis 单线程模型也能效率这么高

  • 纯内存操作。
  • 核心是基于非阻塞的 IO 多路复用机制。
  • C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。
  • 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。

Redis 过期策略

使用Redis常见的有两个问题:

  • 往 redis 写入的数据怎么没了?
  • 数据明明过期了,怎么还占用着内存?

这是由 redis 的过期策略来决定。redis 有两种过期策略:定期删除+惰性删除。

🏷 定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!(引申一点,像zookeeper、eureka做注册中心检查数据过期就是遍历所有数据,HDFS检查文件契约是否过期是通过treeset结构)

🏷 惰性删除:定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如过期的 key 靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的!

但这还是有问题的,如果定期删除漏掉了很多过期 key,然后也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,咋整?

答案是:走内存淘汰机制。

Redis 内存淘汰机制

redis 提供 8种数据淘汰策略:

  1. no-enviction:默认的策略,禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错(DEL请求和部分特殊请求除外)。
  2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间(server.db[i].dict)中,移除最近最少使用的 key(这个是最常用的)。
  3. allkeys-random:当内存不足以容纳新写入数据时,在键空间(server.db[i].dict)中,随机移除某个 key(这个一般没人用,为啥要随机,肯定是把最近最少使用的 key 给淘汰)。
  4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].expires)中,移除最近最少使用的 key(这个一般不太合适)。
  5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].expires)中,随机移除某个 key。
  6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].expires)中,有更早过期时间的 key 优先移除。
  7. volatile-lfu:在设置了过期时间的key中使用LFU算法淘汰key(Redis4.0新增)。
  8. allkeys-lfu:在所有key中使用LFU算法淘汰数据(Redis4.0新增)。

:ice_cream: redis淘汰数据时还会同步到aof。

LRU算法

LRU(Least Recently Used),即最近最少使用,是一种缓存置换算法。其核心思想是:如果一个数据在最近一段时间没有被用到,那么将来被使用到的可能性也很小,所以就可以被淘汰掉。

LRU在Redis中的实现

Redis使用的是近似LRU算法,它跟常规的LRU算法还不太一样。

近似LRU算法通过随机采样法淘汰数据,每次随机出5(默认)个key,从里面淘汰掉最近最少使用的key。

可以通过maxmemory-samples参数修改采样数量:例:maxmemory-samples 10maxmenory-samples配置的越大,淘汰的结果越接近于严格的LRU算法。

Redis为了实现近似LRU算法,给每个key额外增加了一个24bit的字段,用来存储该key最后一次被访问的时间。

Redis3.0对近似LRU的优化

Redis3.0对近似LRU算法进行了一些优化。新算法会维护一个候选池(大小为16),池中的数据根据访问时间进行排序,第一次随机选取的key都会放入池中,随后每次随机选取的key只有在访问时间小于池中最小的时间才会放入池中,直到候选池被放满。

当放满后,如果有新的key需要放入,则将池中最后访问时间最大(最近被访问)的移除。

当需要淘汰的时候,则直接从池中选取最近访问时间最小(最久没被访问)的key淘汰掉就行。

LFU算法

LFU算法是Redis4.0里面新加的一种淘汰策略。它的全称是Least Frequently Used

核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问多的则被留下来。

LFU算法能更好的表示一个key被访问的热度。假如使用LRU算法,一个key很久没有被访问到,只是刚刚被访问了一次,那么它就被认为是热点数据,不会被淘汰,而有些key之前访问频率很高,一段时间没有被访问,但将来是很有可能再被访问到的则被淘汰了。

如果使用LFU算法则不会出现这种情况,因为使用一次并不会使一个key成为热点数据。

Redis 持久化机制

Redis有两种持久化方式,一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)。

快照(snapshotting)持久化(RDB)

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。

Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

Redis会单独fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。

需要注意的是,每次快照持久化都会将主进程的数据库数据复制一遍,导致内存开销加倍,若此时内存不足,则会阻塞服务器运行,直到复制结束释放内存;
都会将内存数据完整写入磁盘一次,所以如果数据量大的话,而且写操作频繁,必然会引起大量的磁盘I/O操作,严重影响性能,并且最后一次持久化后的数据可能会丢失;

快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置:

1
2
3
4
5
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 

save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

优点:

  1. 只有一个文件dump.rdb,方便持久化。
  2. 容灾性好,一个文件可以保存到安全的磁盘。
  3. 性能最大化,fork子进程执行磁盘IO操作来完成持久化,让主进程继续处理命令,所以是IO最大化。
  4. 相对于 AOF 持久化机制来说,直接基于 RDB 数据文件来重启和恢复 redis 进程,更加快速。

缺点:

  1. 如果想要在 redis 故障时,尽可能少的丢失数据,RDB 没有 AOF 好。通过配置save参数来达到定时的写快照,比如 每900 秒有1个键被修改就进行一次快照,每300秒至少有10个键被修改进行快照,每60秒有至少10000个键被修改进行快照。所以如果当服务器还在等待写快照时出现了宕机,那么将会丢失数据。
  2. RDB 每次在 fork 子进程来执行 RDB 快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。

AOF(append-only file)持久化

与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。

以日志的形式记录每个写操作(读操作不记录),只需追加文件但不可以改写文件,Redis启动时会根据日志从头到尾全部执行一遍以完成数据的恢复工作。包括flushDB也会执行。

默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:

1
appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。

AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

1
2
3
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度 
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步

为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。

而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

优点:

  1. AOF 可以更好的保护数据不丢失,AOF持久化可以配置appendfsync属性,有always,每进行一次命令操作就记录到aof文件中一次;everySec,就是每秒内进行一次文件的写操作;no就是不进行aof文件的写操作。
  2. AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复,可以通过Redis-check-aof工具解决数据一致性问题。
  3. AOF机制的rewrite模式,用来将过大的aof文件缩小,实现原理是将所有的set 通过一句set 命令总结,所有的SADD命令总结为一句,这样每种命令都概括为一句来执行,就可以减少aof文件的大小了。(注意,在重写的过程中,是创建子进程来完成重写操作,主进程每个命令都会在AOF缓冲区和AOF重写缓冲区进行保存,这样旧版aof文件可以实现数据最新,当更新完后将重写缓冲区中的数据写入新的aof文件中然后就可以将新的文件替换掉旧版的文件。默认触发是当aof文件大小是上次重写后大小的一倍且文件大于64M时触发。)
  4. AOF 日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如不小心用 flushall 命令清空了所有数据,只要这个时候后台 rewrite 还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条 flushall 命令给删了,然后再将该 AOF 文件放回去,就可以通过恢复机制,自动恢复所有数据。

aof重写

缺点:

  1. 对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大。
  2. 数据集大的时候,比rdb启动效率低。
  3. AOF 开启后,支持的写 QPS 会比 RDB 支持的写 QPS 低,因为 AOF 一般会配置成每秒 fsync 一次日志文件,当然,每秒一次 fsync,性能也还是很高的。(如果实时写入,那么 QPS 会大降,redis 性能会大大降低)

RDB 和 AOF 如何选择

  • 不要仅仅使用 RDB,因为那会导致丢失很多数据;
  • 也不要仅仅使用 AOF,因为那样有两个问题:第一,通过 AOF 做冷备,没有 RDB 做冷备来的恢复速度更快;第二,RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug;
  • redis 支持同时开启两种持久化方式,可以综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。

Redis 4.0 对于持久化机制的优化

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。

这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。

当然缺点也是有的,AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

Redis Master-Slave

主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。
master-slave

redis replication 的核心机制

  • redis 采用异步方式复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量;
  • 一个 master node 是可以配置多个 slave node 的;
  • slave node 也可以连接其他的 slave node;
  • slave node 做复制的时候,不会 block master node 的正常工作;
  • slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;
  • slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。

注意,如果采用了主从架构,那么建议必须开启 master node 的持久化,不建议用 slave node 作为 master node 的数据热备,因为如果未开启或关闭了 master 的持久化,可能在 master 宕机重启的时候数据是空的,然后可能一经过复制, slave node 的数据也丢了。

另外,master 的各种备份方案也需要做。万一本地的所有文件丢失了,从备份中挑选一份 rdb 去恢复 master,这样才能确保启动的时候是有数据的。即使采用了哨兵模式的高可用机制,slave node 可以自动接管 master node,但也可能 sentinel 还没检测到 master failure,master node 就自动重启了,还是可能导致上面所有的 slave node 数据被清空。

redis 主从复制的核心原理

当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。

如果 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。

此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件,同时还会将从客户端新收到的所有写命令缓存在内存中。

RDB 文件生成完毕后,master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。

slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。

主从复制

主从复制的断点续传

从 redis2.8 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。

master node 会在内存中维护一个 backlog,master 和 slave 都会保存一个 replica offset 还有一个 master run id,offset 就是保存在 backlog 中的。

如果 master 和 slave 网络连接断掉了,slave 会让 master 从上次 replica offset 开始继续复制,如果没有找到对应的 offset,那么就会执行一次 resynchronization。

过期 key 处理

slave 不会过期 key,只会等待 master 过期 key。如果 master 过期了一个 key,或者通过 LRU 淘汰了一个 key,那么会模拟一条 del 命令发送给 slave。

复制的完整流程

  1. slave node 启动时,会在自己本地保存 master node 的信息,包括 master node 的host和ip(redis.conf里面的slaveof配置的),但是复制流程没开始。

  2. slave node 内部有个定时任务,每秒检查是否有新的 master node 要连接和复制,如果发现,就跟 master node 建立 socket 网络连接。

  3. slave node 发送 ping 命令给 master node。

  4. 如果 master 设置了 requirepass,那么 slave node 必须发送 masterauth 的口令过去进行认证。

  5. master node 第一次执行全量复制,将所有数据发给 slave node。

  6. master node 后续持续将写命令,异步复制给 slave node。

主从复制完整流程

全量复制

  • master 执行 bgsave ,在本地生成一份 rdb 快照文件。

  • master node 将 rdb 快照文件发送给 slave node,如果 rdb 复制时间超过 60秒(repl-timeout),那么 slave node 就会认为复制失败,可以适当调大这个参数(对于千兆网卡的机器,一般每秒传输 100MB,6G 文件,很可能超过 60s)

  • master node 在生成 rdb 时,会将所有新的写命令缓存在内存中,在 slave node 保存了 rdb 之后,再将新的写命令复制给 slave node。

  • client-output-buffer-limit slave 256MB 64MB 60 如果在复制期间,内存缓冲区持续消耗超过 64MB,或者一次性超过 256MB,那么停止复制,复制失败。

  • slave node 接收到 rdb 之后,清空自己的旧数据,然后重新加载 rdb 到自己的内存中,同时基于旧的数据版本对外提供服务。

  • 如果 slave node 开启了 AOF,那么会立即执行 BGREWRITEAOF,重写 AOF。

全量复制

全量复制的开销包含:bgsave生成rdb的时间、rdb文件通过网络传输的时间、slave清空旧数据的时间、slave加载rdb的时间、可能的aof重写时间。如果复制的数据量在4G~6G之间,那么很可能全量复制时间消耗到1分半到2分钟。

增量复制

  • 如果全量复制过程中,master-slave 网络连接断掉,那么 slave 重新连接 master 时,会触发增量复制。

  • master 直接从自己的 backlog 中获取部分丢失的数据,发送给 slave node,默认 backlog 就是 1MB。

  • master 就是根据 slave 发送的 psync 中的 offset 来从 backlog 中获取数据的。

增量复制

heartbeat

主从节点互相都会发送 heartbeat 信息。master 默认每隔 10秒 发送一次 heartbeat,slave node 每隔 1秒 发送一个 heartbeat。

异步复制

master 每次接收到写命令之后,先在内部写入数据,然后异步发送给 slave node。

Redis Sentinel

哨兵介绍

Redis 官方文档对于哨兵功能的描述:

  • 集群监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
  • 消息通知(Notification):如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。故障转移后将故障转移的结果发送给客户端。
  • 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
  • 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。如果故障转移发生了,通知 client 客户端新的 master 地址。

哨兵系统中的主从节点,与普通的主从节点并没有什么区别,故障发现和转移是由哨兵来控制和完成的。

  • 哨兵节点本质上是 Redis 节点。
  • 每个哨兵节点,只需要配置监控主节点,便可以自动发现其他的哨兵节点和从节点。
  • 在哨兵节点启动和故障转移阶段,各个节点的配置文件会被重写(config rewrite)。

哨兵的核心知识

  • 哨兵至少需要 3 个实例,来保证自己的健壮性。
  • 哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。

哨兵集群必须部署 2 个以上节点,如果哨兵集群仅仅部署了 2 个哨兵实例,quorum = 1。

1
2
3
4
+----+         +----+
| M1 |---------| R1 |
| S1 | | S2 |
+----+ +----+

配置 quorum = 1,如果 master 宕机, s1 和 s2 中只要有 1 个哨兵认为 master 宕机了,就可以进行切换,同时 s1 和 s2 会选举出一个哨兵来执行故障转移。但是同时这个时候,需要 majority,也就是大多数哨兵都是运行的。

2 个哨兵,majority = 2
3 个哨兵,majority = 2
4 个哨兵,majority = 2
5 个哨兵,majority = 3

如果此时仅仅是 M1 进程宕机了,哨兵 s1 正常运行,那么故障转移是 OK 的。但是如果是整个 M1 和 S1 运行的机器宕机了,那么哨兵只有 1 个,此时就没有 majority 来允许执行故障转移,虽然另外一台机器上还有一个 R1,但是故障转移不会执行。

经典的 3 节点哨兵集群是这样的:

1
2
3
4
5
6
7
8
9
       +----+
| M1 |
| S1 |
+----+
|
+----+ | +----+
| R2 |----+----| R3 |
| S2 | | S3 |
+----+ +----+

配置 quorum=2,如果 M1 所在机器宕机了,那么三个哨兵还剩下 2 个,S2 和 S3 可以一致认为 master 宕机了,然后选举出一个来执行故障转移,同时 3 个哨兵的 majority 是 2,所以还剩下的 2 个哨兵运行着,就可以允许执行故障转移。

定时任务

每个哨兵节点维护了 3 个定时任务,定时任务的功能分别如下:

  • 每隔10秒向主从节点发送 info 命令获取最新的主从结构。
  • 每隔2秒通过master结点的channel(_sentinel_:hello频道)交换信息(pub/sub),交换对节点的”看法”和自身信息。
  • 每隔1秒向其他节点发送 ping 命令进行心跳检测,判断是否下线。

主观下线(sdown)

在心跳检测的定时任务中,如果一个哨兵 ping 一个 master,超过了is-master-down-after-milliseconds 指定的毫秒数之后都没有收到回复,就主观认为 master 宕机了,哨兵节点就会将其进行主观下线(sdown)

客观下线(odown)

哨兵节点在对主节点进行主观下线后,会通过 sentinel is-master-down-by-addr 命令询问其他哨兵节点该主节点的状态。
如果一个哨兵在指定时间内,收到了 quorum 数量的其它哨兵也认为那个 master 是 sdown 的,那么就对该主节点进行客观下线(odown)。

需要特别注意的是,客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。

slave 配置的自动纠正

哨兵会负责自动纠正 slave 的一些配置,比如 slave 如果要成为潜在的 master 候选人,哨兵会确保 slave 复制现有 master 的数据;

如果 slave 连接到了一个错误的 master 上,比如故障转移之后,那么哨兵会确保它们连接到正确的 master 上。

选举领导者哨兵节点

当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。

监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是 Raft 算法。

Raft 算法的基本思路是先到先得:即在一轮选举中,哨兵 A 向 B 发送成为领导者的申请,如果 B 没有同意过其他哨兵,则会同意 A 成为领导者。

一般来说,谁先完成客观下线,一般就能成为领导者。

故障转移

选举出的领导者哨兵,开始进行故障转移操作,此时首先要选举一个 slave 来,会考虑 slave 的一些信息:

  • 跟 master 断开连接的时长
  • slave 优先级
  • 复制 offset
  • run id

如果一个 slave 跟 master 断开连接的时间已经超过了 down-after-milliseconds 的 10 倍,外加 master 宕机的时长,那么 slave 就被认为不适合选举为 master。

1
(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state

接下来会对 slave 进行排序:

  • 按照 slave 优先级进行排序,slave priority 越低,优先级就越高。
  • 如果 slave priority 相同,那么看 replica offset,哪个 slave 复制了越多的数据,offset 越靠后,优先级就越高。
  • 如果上面两个条件都相同,那么选择一个 run id 比较小的那个 slave。

configuration epoch

执行故障转移的领导者哨兵,会从要切换到的新 master(salve->master)那里得到一个 configuration epoch,这就是一个 version 号,每次切换的 version 号都必须是唯一的。

如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待 failover-timeout 时间,然后接替继续执行故障转移,此时会重新获取一个新的 configuration epoch,作为新的 version 号。

configuration 传播

哨兵完成故障转移之后,会在自己本地更新生成最新的 master 配置,然后同步给其他的哨兵,就是通过之前说的 pub/sub 消息机制。

这里之前的 version 号就很重要了,因为各种消息都是通过一个 channel 去发布和监听的,所以一个哨兵完成一次新的切换之后,新的 master 配置是跟着新的 version 号的。其他的哨兵都是根据版本号的大小来更新自己的 master 配置的。

redis 哨兵主备切换的数据丢失问题

异步复制导致的数据丢失

因为 master->slave 的复制是异步的,所以可能有部分数据还没复制到 slave,master 就宕机了,此时这部分数据就丢失了。

异步复制导致数据丢失

脑裂导致的数据丢失

假如某个 master 所在机器突然脱离了正常的网络,跟其他 slave 机器不能连接,但是实际上 master 还运行着。此时哨兵可能就会认为 master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时集群里就会有两个 master ,也就是所谓的脑裂。

此时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 写数据。因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,重新从新的 master 复制数据。而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。

脑裂导致的数据丢失

异步复制和脑裂导致的数据丢失解决方案

进行如下配置:

1
2
min-slaves-to-write 1
min-slaves-max-lag 10

上面的配置表示,要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。

如果说一旦所有的 slave数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。

上面两个配置可以减少异步复制和脑裂导致的数据丢失

(1)减少异步复制的数据丢失

有了min-slaves-max-lag这个配置,就可以确保说,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低到可控范围内。

(2)减少脑裂的数据丢失

如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求,这样脑裂后的旧master就不会接受client的新数据,因此在脑裂场景下,最多就丢失 10 秒的数据。

Redis Cluster

Redis Cluster介绍

  • 自动将数据进行分片,每个 master 上放一部分数据。
  • 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的。
  • 客户端与redis节点直连,不需要中间代理层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。

16379 端口号是用来进行节点间通信的,也就是 cluster bus 的通信,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。

cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

节点间的内部通信机制

基本通信原理

集群元数据的维护有两种方式:集中式、Gossip 协议。

集中式

集中式是将集群元数据(节点信息、故障等等)集中存储在某个节点上。

集中式元数据集中存储的一个典型代表就是大数据领域的 storm,它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper(分布式协调的中间件)对所有元数据进行存储维护。
像HDFS的NameNode也属于集中式。

集中式元数据存储与维护

集中式的好处在于,元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到;

不好在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。

Gossip

redis 维护集群元数据采用另一个方式, gossip 协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。

redis-gossip

gossip 协议包含多种消息,包含 ping,pong,meet,fail 等等。

  • meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。

  • ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。

    ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了 `cluster_node_timeout / 2`,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 cluster_node_timeout 可以调节,如果调得比较大,那么会降低 ping 的频率。每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 3 个其它节点的信息,最多包含 总节点数减 2 个其它节点的信息。
  • pong:返回 ping 和 meeet,包含自己的状态和其它信息,也用于信息广播和更新。

  • fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机了。

gossip 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;

不好在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。

  • 10000 端口:每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如 7001,那么用于节点间通信的就是 17001 端口。每个节点每隔一段时间都会往另外几个节点发送 ping 消息,同时其它几个节点接收到 ping 之后返回 pong。

  • 交换的信息:包括故障信息,节点的增加和删除,hash slot 信息等等。

分布式寻址算法

  • hash 算法(大量缓存重建)
  • 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
  • redis cluster 的 hash slot 算法

hash 算法

来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致大部分的请求过来,全部无法拿到有效的缓存,导致大量的流量涌入数据库。

hash算法

一致性 hash 算法

一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。

来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。

在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。

燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。

一致性hash算法

redis cluster 的 hash slot 算法

redis cluster 有固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取 key 对应的 hash slot。

redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 hash tag 来实现。

任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。

hash-slot

redis cluster 的高可用与主备切换原理

redis cluster 的高可用的原理,几乎跟哨兵是类似的。

判断节点宕机

如果一个节点认为另外一个节点宕机,那么就是 pfail,主观宕机。如果多个节点都认为另外一个节点宕机了,那么就是 fail,客观宕机,跟哨兵的原理几乎一样,sdownodown

在 cluster-node-timeout 内,某个节点一直没有返回 pong,那么就被认为 pfail。

如果一个节点认为某个节点 pfail 了,那么会在 gossip ping 消息中,ping 给其他节点,如果超过半数的节点都认为 pfail 了,那么就会变成 fail。

从节点过滤

对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。

检查每个 slave node 与 master node 断开连接的时间,如果超过了 cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成 master。

从节点选举

每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。

所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。

从节点执行主备切换,从节点切换为主节点。

与哨兵比较

整个流程跟哨兵相比,非常类似,所以说,redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。

缓存雪崩

介绍

缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
缓存雪崩

解决方案

  • 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。

  • 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。

  • 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
    缓存雪崩解决方案

  • 给缓存的失效时间,加上一个随机值,避免集体失效。

  • 使用互斥锁,但是该方案吞吐量明显下降了。

  • 双缓存。用两个缓存,缓存 A 和缓存 B。假设缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间。自己做缓存预热操作。
    然后细分以下几个小点:从缓存 A 读数据,有则直接返回;A 没有数据,直接从 B 读数据,直接返回,并且异步启动一个更新线程,更新线程同时更新缓存 A 和缓存 B。

缓存穿透

介绍

一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  • 对空值(不管是数据不存在,还是系统故障)进行缓存,设置较短的失效时间;
    缓存空值

  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
    布隆过滤器拦截非法key

  • 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。
    采用互斥锁

  • 采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。

缓存击穿

介绍

就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库。

解决方案

  • 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。
  • 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
  • 若缓存的数据更新频繁或者缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动的重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。

缓存与数据库双写一致性

如何保证缓存与数据库双写一致性

本文标题:Redis知识点汇总

文章作者:王洪博

发布时间:2019年11月04日 - 12:11

最后更新:2020年02月26日 - 07:02

原始链接:http://whb1990.github.io/posts/da315dd.html

▄︻┻═┳一如果你喜欢这篇文章,请点击下方"打赏"按钮请我喝杯 ☕
0%