- 1、说一下 Redis
- 2、Redis 的数据类型
- 3、为什么要用缓存
- 4、Redis 的单线程模型是怎么实现的
- 既然是单线程,那怎么监听大量的客户端连接呢?
- Redis 没有使用多线程吗?
- Redis 6.0 为什么引入了多线程?
- 为什么要给缓存设置过期时间
- Redis 如何判断数据是过期的呢?
- Redis 的过期策略了解么?
- Redis 内存淘汰机制了解么?
- 你们是怎么让缓存持久化的呢?
- 如何保证缓存和数据库数据的一致性?
- 为什么是删除缓存,而不是更新缓存?
- 什么是缓存穿透
- 如何解决缓存穿透
- 为什么布隆过滤器会误判
- 什么是缓存击穿
- 如何解决缓存击穿
- 什么是缓存雪崩
- 如何解决缓存雪崩
- 你们项目中缓存是如何使用的
- 为啥 Redis 单线程模型也能效率这么高?
- Redis 单机能承载多高并发?
- 如果单机扛不住如何扩容扛更多的并发?
- 如何保证 Redis 的高并发?
- Redis 主从复制的核心机制
- Redis 的主从复制原理能介绍一下么?
- Redis 会不会挂?
- 既然 Redis 会挂,那怎么保证 Redis 是高可用的?
- 你先说一下哨兵模式
- 为什么非得配置 3 个哨兵呢?
- Redis 的哨兵原理能介绍一下么?
- 为什么主备切换会导致数据丢失呢?
- 那如何解决数据丢失呢?
- Redis 集群模式你能说一下吗?
- 在集群模式下,Redis 的 key 是如何寻址的?
- 分布式寻址都有哪些算法?
- 了解一致性 hash 算法吗?
- 集群模式下是怎么实现高可用的呢?
- 你们 Redis 是怎么部署的?
- 你们的机器是什么配置?
- 你存的每条数据大小是多少?
1、说一下 Redis
Redis 是一个使用 C 语言开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的 ,也就是说它是内存数据库,所以读写速度非常快,因此 Redis 常被用在缓存当中。
另外,Redis 除了做缓存之外,也经常用来做分布式锁,甚至是消息队列。
Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。
2、Redis 的数据类型
string
- String 数据结构就是简单的 key-value 类型。
list
- list 是一个双向链表,支持反向查找和遍历
- 常用于发布订阅、消息队列、慢查询
hash
- hash 类似于 JDK1.8 之前的 HashMap,hash 是⼀个 string 类型的 field 和 value 的映射表,特别适合存储对象,后续操作的时候,可以直接修改这个对象中的某个字段的值。 比如存储用户信息、商品信息
set
- 类似于 Java 中的 HashSet,不会出现重复数据,也没有插入顺序,set 还可以很轻易的判断某个成员是否在一个 set 集合内,可以基于 set 轻易实现交集、并集、差集的操作。比如:可以将⼀个用户所有的关注人存在⼀个集合中,将其所有粉丝存在⼀个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
sorted set
- 和 set 相比,sorted set 增加了⼀个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。
- 可以应用在直播系统中,查看实时排行信息,包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息
3、为什么要用缓存
主要是为了提升用户体验以及应对更多的用户。
4、Redis 的单线程模型是怎么实现的
Redis 基于 Reactor 模式来设计开发了自己的一套高效的事件处理模型
既然是单线程,那怎么监听大量的客户端连接呢?
Redis 通过IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。
这样的好处非常明显: I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector
组件很像)。
Redis 没有使用多线程吗?
Redis 在 4.0 中就已经加入了对多线程的支持,不过主要是针对一些大键值对的删除操作的命令,Redis 6.0 之前还是使用单线程处理,我认为有以下几点原因:
- 单线程编程容易并且更容易维护;
- Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
- 多线程会存在死锁、线程上下文切换等问题,甚至会影响性能。
Redis 6.0 为什么引入了多线程?
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈,不过执行命令方面仍然是单线程顺序执行
为什么要给缓存设置过期时间
因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟内存溢出,
当然还有一些特殊的业务场景需要,比如短信验证码、用户登录的 token、一些活动场次、活动商品等等。
Redis 如何判断数据是过期的呢?
Redis 通过一个过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
Redis 的过期策略了解么?
如果你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢?
常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西):
- 惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
- 定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。
但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就内存溢出了。
怎么解决这个问题呢?答案就是:Redis 内存淘汰机制。
Redis 内存淘汰机制了解么?
相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
Redis 提供 6 种数据淘汰策略:
- volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used):当内存不足以容纳写入新数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
4.0 版本后增加以下两种:
- volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
你们是怎么让缓存持久化的呢?
Redis 有两种持久化的方式,一种是快照(snapshotting,RDB),另一种是(append-only file,AOF)
RDB
- Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,等持久化过程都结束了,再用这个临时文件替换上次持久化好的文件,整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,RDB 是 Redis 默认采用的持久化方式
- 优点:适合大规模的数据恢复、
- 缺点:对数据的完整性要求不高;需要一定的时间间隔进行操作,如果Redis意外宕机了,最后一次修改的数据就没有了;fork进程的时候,会占用一定的内存空间
AOF
- 以日志的形式来记录每个写操作,将 Redis 执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,换一种说法,redis 重启的话,就是根据日志文件的内容,将写指令从前到后执行了一次,才完成了数据的恢复,是现在主流的持久化方案,Redis 默认没有开启 AOF,需要手动开启。
- 优点:实时性更好
一般会选择在从机上使用 RDB 持久化模式,15 分钟备份一次就够了,只保留 save 900 1
这条规则。
如果开启 AOF ,好处是在最恶劣情况下也只是丢失不超过两秒的数据,启动脚本比较简单,只需要加载自己的 AOF 文件就可以了,
- 代价一是带来了持续的 IO,
- 二是重写的最后,将重写过程中产生的新数据写到新文件这期间造成的阻塞是不可避免的。只要硬盘许可,应该尽量减少 AOF 重写的频率,AOF 重写的基础大小默认值 64 M太小了,可以设到 5G 以上,默认超过原大小 100% 大小重写可以改到适当的数值。
如果不开启 AOF ,仅靠主从复制实现高可用性也可以,能省掉一大笔 IO,也减少了重写时带来的系统波动。代价是如果主机从机同时宕机,会丢失十几分钟的数据,启动脚本也要比较两个主机从机中的 RDB文件,载入较新的那个,微博就是这种架构。
如何保证缓存和数据库数据的一致性?
在对数据库进行修改的时候,删除缓存数据,如果缓存服务当前不可用导致删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。
为什么是删除缓存,而不是更新缓存?
原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。可能是数据库查询了多个表,然后通过这多个表的值,才能计算出缓存最新的值的。
另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?
举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。
其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都把里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。
什么是缓存穿透
大量请求查询的 key 在缓存中没有,所以去查询数据库,数据库压力瞬时增大,最终导致崩溃。
如何解决缓存穿透
缓存 null 值,并指定短暂的过期时间,比如:1 分钟
- 这种方式可以解决大量请求查询同一个 key,但是如果黑客恶意攻击,每次构建不同的请求 key,就会导致 Redis 中缓存大量无效的 key 。
布隆过滤器:通过它可以非常方便地判断⼀个给定数据是否存在于海量数据中
- 具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
- 注意:布隆过滤器可能会存在误判的情况
为什么布隆过滤器会误判
当一个元素加入布隆过滤器中的时候,会进行以下操作:
- 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
- 根据得到的哈希值,在位数组中把对应下标的值置为 1。
判断一个元素是否存在于布隆过滤器的时候,则会进行以下操作
- 对给定元素再次进行相同的哈希计算;
- 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
然后,一定会出现这样一种情况:不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)
总结:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
什么是缓存击穿
大量请求进入缓存中查找热点数据的时候,它正好到了过期时间,导致缓存查询失败,所有请求打到数据库上,比如秒杀数据。
如何解决缓存击穿
优化一
- 一开始想着给查数据库的方法加上锁,得到锁之后,再去缓存中查询一次,如果没有,再继续查询数据库
- 结果:查询了两次数据库,
- 原因:发现第一个线程查询完数据库之后,往缓存中放数据之前,就将锁释放掉了,结果第二个线程进来之后,去查询缓存,发现缓存中还没有数据,这是因为第一个线程由于是第一次与 Redis 建立连接、以及建立线程池,这一系列的网路交互还没有做完,第二个线程就进来了,所以没查到,导致又查询了一次数据库,然后第三个线程进来的时候,查询缓存中已经有了,那是第一个线程放的数据,所以不会再去查询数据库了。
优化二:将结果放入缓存中的这个操作也加上锁,解决。
什么是缓存雪崩
大量的缓存在同一时间过期,导致请求全部打到数据库上,导致服务器宕机。
如何解决缓存雪崩
- 多增设几台redis,这样一台挂掉之后其它的还可以继续工作,其实就是搭建集群(异地多活的思想)。
- 在缓存失效后,通过加锁或者队列来控制数据库写缓存的线程数量,比如对某个 key 只允许一个线程查询数据和写缓存,其它线程等待。
- 数据预热的含义就是在正式部署之前,先把可能访问的数据预先访问一遍,这样大部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
你们项目中缓存是如何使用的
- 存储了一些热门商品、分类、活动商品,提升了首页的加载速度
- 存储 Session,解决分布式 Session 同步问题
- 存储临时用户与普通用户的购物车数据
- 使用信号量,实现秒杀活动中商品的快速扣减
为啥 Redis 单线程模型也能效率这么高?
- 纯内存操作。
- 核心是基于非阻塞的 IO 多路复用机制。
- C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。
- 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。
Redis 单机能承载多高并发?
单机的 Redis,能够承载的 QPS 大概就在上万到几万不等,一般对于那种并发不高的小项目来说是够了,但是对于那种并发规模稍微大一点的项目就不够了。
如果单机扛不住如何扩容扛更多的并发?
Redis 实现高并发主要依靠主从架构,一主多从,主用来写入数据,单机几万 QPS,从用来查询数据,多个从实例可以提供每秒 10w 的 QPS。
如何保证 Redis 的高并发?
主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样可以很轻松的实现水平扩容,支撑读高并发。
Redis 主从复制的核心机制
- 一个 master node 是可以配置多个 slave node 的;
- Redis 采用异步方式复制数据到 slave 节点,从 Redis2.8 开始,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,这样才能确保启动的时候,是有数据的。
Redis 的主从复制原理能介绍一下么?
当启动一个 slave node 的时候,它会发送一个 PSYNC
命令给 master node。
如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization
全量复制。此时 master 会启动一个后台线程,开始生成一份 RDB
快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。 RDB
文件生成完毕后, master 会将这个 RDB
发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。
Redis 会不会挂?
会,如果 master node 死掉了,那就没法写数据了,写缓存的时候,全部失效。没有 master 给它们复制数据了,slave node 也就没法读数据了,系统相当于不可用了。
既然 Redis 会挂,那怎么保证 Redis 是高可用的?
如果是使用的主从架构部署,那么加上哨兵就可以了,它能够在后台监控 master node 是否故障,如果故障了,将采用投票机制,自动将 slave node 转换为 master node。
你先说一下哨兵模式
哨兵是 Redis 集群架构中非常重要的一个组件,主要有以下功能:
- 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
- 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
- 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
- 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
哨兵用于实现 Redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
- 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行。
- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的。
- 哨兵至少需要 3 个实例,来保证自己的健壮性。
- 哨兵 + Redis 主从的部署架构,是不保证数据零丢失的,只能保证 Redis 集群的高可用性。
为什么非得配置 3 个哨兵呢?
quorum:规定数,指明当有多少个 sentinel 认为一个 master 失效时,master 才算真正失效
majority:大多数,从 2 个哨兵,majority=2 开始,1 个哨兵没有 majority 的情况
如果 quorum < majority,比如 5 个哨兵,majority 就是 3,quorum 设置为 2,那么有 3 个哨兵授权就可以执行切换。
但是如果 quorum >= majority,那么必须 quorum 数量的哨兵都授权,比如 5 个哨兵,quorum 是 5,那么必须 5 个哨兵都同意授权,才能执行切换。
如果哨兵集群仅仅部署了 2 个哨兵实例 s1、s2,配置 quorum = 1,如果 master 宕机, s1 和 s2 中只要有 1 个哨兵认为 master 宕机了,就可以进行切换,同时 s1 和 s2 会选举出一个哨兵来执行故障转移。但是同时这个时候,需要 majority,也就是大多数哨兵都是运行的。
如果此时仅仅是 M1 进程宕机了,哨兵 s1 正常运行,那么故障转移是 OK 的。
但是如果 M1 和 S1 都宕机了,那么哨兵就只剩 1 个,1 个哨兵是没有 majority 来允许执行故障转移的
而 3 节点哨兵集群是这样,配置 quorum=2
,如果 M1 和 S1 都宕机了,那么三个哨兵还剩下 2 个, 3 个哨兵的 majority 是 2,S2 和 S3 可以一致认为 master 宕机了,然后选举出一个来执行故障转移。
Redis 的哨兵原理能介绍一下么?
sdown 和 odown 转换机制
- sdown 是主观宕机,如果一个哨兵 ping 一个 master,超过了指定的毫秒数之后,就主观认为 master 宕机了
- odown 是客观宕机,如果一个哨兵在指定时间内,收到了 quorum 数量的其它哨兵也认为那个 master 是 sdown 的消息,那么就认为是 odown 了。
哨兵集群的自动发现机制
哨兵互相之间的发现,是通过 Redis 的 pub/sub
系统实现的
每隔两秒钟,每个哨兵都会往自己监控的某个 master+slaves 对应的 __sentinel__:hello
channel 里发送一个消息,内容是自己的 host、ip 和 runid 还有对这个 master 的监控配置。
每个哨兵也会去监听自己监控的每个 master+slaves 对应的 __sentinel__:hello
channel,然后去感知到同样在监听这个 master+slaves 的其他哨兵的存在。
每个哨兵还会跟其他哨兵交换对 master
的监控配置,互相进行监控配置的同步。
slave 配置的自动纠正
哨兵会负责自动纠正 slave 的一些配置,比如 slave 如果要成为潜在的 master 候选人,哨兵会确保 slave 复制现有 master 的数据;
如果 slave 连接到了一个错误的 master 上,比如故障转移之后,那么哨兵会确保它们连接到正确的 master 上。
slave -> master 选举算法
如果一个 master 被认为 odown 了,而且 majority 数量的哨兵都允许主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个 slave 来,会考虑 slave 的一些信息:
- 跟 master 断开连接的时长
- slave 优先级
- 复制 offset
- run id
如果一个 slave 跟 master 断开连接的时间已经超过了 down-after-milliseconds
的 10 倍,外加 master 宕机的时长,那么 slave 就被认为不适合选举为 master。
(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
哨兵会对一套 Redis master+slaves 进行监控,有相应的监控的配置。
执行切换的那个哨兵,会从要切换到的新 master(salve->master)那里得到一个 configuration epoch,这就是一个 version 号,每次切换的 version 号都必须是唯一的。
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待 failover-timeout 时间,然后接替继续执行切换,此时会重新获取一个新的 configuration epoch,作为新的 version 号。
configuration 传播
哨兵完成切换之后,会在自己本地更新生成最新的 master 配置,然后同步给其他的哨兵,就是通过之前说的 pub/sub
消息机制。
这里之前的 version 号就很重要了,因为各种消息都是通过一个 channel 去发布和监听的,所以一个哨兵完成一次新的切换之后,新的 master 配置是跟着新的 version 号的。其他的哨兵都是根据版本号的大小来更新自己的 master 配置的。
为什么主备切换会导致数据丢失呢?
异步复制导致的数据丢失
- 因为 master->slave 的复制是异步的,所以可能有部分数据还没复制到 slave,master 就宕机了,此时这部分数据就丢失了。
脑裂导致的数据丢失
- 脑裂,也就是说,某个 master 所在机器突然脱离了正常的网络,跟其他 slave 机器不能连接,但是实际上 master 还运行着。
- 此时哨兵可能就会认为 master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时候,集群里就会有两个 master ,也就是所谓的脑裂。
- 此时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 写数据。因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,重新从新的 master 复制数据。而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。
那如何解决数据丢失呢?
进行如下配置:
min-slaves-to-write 1
min-slaves-max-lag 10
表示,要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。
如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。
减少异步复制数据的丢失
- 有了
min-slaves-max-lag
这个配置,就可以确保说,一旦 slave 复制数据和 ack 延时太长,就认为可能 master 宕机后损失的数据太多了,那么就拒绝写请求,这样可以把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低到可控范围内。
减少脑裂的数据丢失
- 如果一个 master 出现了脑裂,跟其他 slave 丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的 slave 发送数据,而且 slave 超过 10 秒没有给自己 ack 消息,那么就直接拒绝客户端的写请求。因此在脑裂场景下,最多就丢失 10 秒的数据。
Redis 集群模式你能说一下吗?
Redis cluster,主要是针对海量数据+高并发+高可用的场景。Redis cluster 支撑 N 个 Redis master node,每个 master node 都可以挂载多个 slave node。这样整个 Redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。
在 Redis cluster 架构下,每个 Redis 要放开两个端口号,比如一个是 6379,另外一个就是 加 1w 的端口号,比如 16379。
16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。
在集群模式下,Redis 的 key 是如何寻址的?
采用 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 算法(大量缓存重建)
- 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
- Redis cluster 的 hash slot 算法
了解一致性 hash 算法吗?
一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。
来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。
在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。
然而,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡
集群模式下是怎么实现高可用的呢?
高可用的原理跟哨兵模式几乎是一样的
判断节点宕机
如果一个节点认为另外一个节点宕机,那么就是 pfail
,主观宕机。
如果多个节点都认为另外一个节点宕机了,那么就是 fail
,客观宕机,跟哨兵的原理几乎一样,sdown,odown。
在 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 是怎么部署的?
我们采用集群模式,一共10 台机器,5 台机器部署了 Redis 主实例,另外 5 台机器部署了 Redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰 QPS 可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求每秒。
5 台机器对外提供读写,一共有 50g 内存。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,Redis 从实例会自动变成主实例继续提供读写服务。
你们的机器是什么配置?
32G 内存 + 8 核 CPU + 1T 磁盘,分配给 Redis 进程的是 10g 内存
你存的每条数据大小是多少?
像商品这种数据,每条数据是 10KB。100 条数据是 1MB,10 万条数据是 1G。常驻内存的是 200 万条商品数据,占用内存是 20G,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。