目录
- 1. 在项目中缓存是如何使用的?
- 2. 为啥在项目中要用缓存?
- 3. 缓存如果使用不当会造成什么后果?
- 4. redis 和 memcached 有什么区别?
- 5. redis 的线程模型是什么?
- 6. 为什么单线程的 redis 比多线程的 memcached 效率要高得多(为什么 redis 是单线程还能支持高并发)?
- 7. redis 都有哪些数据结构?分别在哪些场景下使用?
- 9. redis 的过期策略有哪些?
- 10. 要不你再手写一个 LRU 算法?
- 11. redis 如何设计能支持 QPS 10w+ 的高并发架构?
- 12. redis replication 的核心机制?
- 12. master 持久化对于主从架构的安全保障的意义
- 13. redis 主从复制的原理
- 14. redis 如何实现高可用架构?
- 15. 为什么 redis 哨兵集群只有 2 个节点无法正常工作?
- 16. redis 哨兵主备切换的数据丢失问题:异步复制、集群脑裂
- 17. redis 哨兵的核心原理
- 18. redis 的持久化有哪几种?
- 19. RDB 和 AOF 的优缺点都有哪些?
- 20. 什么是缓存雪崩和缓存穿透?并如何解决?
- 21. 如何保证缓存和数据库双写一致性?
- 22. redis 并发竞争问题该如何解决?
中华石杉老师-互联网Java工程师面试突击训练第1季:https://www.bilibili.com/video/BV1FE411y79Y?p=31
1. 在项目中缓存是如何使用的?
这个要结合项目的场景来说明,比如说通常会把用户登录的一些信息、token 等放到缓存中,在项目中可以比较方便的获取当前登陆人的信息或者是检验当前登录人的 token 是否有效或者已失效等情况
或者是在某一个业务场景下,需要经常去调用某个接口,或者是该接口耗时会比较长,而且接口返回的数据一般不会更新,就可以将该接口返回的数据放到缓存中,提高访问的速度以及减少对服务器的压力
2. 为啥在项目中要用缓存?
用缓存主要就两个作用:高性能、高并发
- 高性能
比如说有这么一个场景,用户向系统 A 发送一个请求查询某个数据,然后接口查询数据库都耗费了 800 ms,假如在十分钟内有 1000 个用户来都需要获取这个数据,并且在这个时间段内该数据都不会发生变化
这种情况就可以使用缓存,将接口返回的结果放到缓存中,之后的请求直接从缓存中拿去数据就会快很多
之前接口返回要 800 多 ms,如果直接从缓存中拿取数据就只需要花费 5 ms,这就是高性能的体现
- 高并发
比如说有这么一个场景,在高峰期时有 100 万用户访问系统 A,每秒钟就有 5000 个请求访问,如果对这些请求不做处理全部落在数据库上,每秒 5000 个请求就足以让数据库宕机
在这种场景下,就可以将部分数据放到缓存中,使得在高峰期时每秒 4000 多个请求走缓存,1000 多个请求走数据库的话,就能安全的度过这个高峰期,不让数据库因为访问量过大而宕机
这就是缓存高并发的一个体现
缓存是走内存的,内存天然支持高并发,但是数据库一般建议并发请求不要超过 2000 /s
3. 缓存如果使用不当会造成什么后果?
常见的缓存问题如下:
缓存与数据库双写不一致
缓存雪崩
缓存穿透
- 缓存并发竞争
4. redis 和 memcached 有什么区别?
- Redis 支持服务器端的数据操作
- Redis 相比 Memcached 来说,拥有更多的数据结构,并支持更丰富的数据操作,通常在 Memcached 里,需要将数据拿到客户端来进行类似的修改再 set 回去,这大大增加了网络 IO 的次数和数据体积。在 Redis 中,这些复杂的操作通常和一般的 GET/SET 一样高效,所以,如果需要缓存能支持更复杂的结构和操作,那么 Redis 会是不错的选择
- 内存使用效率对比
- 使用简单的 key-value 存储的话,Memcached 的内存利用率更高,而如果 Redis 采用 hash 结构来做 key-value 存储,由于其组合式的压缩,其内存利用率要高于 Memcached
- 性能对比
- 由于 Redis 只使用单核,而 Memcached 可以使用多核,所以平均每一个核上 Redis 在存储小数据时比 Memcached 性能更高。而在 100k 以上的数据中,Memcahced 性能要高于 Redis,虽然 Redis 最近也在存储大数据的性能上进行优化,但比起 Memcached 还是逊色了点
- 集群模式
- Memcached 没有原生的集群,需要依靠客户端来实现往集群中分片写入数据,但是 Redis 目前是原生支持集群模式的
5. redis 的线程模型是什么?
1)文件事件处理器
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 当前产生的事件,来选择对应的事件处理器来处理
2)文件事件
当 socket 变得可读时(比如客户端对 redis 执行 write 操作,或者 close 操作),或者有新的可以应答的 socket 出现时(客户端对 redis 执行 connect 操作),socket 就会产生一个 AE_READABLE 事件
当 socket 变得可写的时候(客户端对 redis 执行 read 操作),socket 会产生一个 AE_WRITEABLE 事件
IO 多路复用程序可以同时监听 AE_READABLE 和 AE_WRITEABLE 两种事件,要是一个 socket 同时产生了 AE_READABLE 和 AE_WRITEABLE 这两种事件,那么文件事件分派器优先处理 AE_READABLE 事件,然后才是 AE_WRITEABLE 事件
3)文件事件分派器
- 如果是客户端要连接 redis,那么会为 socket 关联应答处理器
- 如果是客户端要些数据到 redis,那么会为 socket 关联命令请求处理器
- 如果是客户端要从 redis 读数据,那么会为 socket 关联命令回复处理器
客户端与 redis 通信的一次流程
在 redis 启动初始化的时候,redis 会将连接应答处理器跟 AE_READABLE 事件关联起来,
如果一个客户端跟 redis 发起连接,此时会产生一个 AE_READABLE 事件
IO 多路复用程序会监听所有的 socket 产生的事件,也就是说 server socket 产生的 AE_READABLE 会被 IO 多路复用程序监听到,并且该事件会被压到队列中去
文件事件分派器会从队列中拿到 socket 所产生的事件,也就是 server socket 产生的 AE_READABLE,而 AE_READABLE 在 redis 初始化的时候就与连接应答处理器关联起来了,所以就会由连接应答处理器来处理这个事件
此时连接应答处理器就会跟客户端建立连接,创建客户端对应的 socket(socket_01),同时将这个 socket_01 的 AE_READABLE 事件跟命令请求处理器关联起来
假如客户端又向 redis 发送了一个 SET 的请求,那么就会直接与连接时 redis 创建的 socket_01 建立联系
该 socket 也会产生一个 AR_READABLE 事件,IO 多路复用程序也会监听到这一个事件并压入队列中,文件事件分派器从队列中拿到这个 AR_READABLE 事件之后会将该事件分派给命令请求处理器来处理(在建立连接之后 AE_READABLE 事件会跟命令请求处理器关联),这个命令请求处理器会从 socket_01 中读取相关数据,然后进行执行和处理,从 socket_01 中读取 key-value 并在内存中完成 key-value 的设置
接着 redis 这把准备好了给客户端的响应数据之后,就会将 socket_01 的 AE_WRITEABLE 事件跟命令回复处理器关联起来
当客户端这边准备好读取响应数据时,就会在 socket_01 上产生一个 AE_WRITEABLE 事件,会由对应的命令回复处理器来处理,就是将准备好的响应数据写入 socket,供客户端来读取
命令回复处理器写完之后,就会删除这个 socket_01 的 AE_WRITEABLE 事件和命令回复处理器的关联关系
redis 在 6.0 之后支持多线程并不是说指令操作的多线程,而是针对网络 IO 的一个多线程知识,也就是说在 redis 命令操作里面仍然是线程安全的,其次 reids 本身的性能瓶颈取决于三个方面:① 网络;② CPU;③ 内存。而真正会影响到性能的关键问题就是内存和网络,而 redis 6.0 的多线程本质上就是解决网络 IO 的处理效率的问题,在 redis 6.0 之前 redis 的 server 端去处理接受客户端请求的时候。socket 连接的建立和指定的数据读取、解析、执行、写回都是由同一个线程来处理的,在这种方式里面,客户端请求比较多的时候单个线程的网络处理效率太慢,导致客户端的请求处理消费非常低,于是 redis 6.0 对应网络 IO 的处理方案改成多线程,不过对应客户端指定的执行过程还是采用单线程的方式来执行。
redis 6.0 中多线程默认是关闭的,需要在 redis.conf 配置文件中去修改 io-threads-do-redis 的配置才能开启,之所以指令执行不使用多线程,又两个方面的原因:① 内存的 IO 操作,本身不存在性能瓶颈,redis 在数据结构上已经做了非常多的优化了;② 如果指令执行使用多线程,那 redis 就需要去解决线程安全的问题,需要对数据的操作同步加锁,不仅增加了复杂度,还会影响性能
6. 为什么单线程的 redis 比多线程的 memcached 效率要高得多(为什么 redis 是单线程还能支持高并发)?
- 核心是基于非阻塞的 IO 多路复用机制:IO 多路复用程序能偶够监听大量的 socket,将 socket 上产生的事件直接压到队列中,不进行阻塞处理
- 纯内存操作:文件事件处理器都是基于纯内存上进行操作的,效率非常高
- 单线程反而避免了多线程的频繁上下文切换的问题
7. redis 都有哪些数据结构?分别在哪些场景下使用?
- (1)string
最基本的类型,一般用于做简单的缓存
- (2)hash
类似于于 Map 的一种结构,可以将结构化的数据,比如说一个对象(前提是这个对象没有嵌套其它对象)给缓存在 redis 里,然后每次读写缓存的时候,就可以操作 hash 里面的某个字段
- (3)list
有序列表,可以通过 list 存储一些列表的数据结构,类似商品列表
还可以基于 lrange 命令,从某个元素开始读取多少个元素,实现高性能的分页查询
- (4)set
无序集合,自动去重
直接基于 set 将系统需要去重的数据扔进去,自动就给去重了
如果需要对一些数据进行快速的全局去重,可以使用
- (5)sorted set
排序的 set,可以去重也可以排序
写数据的时候可以指定一个字段,就会自动根据这个字段进行排序,可以做类似于排行榜这类的功能
9. redis 的过期策略有哪些?
redis 是缓存,缓存是要使用到内存的,而内存资源非常珍贵,比如内存只有 10 G,但是要 redis 存储 20 G 的数据,那么 redis 就会淘汰掉 10 G 的数据
缓存的最基本的一个概念就是数据是会过期的,要么自己设置一个过期时间,要么就会被 redis 给干掉
-
① 设置过期时间
在 set key 的时候,都可以给一个 expire time,就是过期时间,指定这个 key 到期就失效
假如设置了一个 key 只能存活 1 小时,那么 1 小时之后,redis 是怎么对这个 key 进行删除的呢?
答案:定期删除 + 惰性删除
所谓定期删除,指的是 redis 默认是每隔 100ms 就随机抽取一些
设置了过期时间的 key,检查其是否过期,如果过期就删除,但是定期删除可能会导致很多过期 key 到了时间并没有被删除。
惰性删除就是在获取某个 key 的时候,redis 会先检查一下这个 key 是否设置了过期时间,如果过期了此时就会删除掉
结合上述两种方式,就能保证过期的 key 一定会被删除掉 -
② 内存淘汰
如果 redis 定期删除漏掉了很多过期的 key,然后也没有及时去查,导致也没有走惰性删除,就可能会造成大量过期的 key 堆积在内存中,导致内存占用过高,这个时候就需要走内存淘汰机制,redis 会自行删除掉一些数据,将内存腾出来给新的数据写入
如果 redis 的内存占用过多的时候,此时会进行内存淘汰,有如下一些策略:
- noeviction:当内存不足以容纳新写入的数据时,新写入的操作会报错
- allkeys-lru:当内存不足以容纳新写入的数据时,在键空间中,移除最近最少使用的 key,这个是最常用的
- allkeys-random:当内存不足以容纳新写入的数据时,在键空间中,随机移除某个 key
- volatile-lru:当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,移除最近最少使用的 key
- volatile-random:内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,随机一次某个 key
- volatile-ttl:内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除
10. 要不你再手写一个 LRU 算法?
LRU (Least Recently Used) 是一种常用的缓存淘汰策略,当缓存满时,会淘汰最久未使用的数据。下面是一个简单的 Java 实现,使用了 LinkedHashMap 这个类,因为它内部维护了一个双向链表,可以很方便地实现 LRU 策略。
import java.util.LinkedHashMap;
import java.util.Map; public class LRUCache<K, V> extends LinkedHashMap<K, V> { private int capacity; public LRUCache(int capacity) { // true 表示让 LinkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。 // false 表示按照插入顺序进行排序,如果是按照创建顺序的话,就不能满足 LRU 缓存的策略了。 super(16, 0.75f, true); this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { // 当 map 中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。 return size() > capacity; } public static void main(String[] args) { LRUCache<Integer, String> cache = new LRUCache<>(3); cache.put(1, "one"); cache.put(2, "two"); cache.put(3, "three"); // 此时缓存已满,再次添加元素,最久未使用的元素(key 为 1 的元素)会被移除 cache.put(4, "four"); System.out.println(cache.get(1)); // 输出 null(未找到 key 为 1 的元素) System.out.println(cache.get(2)); // 输出 "two" System.out.println(cache.get(3)); // 输出 "three" System.out.println(cache.get(4)); // 输出 "four" }
}
11. redis 如何设计能支持 QPS 10w+ 的高并发架构?
redis 高并发跟整个系统的高并发之间的关系
在做电商项目中,有时候 QPS 能上十万甚至百万
光是用 redis 是不够的,但是 redis 在大型的缓存架构中,支持高并发的架构里面,是非常重要的一个环节
首先,缓存系统必须要支撑起高并发,再经过良好的缓存架构设计(多级缓存架构,热点缓存)
redis 不能支持高并发的瓶颈在哪?
单机,一般而言单机能承载的 QPS 大概是上万到几万不等,这个要根据业务场景来看的,如果业务操作比较复杂,还有 lua 脚本的话能承载的 QPS 就会低一些
单机的 redis 的 QPS 几乎不可能超过 10W+,除非一些特殊的情况,比如说机器特别好,配置很高,维护做得也很好,操作也没有太复杂
redis 支持 QPS 10w+
架构一般会做成主从架构,一主多从,读写分离,一般来说,对于缓存一般都是用来支持高并发的,写的请求比较少,可能就几千,大量的请求都是在读,可能有一二十万
所有的写操作都在 master(主库)上进行,然后 master 将数据同步到 slave(从库)上,假设一个 redis 每秒能承受 5w 的 QPS,如果要做一个能承载 10w 的 QPS ,只需要两个 slave 就行了
这种架构还有个好处就是可以水平扩容,如果要支持更高的 QPS 再新增 redis 就行了
12. redis replication 的核心机制?
- redis 采用异步方式复制数据到 slave 节点,不过 redis 2.8 开始,slave node 会周期性地确认自己每次复制的数据量
- 一个 master node 是可以配置多个 slave node
- slave node 也可以连接其他的 slave node
- slave node 做复制的时候,是不会 bloack master node 的正常工作的
- slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务,但是复制完成之后,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了
- slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量
12. master 持久化对于主从架构的安全保障的意义
如果用了主从架构,那么建议必须开启 master node 的持久化
不建议用 slave node 作为 master node 的数据热备,因为那样的话,如果关掉 master 的持久化,可能在 master 宕机重启的时候数据是空的,然后经过一轮复制之后,slave node 数据也丢了
即使采用高可用机制,slave node 可以自动接管 master node,但是也可能 sentinal 还没有检测到 master failure,master node 就自动重启了,还是可能导致所有的 slave node 数据全部清空
13. redis 主从复制的原理
主从架构的核心原理
当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node
如果这个 slave node 是重新连接 master node,那么 master node 仅仅会复制给 slave node 部分缺少的数据
否则如果是 slave node 第一次连接 master node,那么会触发一次 full resynchronization
开始 full resynchronization 的时候,master 会启动一个后台线程,开始生产一份 RDB 快照文件,同时还会将从客户端收到的所有写命令缓存在内存中,RDB 文件生成完毕之后,master 会将这个 RDB 发送给 slave node,slave node 会先写入本地磁盘,然后再从本地磁盘加载到内存中,然后 master 会将内存中缓存的写命令发送给 slave,slave 也会同步这些数据
slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,master 如果发现有多个 slave node 都来重新连接,仅仅会启动一个 RDB SAVE 操作,用一份数据服务所有 slave node
主从复制的断点续传
从 redis 2.0 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份
master node 会在内存中创建一个 backlog,master 和 slave 都会保存一个 replica offset 还有一个 master id,offset 就是保存在 backlog 中的,如果 master 和 slave 网络连接断掉了,slave 会让 master 从上次的 replica offset 开始继续复制
但是如果没有找到对应的 offset,那么就会执行一次 resynchronization
无磁盘化复制
master 在内存中直接创建 RDB,然后发送给 slave,不会在自己本地落地磁盘了
repl-diskless-sync
repl-diskless-sync-delay 等待一定时长再开始复制,因为要等更多 slave 重新连接过来
过期 key 处理
slave 不会过期 key,只会等待 master 过去 key
如果 master 过期了一个 key,或者通过 LRU 淘汰了一个 key,那么会模拟一条 del 命令发送给 slave
14. redis 如何实现高可用架构?
在主从架构的场景下
如果 slave 挂掉一个之后,因为还存在其它的 slave,所以 redis 依旧可以正常运行
但是如果 master 宕机了,就会造成无法进行写操作了,此时的 redis 就不可用了
为了应对这种情况,就引入了 sentinal 哨兵,当 master 发送故障时,能够自动检测,并且在很短的时间内将某个 slave node 自动切换为 master node,这个过程叫做主备切换,实现了 redis 的主从架构下的高可用性
哨兵是 redis 集群架构中非常重要的一个组件,主要功能如下:
- ① 集群监控:负责监控 redis master 和 slave 进程是否正常工作
- ② 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员
- ③ 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上
- ④ 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址
哨兵也是分布式的,作为一个哨兵集群去运行,互相协同工作
- ① 故障转移时,判断一个 master node 是宕机了,需要大部分的哨兵同意才行,涉及到分布式选举的问题
- ② 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身也是高可用的
哨兵的核心知识
- ① 哨兵至少需要 3 个实例来保证自己的健壮性
- ② 哨兵 + redis 主从的部署架构,是不会保证数据零丢失的,只能保证 redis 集群的高可用
除了采用哨兵机制,对 redis 的数据持久化也是高可用架构的一部分
15. 为什么 redis 哨兵集群只有 2 个节点无法正常工作?
如果哨兵集群仅仅部署了 2 个哨兵实例
master 宕机,sentinal 1 和 sentinal 2 中只要有 1 个哨兵认为 master 宕机就可以进行切换,同时 sentinal 1 和 sentinal 2 会选举出一个哨兵来执行故障转移
同时这个时候,需要 majority,也就是大多数哨兵都是运行的,2 个哨兵的 majority 就是 2,也就是 2 个哨兵都运行着,就可以运行执行故障转移
但是如果 master 和 sentinal 1 运行的机器宕机了,那么就只要 sentianl 2 这一个哨兵在运行,就没有足够的 majority 来运行执行故障转移
经典的 3 节点哨兵集群
如果 master 所在的机器宕机了,那么三个哨兵还剩 2 个,sentinal 1 和 sentinal 2 可以一致认为 master 宕机,然后选举出一个来执行故障转移
16. redis 哨兵主备切换的数据丢失问题:异步复制、集群脑裂
两种导致数据丢失的情况:
- (1)异步复制导致数据丢失
因为 master —> slave 复制是异步的,所以可能有部分数据还没有复制到 slave,master 就宕机了,此时这部分数据就丢失了
- (2)脑裂导致的数据丢失
脑裂,也就是说,某个 master 所在机器突然脱离了正常的网络,跟其它的 slave 机器不能连接,但是实际上 master 还运行着
此时哨兵可能就会认为 master 宕机了,然后开始选举,将其它的 slave 选举为 master
这个时候集群中就有两个 master,也就是所谓的脑裂
此时虽然某个 slave 被切换成 master,但是可能 client 还没来得及切换到新的 master,还继续写向旧的 master 的数据可能就丢失了
因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上,自己的数据会清空,重新从新的 master 复制数据
解决异步复制和脑裂导致的数据丢失
min-slaves-to-write 1
min-slaves-max-lag
要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒
如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒,那么这个时候 master 就不会接收任何请求
上面两个配置可以减少异步复制和脑裂导致的数据丢失
-
① 减少异步复制的数据丢失
有了 min-slaves-max-lag 这个配置,就可以确保说,一旦 slave 复制数据和 ack 延时太长,就认为可能 master 宕机损失的数据太多了,那么就拒绝写请求,这样可以把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低到可控范围内
-
② 减少脑裂的数据丢失
如果一个 master 出现了脑裂,跟其它 slave 丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的 slave 发送数据,而且 slave 超过 10 秒没有给自己 ack 消息,那么就直接拒绝客户端的写请求
这样脑裂后的旧 master 就不会接收 client 的新数据,也就避免了数据丢失
17. redis 哨兵的核心原理
1、sdown 和 odown 转换机制
- sdown 是主观宕机,就一个哨兵如果自己觉得一个 master 宕机了,那么就是主管宕机
- odown 是客观宕机,如果 quorum 数量的哨兵都觉得一个 master 宕机了,那么就算客观宕机
sdown 达成的条件很简单,如果一个哨兵 ping 一个 master,超过了 is-master-down-after-milliseconds 指定的毫秒数之后,就主观认为 master 宕机了
sdown 到 odown 转换的条件很简单,如果一个哨兵在指定的时间内,收到 quorum 指定数量的其他哨兵也认为那个 master 是 sdown,那么就认为是 odown了,客观认为 master 宕机
2、哨兵和 slave 集群的自动发现机制
哨兵互相之间的发现,是通过 redis 的 pub/sub 系统实现的,每个哨兵都会往 _sentinal_:hello
这个 channel 里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵存在
每隔两秒钟,每个哨兵都会往自己监控的某个 master+slave 对应 _sentinal_:hello
channel 里发送一个消息,内容是自己的 host、ip 和 runid 还有对这个 master 的监控配置
每个哨兵也会去监听自己监控的每个 master+slave 对应 _sentinal_:hello
channel ,然后去感知到同样在监听这个 master+slave 的其他哨兵的存在
每个哨兵还会跟其他哨兵交换对 master 的监控配置,互相进行监控配置的同步
3、slave 配置的自动纠正
哨兵会负责自动纠正 slave 的一些配置,比如 slave 如果要成为潜在的 master 候选人,哨兵会确保 slave 在复制现有的 master 的数据,如果 slave 连接到一个错误的 master 上,比如故障转移之后,那么哨兵会确保它们连接到正确的 master 上
4、slave —> master 选举算法
如果一个 master 被认为 odown 了,而且 majority 哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个 slave 来,会考虑 slave 的一些信息
- ① 跟 master 断开连接的时长
- ② slave 优先级
- ③ 复制 offset
- ④ run id
如果一个 slave 跟 master 断开连接已经超过了 down-after-milliseconds 的 10 倍,外加 master 宕机的时间长,那么 slave 就被认为不适合选举为 master
(dwon-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
接下来会对 slave 进行排序
- ① 按照 slave 优先级进行排序,slave priority 越低,优先级就越高
- ② 如果 slave priority 相同,那么看 replica offset 哪个 slave 复制了越多的数据,offset 越靠后,优先级就越高
- ③ 如果上面两个条件都相同,那么选择一个 run id 比较小的哪个 slave
5、quorum 和 majority
每次一个哨兵要做主备切换。首先需要quoum数量的哨兵认为odomn,然后选举出一个哨兵来做切换,这个哨兵还得得到majority哨兵的授权,才能正式执行切换
如果quorum < majority,比如 5 个哨兵,majority就是 3,quorum 设置为 2,那么就 3 个哨兵授权就可以执行切换
但是如果 quorum >= majority,那么必须 quorum 数量的哨兵都授权,比如 5 个哨兵,quorum 是 5,那么必须 5 个哨兵都同意授权,才能执行切换
6、configuration epoch
执行切换的那个哨兵,会从要切换到的新 master 那里得到一个 configuration epoch,这就是一个 version 号,每次切换的 version 号都必须是唯一的
如果第一个选举出的硝兵切换失败了,那么其他哨兵,会等将待 failover-timeout 时间,然后接替继续执行切换。此时会重新获取一个新的 configuration epoch,作为新的 version 号
7、configuraiton传播
哨兵完成切换之后,会在自己本地更新生成最新的 master 配置,然后同步给其他的哨兵,就是通过之前说的 pub/sub 消息机制
这里之前的 version 号就很重要了,因为各种消息都是通过一个 channel 去发布和监听的,所以一个哨兵完成一次新的切换之后,新的 master 配置是跟着新的 version 号的其他的哨兵都是根据版本号的大小来更新自己的 master 配置的
18. redis 的持久化有哪几种?
redis 如果仅仅是将数据缓存到内存中,如果 redis 宕机了,再重启,内存中的数据就会被全部弄丢
所以必须要将 redis 中的数据写入到内存的同时持久化到磁盘中,这样即使 redis 宕机了,也不至于所有的数据都会丢失
-
RDB:对 redis 中的数据执行周期性的持久化,也就是说每隔一段时间生成 redis 中数据的一份完整快照
-
AOF:对每条写命令做日志,以 append-only 的模式写入一个日志文件中,在 redis 重启的时候,可以通过回放 AOF 日志中的写入指令来重构整个数据集
现代操作系统中, 写文件不是直接写磁盘,会先写到 OS Cache,然后每隔一定时间调一次 fsync 操作,强制将 OS Cache 中的数据刷入磁盘文件中
redis 中的数据是限量的,不可能说 redis 内存中的数据无限增长,进而导致 AOF 文件无限增长
内存大小是一定的,当 redis 占用内存到一定的时候,redis 就会用缓存淘汰算法 LRU,自动将一部分数据从内存中清楚
AOF 存放的是写命令,所以会不断的膨胀,当大到一定的时候,AOF 就会做 rewrite 操作,redis rewrite 操作就会基于当时 redis 内存中的数据,来重新构建一个更小的 AOF 文件,然后将旧的 AOF 文件给删除掉
如果想要 redis 仅仅作为纯内存的缓存来用,那么可以禁止 RDB 和 AOF 所有的持久化机制
通过 RDB 和 AOF ,都可以将 redis 内存中的数据给持久化到磁盘上来,然后可以将这些数据备份到别的地方,比如说阿里云
如果 redis 挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务器上拷贝回来之前的数据,放在指定的目录中,然后重新启动 redis,redis 就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外服务
如果同时使用 RDB 和 AOF 两种持久化机制,那么在 redis 重启的时候,会使用 AOF 来重新构建数据,因为 AOF 中的数据更加完整
19. RDB 和 AOF 的优缺点都有哪些?
RDB 持久化机制的优点
- ① RDB 会生成多个数据文件,每个数据文件都代表了某一个时刻中 redis 的数据,这种多个数据文件的方式,非常适合做冷备份,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说云服务上去,在国内可以是阿里云的 ODPS 分布式存储上,以预定好的备份策略来定期备份 redis 中的数据
- ② RDB 对 redis 对外提供的读写服务,影响非常小,可以让 redis 保持高性能,因为 redis 主进程只需要 fork 一个子进程,让子进程执行磁盘 IO 操作来进行 RDB 持久化即可
- ③ 相对于 AOF 持久化机制来说,直接基于 RDB 数据文件来重启和恢复 redis 进程,更加快速
RDB 持久化机制的缺点
- ① 如果想要在 redis 故障时,尽可能少的丢失数据,那么 RDB 没有 AOF 好,一般来说,RDB 数据快照文件,都是每隔 5 分钟或者更长时间生成一次,这个时候就得接受一旦 redis 进程宕机,那么会丢失最近 5 分钟的数据
- ② RDB 每次在 fork 子进程来执行 RDB 快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,甚至是数秒
AOF 持久化机制的优点
- ① AOF 可以更好的保护数据不丢失,一般 AOF 会每隔 1 秒,通过一个后台线程执行一次 fsync 操作,最多丢失 1 秒钟的数据
- ② AOF 日志文件以 append-only 模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复
- ③ AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写,因为在 rewrite log 的时候,会对其中的数据进行压缩,创建出一份需要恢复数据的最小日志出来,再创建新日志文件的时候,老的日志文件还是照常写入,当新的 merge 后的日志文件 ready 的时候,再交换新老日志文件即可
- ④ AOF 日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复,比如某人不小心用 flushall 命令清空了所有数据,只要这个时候后台 rewrite 还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条 flushall 命令给删了,然后再将 AOF 文件放回去,就可以通过恢复机制,自动恢复所有数据
AOF 持久化机制的缺点
- ① 对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大
- ② AOF 开启后,支持的写 QPS 会比 RDB 支持的写 QPS 地,因为 AOF 一般会配置成每秒 fsync 一次日志文件,当然,每秒一次 fsync 性能也还是很高的
- ③ 以前 AOF 发生过 bug,就算通过 AOF 记录的日志,进行数据恢复的时候,没有恢复一摸一样的数据出来,所有说,类似 AOF 这种较为复杂的基于命令日志 /merge/ 回放的方式,比基于 RDB 每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有 bug,不过 AOF 就是为了避免 rewrite 过程导致 bug,因此每次 rewrite 并不是基于旧的指令日志进行 merge 的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多
RDB 每次写,都是直接写 redis 内存,只是在一定的时候,才会将数据写入磁盘中
AOF 每次都是要写文件的,虽然可以快速写入 OS Cache 中,但是还是有一定的时间开销的,速度比 RDB 略慢一些
20. 什么是缓存雪崩和缓存穿透?并如何解决?
缓存雪崩现象
假如说在高峰期时,一秒有 5000 个请求去访问系统 A,正常清空下这个 5000 个请求有 4000 个请求是走缓存的,但是此时 redis 宕机了,导致这 5000/s 个请求直接落在数据库上,数据库扛不住这么多请求,导致数据库挂掉了,整个系统直接崩溃
缓存雪崩的解决方案
- 事前:redis 高可用,主从+哨兵,redis 集群避免全盘崩溃
- 事中:本地 ehchache 缓存 + hystrix 限流 & 降级,避免 MySQL 被打死
- 事后:redis 持久化,快速 恢复缓存数据
缓存穿透现象
假如说有 5000/s 个请求访问系统 A,只有 1000 个请求走了缓存,还有 4000 个请求是黑客恶意攻击的,没有走缓存而是直接落在数据库上,就导致数据库被打死了
缓存穿透就是大量的请求访问系统,但是缓存中没有这个数据,然后请求只能走数据库
缓存穿透的解决方案
就算从数据库中没有查到对应的值,也在缓存中存入这个 key,只不过 value 设置为 null 就行了
21. 如何保证缓存和数据库双写一致性?
最经典的缓存+数据库读写模式:cache aside pattern
- ① 读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应
- ② 更新的时候,先删除缓存,然后再更新数据库
为什么是删除缓存,而不是更新缓存呢?
原因很简单,因为缓存有的时候,不简单是数据库中直接取出来的值
比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据,并进行运算,才能计算出缓存最新的值
更新缓存的代价是很高的,如果频繁修改一个缓存涉及的多个表,那么这个缓存也需要被频繁的更新
而删除缓存,其实就是一个懒加载的思想,不需要每次都重新做复杂的计算,让它在需要被使用的时候再重新计算
1、最初级的缓存不一致问题以及解决方案
问题:先修改数据库,再删除缓存,如果删除缓存失败,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致
解决思路:先删除缓存,再修改数据库,如果删除缓存成功了,但是修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致,因为读的时候缓存没有,则读数据库中的旧数据,然后更新到缓存中
2、比较复杂的数据不一致问题分析
数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没有修改
一个请求过来,去读缓存,发现缓存空了,去查数据库,查到修改前的旧数据,放到缓存中
数据变更的程序完成了数据库的修改,导致缓存中存放的是旧数据,但是数据库中的数据已经更新了
为什么上亿流量高并发场景下,缓存会出现这个问题?
只有在对一个数据进行并发读写的时候,才可能会出现这种问题
其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景
但是问题是,如果每天是上亿的流量,每秒并发读都是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况
数据库与缓存更新和读取操作进行异步串行化
如上图所示,为了保证库存系统要保证数据库和缓存的一致性
可以在库存系统中添加队列,可以根据商品 id 进行 hash 取值,再加上对内存队列进行取模,均匀的将每个商品路由到不同的内存队列中
每一个线程分别处理一个队列中积压的请求,读和写的操作和 cache aside pattern 模式相同
读的时候先读缓存,缓存没有就读数据库,取出数据之后再写入缓存
更新的时候先删除缓存,再更新数据库
因为每个线程处理存放同一方式路由到指定队列的数据,而且整个过程是串行的,所有避免了并发的问题
延时双删
或者采用延时双删的策略,就是修改数据的时候先删除缓存,再去更新数据库的数据,然后再删除缓存一次,这样就能保证每次更新完数据之后,缓存都是没有的
22. redis 并发竞争问题该如何解决?
redis 并发竞争问题是什么?
就是多个客户端同时并发写一个 key,可能本来应该先到的数据后到了,导致数据版本错了。或者是多个客户端同时获取一个 key,修改之后再回写去,只要顺序错了,数据就错了
如下情况,本来 test_key = v1,系统 A 部署在三台机器上,在同一个时刻并发修改了 test_key ,本来 test_key 的值应该变化为:v1 -> v2 -> v3 -> v4
,而实际情况为:v1 -> v2 -> v4 -> v3
,就出问题了
解决方案
首先可以添加一个分布式锁,在修改缓存之前先去获取锁,确保在同一个时间只能有一个系统实例在操作某个 key,别的都不允许读和写,其次在写入缓存的时候还需要加上一个时间戳的信息,在系统实例竞争到锁之后,在修改缓存时间的时候先去判断缓存中 key 的时间戳是否在当前需要修改值的时间戳之前,如果是,则修改,否则不修改,这样就能保证前修改的数据不会覆盖掉之后修改的数据