Redis
Redis面试知识点
听说过Redis吗?它是什么?
Redis是一个数据库,不过与传统数据库不同的是Redis的数据库是存在内存中,所以读写速度非常快,因此 Redis被广泛应用于缓存方向。
除此之外,Redis也经常用来做分布式锁,Redis提供了多种数据类型来支持不同的业务场景。除此之外,Redis 支持事务持久化、LUA脚本、LRU驱动事件、多种集群方案。
Redis的五种数据结构整理
简单动态字符串(Simple Dynamic String,SDS)
Redis没有直接使用C语言传统的字符串,而是自己构建了一种名为简单动态字符串(Simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
其实SDS等同于C语言中的char *,但它可以存储任意二进制数据,不能像C语言字符串那样以字符’\0’来标识字符串的结束,因此它必然有个长度字段。
定义:
1 |
|
优点:
- 获取字符串长度的复杂度为O(1)。
- 杜绝缓冲区溢出。
- 减少修改字符串长度时所需要的内存重分配次数。
- 二进制安全。
- 兼容部分C字符串函数。
它具有很常规的 set/get 操作,value 可以是String也可以是数字,一般做一些复杂的计数功能的缓存。
链表
当有一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的额字符串时,Redis就会使用链表作为列表建的底层实现。
节点底层结构:
1 |
|
list底层结构:
1 |
|
特性
- 链表被广泛用于实现Redis的各种功能,比如列表建、发布与订阅、慢查询、监视器等。
- 每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以Redis的链表实现是双端链表。
- 每个链表使用一个list结构表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息。
- 因为链表表头的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。
- 通过为链表设置不同的类型特定函数,Redis的链表可以用于保存各种不同类型的值。
字典
字典的底层是哈希表,类似 C++中的 map ,也就是键值对。
哈希表
1 |
|
哈希算法
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash算法。这种算法的优点在于即使输入的键是规律的,算法仍能给出一个个很好的随机分布性,并且算法的计算速度非常快。
哈希冲突的解决方式
Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用这个单向链表连接起来,这就解决了键冲突的问题。
特性
- 字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。
- Redis中的字典使用哈希表作为底层结构实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
- Redis使用MurmurHash2算法来计算键的哈希值。
- 哈希表使用链地址法来解决键冲突。
跳跃表
先看这样一张图:
如上图,我们要查找一个元素,就需要从头节点开始遍历,直到找到对应的节点或者是第一个大于要查找的元素的节点(没找到)。时间复杂度为O(N)。
这个查找效率是比较低的,但如果我们把列表的某些节点拔高一层,如下图,例如把每两个节点中有一个节点变成两层。那么第二层的节点只有第一层的一半,查找效率也就会提高。
查找的步骤是从头节点的顶层开始,查到第一个大于指定元素的节点时,退回上一节点,在下一层继续查找。
比如我们要查找16:
- 从头节点的最顶层开始,先到节点7。
- 7的下一个节点是39,大于16,因此我们退回到7。
- 从7开始,在下一层继续查找,就可以找到16。
这个例子中遍历的节点不比一维列表少,但是当节点更多,查找的数字更大时,这种做法的优势就体现出来了。还是上面的例子,如果我们要查找的是39,那么只需要访问两个节点(7、39)就可以找到了。这比一维列表要减少一半的数量。
为了避免插入操作的时间复杂度是O(N),skiplist每层的数量不会严格按照2:1的比例,而是对每个要插入的元素随机一个层数。
随机层数的计算过程如下:
- 每个节点都有第一层
- 那么它有第二层的概率是p,有第三层的概率是p*p
- 不能超过最大层数
zskiplistNode
1 |
|
一般来说,层的数量越多,访问其他节点的速度越快。
zskipList
1 |
|
特性
- 跳跃表是有序集合的底层实现之一
- Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点(redis5.0之后用listpack实现跳表)
- 每个跳跃表节点的层高都是1至32之间的随机数
- 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
- 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
- 跳表是一种实现起来很简单,单层多指针的链表,它查找效率很高,堪比优化过的二叉平衡树,且比平衡树的实现。
ziplist
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
特性
看他的名字就能看出来,是为了节省内存造的列表结构
Redis常见数据结构以及使用场景分别是什么?
String
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存
应用; 常规计数:微博数,粉丝数等。
List
list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表, 消息列表等功能都可以用Redis的 list 结构来实现。
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
Set
set 对外提供的功能与list类似,是一个列表的功能,特殊之处在于 set 是可以自动排重的。
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在 一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常
方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:sinterstore key1 key2 key3
将交集存在key1内。
Sorted Set
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维 度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。
有MySQL不就够用了吗?为什么要用Redis这种新的数据库?
主要是因为 Redis 具备高性能和高并发两种特性。
- 高性能:假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
- 高并发:直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
C++中的Map也是一种缓存型数据结构,为什么不用Map,而选择Redis做缓存?
严格意义上来说缓存分为本地缓存和分布式缓存。
那以 C++ 语言为例,我们可以使用 STL 下自带的容器 map 来实现缓存,但只能实现本地缓存,它最主要的特点是轻量以及快速,但是其生命周期随着程序的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用 Redis 或 Memcached 之类的称为分布式缓存,在多实例的情况下,各实例共享一份缓存数据,缓存具有一致性。这是Redis或者Memcached的优点所在,但它也有缺点,那就是需要保持 Redis 或 Memcached服务的高可用,整个程序架构上较为复杂。
使用Redis的好处有哪些?
- 访问速度快,因为数据存在内存中,类似于Java中的HashMap或者C++中的哈希表(如unordered_map/unordered_set),这两者的优势就是查找和操作的时间复杂度都是O(1)
- 数据类型丰富,支持String,list,set,sorted set,hash这五种数据结构
- 支持事务,Redis中的操作都是原子性
- 特性丰富:Redis可用于缓存,消息,按key设置过期时间,过期后将会自动删除
Memcached与Redis的区别都有哪些?
- 数据类型支持:Memcached仅支持简单的key-value结构的数据记录,而Redis支持更丰富的数据类型,包括字符串、哈希表、链表、集合和有序集合。
- IO模型:Memcached是多线程,非阻塞IO复用的网络模型,而Redis使用单线程的IO复用模型。由于Redis还提供一些非KV存储之外的排序,聚合功能,在执行这些功能时,复杂的CPU计算,会阻塞整个IO调度。
- 底层模型不同:
- 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。
- Redis直接自己构建了VM(virtual memory)机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
- 内存管理机制:Memcached使用Slab Allocation机制管理内存,而Redis采用的是包装的malloc/free。在Redis中,并不是所有的数据都一直存储在内存中的。当物理内存用完时,Redis可以将一些很久没用到的value交换到磁盘,也就是说可以持久化数据。
- 复制和高可用性:Redis支持主从复制,可以将数据从一个主节点同步到多个从节点,提高读取性能和可用性。而Memcached没有内置的复制机制,只能通过客户端实现数据的复制。
- 扩展性:Redis支持分片(Sharding)和集群(Cluster)两种方式来扩展性能和容量。而Memcached只支持简单的多节点部署,需要通过客户端来实现数据的分片。
- value值大小不同:Redis 最大可以达到 512MB;Memcached 只有 1MB
Memcached
Memcached是一个自由开源的,高性能,分布式内存对象缓存系统。Memcached是一种基于内存的key-value存储,用来存储小块的任意数据(字符串、对象)。这些数据可以是数据库调用、API调用或者是页面渲染的结果。
Redis比Memcached的优势在哪里?
- Memcached所有的值均是简单字符串,Redis作为其替代者,支持更为丰富的数据类型
- Redis 的速度比 Memcached 快很多
- Redis可以做到持久化数据
缓存中常说的热点数据和冷数据是什么?
热数据就是访问次数较多的数据,冷数据就是访问很少或者从不访问的数据。
note
需要注意的是只有热点数据,缓存才有价值对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
Redis 为什么是单线程的而不采用多线程方案?
从redis6.0开始支持多线程,但是多线程默认是禁用的,这主要是基于一种客观原因来考虑的。因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)
redis6.0以后支持多线程是因为随着硬件性能的提升,Redis的性能瓶颈可能出现在网络IO的读写上。这时候就可以使用多线程提高网络请求处理的并行度从而提高redis的性能。
单线程的Redis为什么这么快?
主要是有三个原因:
- Redis的全部操作都是纯内存的操作;
- Redis采用单线程,有效避免了频繁的上下文切换;
- 采用了非阻塞I/O多路复用机制。
了解Redis的线程模型吗?可以大致说说吗?
如果你打开看过 Redis 的源码就会发现Redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
- 多个 socket
- IO多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
使用 I/O 多路复用程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
一句话总结就是:“I/O 多路复用程序负责监听多个套接字, 并向文件事件分派器传送那些产生了事件的套接字。”
Redis设置过期时间的两种方案是什么?
我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间,主要可采用定期删除和惰性删除两种方案。
- 定期删除:Redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期这里是随机抽取的。为什么要随机呢?你想一想假如 Redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
- 惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。它是指某个键值过期后,此键值不会马上被删除,而是等到下次被使用的时候,才会被检查到过期,此时才能得到删除,惰性删除的缺点很明显是浪费内存。 除非你的系统去查一下那个 key,才会被Redis给删除掉。这就是所谓的惰性删除!
定期和惰性一定能保证删除数据吗?如果不能,Redis会有什么应对措施?
并不能保证一定删除,Redsi有一个Redis 内存淘汰机制来确保数据一定会被删除。(定期删除和惰性删除见上一条)
采用定期删除+惰性删除的方式,如果定期删除没有删除掉,同时我们也没有即时去请求key,也就是说惰性删除也没生效,那么此时内存占用就会越来越高,这时候就要用到内存淘汰机制。
在Redis.conf中有一行配置:maxmemory-policy volatile-lru,该配置就是配内存淘汰策略的,主要有以下六种方案:
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据,新写入操作会报错 ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
Redis对于大量的请求,是怎样处理的?
Redis是一个单线程程序,也就说同一时刻它只能处理一个客户端请求; 2、Redis是通过IO多路复用(select,epoll,kqueue,依据不同的平台,采取不同的实现)来处理多个客户端请求。
缓存雪崩、缓存穿透、缓存预热、缓存击穿、服务降级
缓存雪崩
缓存雪崩指的是缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决办法
- 设置不同的过期时间:为了避免大量的缓存数据同时失效,可以为不同的key设置不同的过期时间,使得缓存失效的时间点尽量均匀,一种做法是再设置的过期时间上加一个随机数,如加1~60s的随机数,这样即使再高并发的情况下。多个请求同时设置过期时间,由于有随机数的存在,也不会出现太多相同的过期key
- 使用互斥锁:当缓存失效的时候,第一个访问数据的线程可以使用互斥锁或者分布式锁去加载数据。这样其他的线程就会等待,直到数据被加载到缓存。这样可以防止所有的线程都去查询数据库,给数据库带来巨大的压力。
- 提高缓存的高可用性:通过使用一些高可用性的缓存解决方案,比如Redis Cluster,可以在某个节点失效的时候,自动切换到其他的节点,提高系统的可用性。
- 熔断机制:参考电路熔断,如果一条线路电压过高,保险丝会熔断,防止火灾。放到我们的系统中,如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回“系统繁忙”,快速释放资源。如果目标服务情况好转则恢复调用。
- 隔离机制:对不同类型的请求使用线程池来资源隔离,每种类型的请求互不影响,如果一种类型的请求线程资源耗尽,则对后续的该类型请求直接返回,不再调用后续资源,也不影响其他资源
- 限流模式:上述的熔断模式和隔离模式都属于出错后的容错处理机制,而限流模式则可以称为预防模式。限流模式主要是提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。这种模式不能解决服务依赖的问题,只能解决系统整体资源分配问题,因为没有被限流的请求依然有可能造成雪崩效应
- 预先更新缓存:在缓存失效之前,提前更新缓存。
缓存穿透
一般是黑客故意去请求缓存中不存在的数据(比如数据库中不存在的数据,所以一定不会出现在缓存中),导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方法:
布隆过滤器:这是最常见的一种解决方法了,它是将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压 力。
缓存空对象:当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;如果一个查询返回的数据为空(不管是数据不存 在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
但是这种方法会存在两个问题:
- 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
- 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
我们可以从适用场景和维护成本两方面对这两汇总方法进行一个简单比较:
- 适用场景:缓存空对象适用于数据命中不高且数据频繁变化且实时性较高的情况,而布隆过滤器适用数据命中不高且数据相对固定即实时性较低的情况
- 维护成本:缓存空对象的方法适合代码维护简单、需要较多的缓存空间、可以容忍数据不一致的情况,布隆过滤器适合代码维护较复杂以及缓存空间少的情况
缓存预热:系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户会直接查询事先被预热的缓存数据!
缓存击穿
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
解决方案:
- 加锁:在访问数据库时加锁,防止多个相同productId的请求同时访问数据库。
- 自动续期:出现缓存击穿问题是由于key过期了导致的。那么,在热点key快要过期之前,就自动给它续期
- 缓存不失效:很多热门key可以设置永久有效,比如双十一秒杀活动开始之前设置永久有效,等双十一过去之后再手动删除这些无用的缓存即可。
服务降级
服务降级一般是指在服务器压力剧增的时候,根据实际业务使用情况以及流量,对一些服务和页面有策略的不处理或者用一种简单的方式进行处理,从而释放服务器资源的资源以保证核心业务的正常高效运行。
服务降级和熔断的区别
服务降级和服务熔断都是在面对系统异常或压力过大的情况下,采取的一种保护措施,以保证系统的稳定性和可用性。然而,它们的触发原因、管理目标层次和实现方式都有所不同。
服务降级是从整体系统负荷出发,对某些负荷较高的情况,为了预防某些功能(业务场景)出现负荷过载或响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。
服务熔断则是应对系统服务雪崩的一种保险措施,给出的一种特殊降级措施。当某个服务(下游服务)出现故障引起,为了防止整个系统出现雪崩,暂时停止对该服务的调用。服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。
熔断时可能会调用降级机制,而降级时通常不会调用熔断机制。因为熔断是从全局出发,为了保证系统稳定性而停用服务,而降级是退而求其次,提供一种保底的解决方案,所以它们的归属关系是不同(熔断 > 降级)。
假如MySQL有1000万数据,采用Redis作为中间缓存,取其中的10万,如何保证Redis中的数据都是热点数据?
可以使用Redis的数据淘汰策略,Redis 内存数据集大小上升到一定大小的时候,就会施行这种策略。具体说来,主要有 6种内存淘汰策略:
- voltile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据
Redis持久化机制可以说一说吗?
Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后回复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。
持久化机制有两种:RDB持久化和AOF持久化
快照(snapshotting)持久化(RDB持久化)
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是Redis默认采用的持久化方式,在Redis.conf配置文件中默认有此下配置:
1 |
|
AOF(append-only file)持久化
与快照持久化相比,AOF持久化的实时性更好。默认情况下Redis没有开启
AOF(append only
file)方式的持久化,可以通过appendonly参数开启:appendonly yes
开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的 保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof。
1 |
|
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能 几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
AOF
AOF重写是Redis的一种机制,用于压缩和优化AOF文件的内容,以提高Redis的性能和稳定性。
AOF重写的原理是根据Redis进程内的数据生成一个新的AOF文件,只包含当前有效和存在的数据的写入命令。这个过程是通过创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态是相同的,但是新的AOF文件不会包含任何浪费空间的冗余命令。
该功能是通过拷贝数据库中的键值对来实现的,程序无须对现有AOF文件进行任伺读 入、分析或者写入操作。
在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容 追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的 AOF文件,以此来完成AOF文件重写操作。
是否使用过Redis集群?集群的原理是什么?
Redis Sentinel(哨兵)着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从服务器中选举出新的主服务器。
Redis Cluster(集群)着眼于扩展性,在单个Redis内存不足时,使用Cluster进行分片存储。
如何解决Redis的并发竞争Key问题
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 Redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问 题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的 与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有 序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁 无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
如何保证缓存与数据库双写时的数据一致性\(*****\)(五星高频)
- 先写数据库,再写缓存:这种方案在低并发编程中有人在用。但在高并发场景中,如果多个线程同时执行先写数据库,再写缓存的操作,可能会出现数据库是新值,而缓存中是旧值,两边数据不一致的情况。
- Cache-Aside:虽然不能100%避免数据不一致问题,但出现该问题的概率,相对于其他方案来说是最小的。在读请求中,首先请求缓存,若缓存命中(cache hit),则直接返回缓存中的数据;若缓存未命中(cache miss),则查询数据库并将查询结果更新至缓存,然后返回查询出的数据(demand-filled look-aside)。在写请求中,先更新数据库,再删除缓存(write-invalidate)。
- 延迟双删:先删除缓存,再写数据库,再次延迟删除缓存,这里延迟时间要稍微大于将数据库中旧数据写入缓存的耗时。
补偿机制
在Cache-Aside和延迟双删中都可能会出现更新数据库成功,但删除缓存失败的场景,如果发生这种情况,那么便会导致缓存中的数据落后于数据库,产生数据的不一致的问题。针对可能出现的删除失败问题,目前业界主要有以下几种补偿机制。
删除重试机制
由于同步重试删除在性能上会影响吞吐量,所以常通过引入消息队列,将删除失败的缓存对应的key放入消息队列中,在对应的消费者中获取删除失败的key,异步重试删除。这种方法在实现上相对简单,但由于删除失败后的逻辑需要基于业务代码的trigger来触发,对业务代码具有一定入侵性。
基于数据库日志(MySQL binlog)增量解析、订阅和消费
鉴于上述方案对业务代码具有一定入侵性,所以需要一种更加优雅的解决方案,让缓存删除失败的补偿机制运行在背后,尽量少的耦合于业务代码。一个简单的思路是通过后台任务使用更新时间戳或者版本作为对比获取数据库的增量数据更新至缓存中,这种方式在小规模数据的场景可以起到一定作用,但其扩展性、稳定性都有所欠缺。
一个相对成熟的方案是基于MySQL数据库增量日志进行解析和消费,这里较为流行的是阿里巴巴开源的作为MySQL binlog增量获取和解析的组件canal(类似的开源组件还有Maxwell、Databus等)。canal sever模拟MySQL slave的交互协议,伪装为MySQL slave,向MySQL master发dump协议,MySQL master收到dump请求,开始推送binary log给slave(即canal sever),canal sever解析binary log对象(原始为byte流),可由canal client拉取进行消费,同时canal server也默认支持将变更记录投递到MQ系统中,主动推送给其他系统进行消费。在ack机制的加持下,不管是推送还是拉取,都可以有效的保证数据按照预期被消费。当前版本的canal支持的MQ有kafka或者RocketMQ。另外,canal依赖zookeeper作为分布式协调组件来实现HA,canal的HA分为两个部分:
- 为了减少对MySQL dump的请求压力,不同canal server上的instance要求同一时间只能有一个处于运行状态,其他的instance处于standby状态;
- 为了保证有序性,对于一个instance在同一时间只能由一个canal client进行get/ack等动作。
那么,针对缓存的删除操作便可以在canal client或consumer中编写相关业务代码来完成。这样,结合数据库日志增量解析消费的方案以及Cache-Aside模型,在读请求中未命中缓存时更新缓存(通常这里会涉及到复杂的业务逻辑),在写请求更新数据库后删除缓存,并基于日志增量解析来补偿数据库更新时可能的缓存删除失败问题,在绝大多数场景下,可以有效的保证缓存的最终一致性。
另外需要注意的是,还应该隔离事务与缓存,确保数据库入库后再进行缓存的删除操作。比如考虑到数据库的主从架构,主从同步及读从写主的场景下,可能会造成读取到从库的旧数据后便更新了缓存,导致缓存落后于数据库的问题,这就要求对缓存的删除应该确保在数据库操作完成之后。所以,基于binlog增量日志进行数据同步的方案,可以通过选择解析从节点的binlog,来避免主从同步下删除缓存过早的问题。
数据传输服务DTS
数据传输服务(Data Transmission Service,简称DTS)是云服务商提供的一种支持RDBMS(关系型数据库)、NoSQL、OLAP等多种数据源之间进行数据交互的数据流服务。DTS提供了包括数据迁移、数据订阅、数据同步等在内的多种数据传输能力,常用于不停服数据迁移、数据异地灾备、异地多活(单元化)、跨境数据同步、实时数据仓库、查询报表分流、缓存更新、异步消息通知等多种业务应用场景。
相对于上述基于canal等开源组件自建系统,DTS的优势体现在对多种数据源的支持、对多种数据传输方式的支持,避免了部署维护的人力成本。目前,各家云服务商的DTS服务已 针对云数据库,云缓存等产品进行了适配,解决了Binlog日志回收,主备切换等场景下的订阅高可用问题。在大规模的缓存数据一致性场景下,优先推荐使用DTS服务。
数据为什么会出现不一致的情况?
这样的问题主要是在并发读写访问的时候,缓存和数据相互交叉执行。以先删缓存,再写数据库为例
单库情况下
同一时刻发生了并发读写请求,例如为A(写) B (读)2个请求
- A请求发送一个写操作到服务端,第一步会淘汰cache,然后因为各种原因卡主了,不在执行后面业务(例:大量的业务操作、调用其他服务处理消耗了1s)。
- B请求发送一个读操作,读cache,因为cache淘汰,所以为空。
- B请求继续读DB,读出一个脏数据,并写入cache。
- A请求终于执行完全,在写入数据到DB。
总结:因最后才把写操作数据入DB,并没同步。cache里面一直保持脏数据
脏数据是指源系统中的数据不在给定的范围内或对于实际业务毫无意义,或是数据格式非法,以及在源系统中存在不规范的编码和含糊的业务逻辑。
主从同步,读写分离的情况下,读从库而产生脏数据
- A请求发送一个写操作到服务端,第一步会淘汰cache
- A请求写主数据库,写了最新的数据。
- B请求发送一个读操作,读cache,因为cache淘汰,所以为空
- B请求继续读DB,读的是从库,此时主从同步还没同步成功。读出脏数据,然后脏数据入cache
- 最后数据库主从同步完成
总结:这种情况下请求A和请求B操作时序没问题,是主从同步的时延问题(假设1s),导致读请求读取从库读到脏数据导致的数据不一致
根本原因:
单库下,逻辑处理中消耗1s,可能读到旧数据入缓存
主从+读写分离,在1s的主从同步时延中,到从库的旧数据入缓存
常见的数据优化方案你了解吗?
延迟双删
- 先删除缓存
- 再写数据库
- 往消息总线esb发送一个删除消息,发送立即返回。写请求的处理时间几乎没有增加,这个方法删除了缓存两次。因此被称为“延迟双删“,而在消息总线下游,有一个异步淘汰删除的消费者,在拿到删除消息延迟一定时间后删除缓存,这样,即使在一秒内有脏数据入缓存,也能够被淘汰掉。注意这里的延迟时间要略大于数据库将旧数据写入缓存的时间.
异步淘汰缓存
上述的步骤,都是在业务线里面执行,新增一个线下的读取binlog异步淘汰缓存模块,读取binlog总的数据,然后进行异步淘汰。
这里简单提供一个思路
1.思路:
MySQL binlog增量发布订阅消费+消息队列+增量数据更新到Redis
1)读请求走Redis:热数据基本都在Redis
2)写请求走MySQL: 增删改都操作MySQL
3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis
2.Redis更新
1)数据操作主要分为两块:
- 一个是全量(将全部数据一次写入到Redis)
- 一个是增量(实时更新)
这里说的是增量,指的是mysql的update、insert、delete变更数据。这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新,就无需在从业务线去操作缓存内容。
Redis 的高并发和高可用是如何保证的?
这样的问题主要是在并发读写访问的时候,缓存和数据相互交叉执行。
Redis的主从架构模式
Redis的主从架构模式是实现高并发的主要依赖,一般很多项目只需要一主多从就可以实现其所需要的功能。通常使用单个master用来写入数据,单机几万 QPS;多个slave一般是查询数据,多个从实例可以提供每秒 10w 的 QPS。
一些项目需要在实现高并发的同时,尽可能多的容纳大量的数据,这时需要使用Redis 集群,使用Redis 集群之后,可以提供每秒几十万的读写并发。
Redis 高可用,如果是做主从架构部署,那么加上哨兵就可以实现,任何一个实例宕机,可以进行主备切换。
单机的 Redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。
Redis replication -> 主从架构 -> 读写分离 -> 水平扩容支撑读高并发
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
快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。
RDB
文件生成完毕后, master 会将这个 RDB
发送给 slave,slave
会先写入本地磁盘,然后再从本地磁盘加载到内存中,接着
master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。slave
node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后
master node 仅会复制给 slave 部分缺少的数据。
主从复制的断点续传
从 Redis2.8 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
master node 会在内存中维护一个 backlog,master 和 slave 都会保存一个
replica offset 还有一个 master run id,offset 就是保存在 backlog
中的。如果 master 和 slave 网络连接断掉了,slave 会让 master 从上次
replica offset 开始继续复制,如果没有找到对应的 offset,那么就会执行一次
resynchronization
。
note
如果根据 host+ip 定位 master node,是不靠谱的,如果 master node 重启或者数据出现了变化,那么 slave node 应该根据不同的 run id 区分。
无磁盘化复制
master 在内存中直接创建 RDB
,然后发送给
slave,不会在自己本地落地磁盘了。只需要在配置文件中开启
repl-diskless-sync yes
即可。
1 |
|
过期 key 处理
slave 没有过期 key,只会等待 master 过期 key。如果 master 过期了一个 key,或者通过 LRU 淘汰了一个 key,那么会模拟一条 del 命令发送给 slave。
复制的完整流程
slave node 启动时,会在自己本地保存 master node 的信息,包括 master
node 的 host
和 ip
,但是复制流程没开始。
slave node 内部有个定时任务,每秒检查是否有新的 master node
要连接和复制,如果发现,就跟 master node 建立 socket 网络连接。然后
slave node 发送 ping
命令给 master node。如果 master 设置了
requirepass,那么 slave node 必须发送 masterauth
的口令过去进行认证。master node
第一次执行全量复制,将所有数据发给 slave
node。而在后续,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。
- 如果在复制期间,内存缓冲区持续消耗超过 64MB,或者一次性超过 256MB,那么停止复制,复制失败。
1 |
|
- slave node 接收到 rdb 之后,清空自己的旧数据,然后重新加载 rdb 到自己的内存中,同时基于旧的数据版本对外提供服务。
- 如果 slave node 开启了 AOF,那么会立即执行 BGREWRITEAOF,重写 AOF。
增量复制
- 如果全量复制过程中,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 如何才能做到高可用?
如果系统在 365 天内,有 99.99% 的时间,都是可以哗哗对外提供服务的,那么就说系统是高可用的。
一个 slave 挂掉了,是不会影响可用性的,还有其它的 slave 在提供相同数据下的相同的对外的查询服务。
但是,如果 master node 死掉了,会怎么样?没法写数据了,写缓存的时候,全部失效了。slave node 还有什么用呢,没有 master 给它们复制数据了,系统相当于不可用了。
Redis 的高可用架构,叫做 failover
故障转移,也可以叫做主备切换。
master node 在故障时,自动检测,并且将某个 slave node 自动切换为 master node 的过程,叫做主备切换。这个过程,实现了 Redis 的主从架构下的高可用。
Redis基于哨兵集群实现高可用?
哨兵的定义
sentinel,中文名是哨兵。哨兵是 Redis 集群架构中非常重要的一个组件,主要有以下功能:
- 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
- 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
- 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
- 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
哨兵用于实现 Redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
- 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。
哨兵的核心知识
- 哨兵至少需要 3 个实例,来保证自己的健壮性。
- 哨兵 + Redis 主从的部署架构,是不保证数据零丢失的,只能保证 Redis 集群的高可用性。
- 对于哨兵 + Redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
哨兵集群必须部署 2 个以上节点,如果哨兵集群仅仅部署了 2 个哨兵实例,quorum = 1。
info
主从切换需要至少quorum数量的哨兵认为主机宕机,才会认为这个主机真正宕机了,然后需要选举出一个哨兵代表去执行主从切换。选举出这个哨兵的时候需要至少majority个哨兵的支持。
1 |
|
配置 quorum=1
,如果 master 宕机, s1 和 s2 中只要有 1
个哨兵认为 master 宕机了,就可以进行切换,同时 s1 和 s2
会选举出一个哨兵来执行故障转移。但是同时这个时候,需要
majority,也就是大多数哨兵都是运行的。
1 |
|
如果此时仅仅是 M1 进程宕机了,哨兵 s1 正常运行,那么故障转移是 OK 的。但是如果是整个 M1 和 S1 运行的机器宕机了,那么哨兵只有 1 个,此时就没有 majority 来允许执行故障转移,虽然另外一台机器上还有一个 R1,但是故障转移不会执行。
经典的 3 节点哨兵集群是这样的:
1 |
|
配置 quorum=2
,如果 M1
所在机器宕机了,那么三个哨兵还剩下 2 个,S2 和 S3 可以一致认为 master
宕机了,然后选举出一个来执行故障转移,同时 3 个哨兵的 majority 是
2,所以还剩下的 2 个哨兵运行着,就可以允许执行故障转移。
Redis 哨兵主备切换的数据丢失问题
导致数据丢失的两种情况
主备切换的过程,可能会导致数据丢失:
异步复制导致的数据丢失
因为 master->slave 的复制是异步的,所以可能有部分数据还没复制到 slave,master 就宕机了,此时这部分数据就丢失了。
脑裂导致的数据丢失
脑裂指的是某个 master 所在机器突然脱离了正常的网络,跟其他 slave 机器不能连接,但是实际上 master 还运行着。此时哨兵可能就会认为 master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时候,集群里就会有两个 master。
此时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 写数据。因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,重新从新的 master 复制数据。而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。
### 数据丢失的解决方案
进行如下配置:
1
2min-slaves-to-write 1
min-slaves-max-lag 10min-slaves-to-write默认情况下是0,min-slaves-max-lag默认情况下是10。这两个参数表示至少有1个salve数据复制与同步的延迟不能超过10s,一旦所有的slave复制和同步的延迟达到了10s,那么此时master就不会接受任何写请求。
这样做的优点有:
- 减少异步复制数据的丢失:有了
min-slaves-max-lag
这个配置,就可以确保说,一旦 slave 复制数据和 ack 延时太长,就认为可能 master 宕机后损失的数据太多了,那么就拒绝写请求,这样可以把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低的可控范围内。 - 减少脑裂的数据丢失:如果一个 master 出现了脑裂,跟其他 slave 丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的 slave 发送数据,而且 slave 超过 10 秒没有给自己 ack 消息,那么就直接拒绝客户端的写请求。因此在脑裂场景下,最多就丢失 10 秒的数据。
- 减少异步复制数据的丢失:有了
sdown 和 odown 转换机制
S_DOWN:主观宕机,就一个哨兵如果自己觉得一个 master 宕机了,那么就是主观宕机。
O_DOWN:客观宕机,如果 quorum 数量的哨兵都觉得一个 master 宕机了,那么就是客观宕机。
S_DOWN达成的条件很简单,如果一个哨兵 ping 一个 master,超过了
is-master-down-after-milliseconds
指定的毫秒数之后,就主观认为 master
宕机了;如果一个哨兵在指定时间内,收到了 quorum 数量的其它哨兵也认为那个
master 是 sdown 的,那么就认为是 odown 了。
哨兵集群的自动发现机制
哨兵互相之间的发现,是通过 Redis 的 pub/sub
系统实现的,每个哨兵都会往 __sentinel__:hello
这个 channel
里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。
每隔两秒钟,每个哨兵都会往自己监控的某个 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 被认为 O_DOWN了,而且 majority 数量的哨兵都允许主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个 slave 来,会考虑 slave 的一些信息:
- 跟 master 断开连接的时长
- slave 优先级
- 复制 offset
- run id
如果一个 slave 跟 master 断开连接的时间已经超过了
down-after-milliseconds
的 10 倍,外加 master
宕机的时长,那么 slave 就被认为不适合选举为 master。
1 |
|
接下来会对 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 配置的。
Redis集群模式的工作原理是什么?
基本通信原理
集群元数据的维护有两种方式:集中式、Gossip 协议。Redis cluster 节点间采用 gossip 协议进行通信。
集中式是将集群元数据(节点信息、故障等等)集中存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的
storm
。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于
zookeeper(分布式协调的中间件)对所有元数据进行存储维护。
Redis 维护集群元数据采用另一个方式, gossip
协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。
集中式的好处在于,元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到;不好在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。
gossip 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;不好在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。
- 10000
端口:每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如
7001,那么用于节点间通信的就是 17001
端口。每个节点每隔一段时间都会往另外几个节点发送
ping
消息,同时其它几个节点接收到ping
之后返回pong
。 - 交换的信息:信息包括故障信息,节点的增加和删除,hash slot 信息等等。
gossip 协议
gossip 协议包含多种消息,包含 ping
, pong
,
meet
, fail
等等。
- meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。
1 |
|
其实内部就是发送了一个 gossip meet 消息给新加入的节点,通知那个节点去加入我们的集群。
- ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。
- pong:返回 ping 和 meet,包含自己的状态和其它信息,也用于信息广播和更新。
- fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。
ping 消息深入
ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。
每个节点每秒会执行 10 次 ping,每次会选择 5
个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了
cluster_node_timeout / 2
,那么立即发送
ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10
分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以
cluster_node_timeout
可以调节,如果调得比较大,那么会降低
ping 的频率。
每次 ping,会带上自己节点的信息,还有就是带上 1/10
其它节点的信息,发送出去,进行交换。至少包含 3
个其它节点的信息,最多包含 总节点数减 2
个其它节点的信息。
分布式寻址算法
- hash 算法(大量缓存重建)
- 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
- Redis cluster 的 hash slot 算法
hash 算法
来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致大部分的请求过来,全部无法拿到有效的缓存,导致大量的流量涌入数据库。
一致性 hash 算法
一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。
来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。
在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。
燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个master节点计算多个 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,不是机器。
Redis cluster 的高可用与主备切换原理
Redis cluster 的高可用的原理,几乎跟哨兵是类似的。
判断节点宕机
如果一个节点认为另外一个节点宕机,那么就是 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 cluster 功能强大,直接集成了 replication 和 sentinel 的功能。
Redis 的并发竞争问题是什么?如何解决这个问题?了解 Redis 事务的 CAS 方案吗?
这个也是线上非常常见的一个问题,就是多客户端同时并发写一个 key,可能本来应该先到的数据后到了,导致数据版本错了;或者是多客户端同时获取一个 key,修改值之后再写回去,只要顺序错了,数据就错了。
而且 Redis 自己就有天然解决这个问题的 CAS 类的乐观锁方案。
某个时刻,多个系统实例都去更新某个 key。可以基于 zookeeper 实现分布式锁。每个系统通过 zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 key,别人都不允许读和写。
你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。
每次要写之前,先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。
生产环境中的 Redis 是怎么部署的?
看看你了解不了解你们公司的 Redis 生产集群的部署架构,如果你不了解,那么确实你就很失职了,你的 Redis 是主从架构?集群架构?用了哪种集群方案?有没有做高可用保证?有没有开启持久化机制确保可以进行数据恢复?线上 Redis 给几个 G 的内存?设置了哪些参数?压测后你们 Redis 集群承载多少 QPS?
Redis cluster,10 台机器,5 台机器部署了 Redis 主实例,另外 5 台机器部署了 Redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰 QPS 可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求每秒。
机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 Redis 进程的是 10g 内存,一般线上生产环境,Redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。
5 台机器对外提供读写,一共有 50g 内存。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,Redis 从实例会自动变成主实例继续提供读写服务。
你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。
其实大型的公司,会有基础架构的 team 负责缓存集群的运维。
redis三个缓冲区
客户端缓冲区
客户端缓冲区又有两个,输入缓冲区和输出缓冲区,都是为了解决客户端和服务器端的请求发送和处理速度不匹配所设置的。
输入缓冲区会先暂存客户端发送过来的命令,Redis 主线程从输入缓冲区中读取命令,进行处理。当 Redis 主线程处理完数据后,会把结果写入到输出缓冲区,再从输出缓冲区返回给客户端。
输出缓冲区暂存的是 Redis 主线程要返回给客户端的数据。这个数据,既有简单且大小固定的 OK 响应(例如,执行 SET 命令)或报错信息,也有大小不固定的、包含具体数据的执行结果(例如,执行 HGET 命令)
复制缓冲区
复制缓冲区是用于Redis主从节点之间复制时使用的。由于主从节点间的数据复制包括全量复制和增量复制两种。因此复制缓冲区也分为复制缓冲区和复制积压缓冲区两种。
复制缓冲区
在全量复制过程中,主节点在向从节点传输 RDB 文件的同时,会继续接收客户端发送的写命令请求。这些写命令就会先保存在复制缓冲区中,等 RDB 文件传输完成后,再发送给从节点去执行。主节点上会为每个从节点都维护一个复制缓冲区,来保证主从节点间的数据同步。
复制积压缓冲区
增量复制时,主节点和从节点进行常规同步时,会把写命令也暂存在复制积压缓冲区中。如果从节点和主节点间发生了网络断连,等从节点再次连接后,可以从复制积压缓冲区中同步尚未复制的命令操作。
AOF缓冲区
AOF缓冲区是Redis在AOF持久化的所设置的缓冲区,AOF缓冲区也有两种AOF缓冲区和AOF重写缓冲区。
AOF缓冲区
我们都知道,即使是固态硬盘,它的读写速度也是与内存的读写速度相差很多的。AOF缓冲区就主要是Redis用来解决主进程执行命令速度与磁盘写入速度不同步所设置的,通过AOF缓冲区可以有效地避免频繁对硬盘进行读写,进而提升性能。Redis在AOF持久化的时候,会先把命令写入到AOF缓冲区,然后通过回写策略来写入硬盘AOF文件。
AOF缓冲区的溢出可能与磁盘写入速度有关系,也可能与AOF回写策略有关系,当大量命令积压在AOF缓冲区,超过其设置阈值之后,就会导致缓冲区溢出,想要避免这个问题,我们可以通过调整回写策略,或者调整AOF缓冲区大小的方式来解决。
AOF重写缓冲区
AOF重写缓冲区是Redis在子进程进行AOF重写的时候,父进程接受了新的命令,此时会把命令写入AOF重写缓冲区,等到子进程重写完成后,把AOF重写缓冲区命令追加到新的AOF文件中。
AOF重写缓冲区的溢出与AOF重写期间主进程所处理的命令数有关系,当AOF重写期间Redis主进程处理了大量的命令,这些命令都会写入AOF重写缓冲区,当超过设定阈值之后,就会导致溢出。避免AOF重写缓冲区的溢出我们也可以通过调整AOF重写缓冲区的大小来解决。
redis内存管理机制
内存最大限制
Redis使用 maxmemory 参数限制最大可用内存,默认值为0,表示无限制。限制内存的目的主要有:
- 用于缓存场景,当超出内存上限 maxmemory 时使用 LRU 等删除策略释放空间。
- 防止所用内存超过服务器物理内存。因为 Redis 默认情况下是会尽可能多使用服务器的内存,可能会出现服务器内存不足,导致 Redis 进程被杀死。
maxmemory 限制的是Redis实际使用的内存量,也就是 used_memory统计项对应的内存。由于内存碎片率的存在,实际消耗的内存 可能会比maxmemory设置的更大,实际使用时要小心这部分内存溢出。
内存回收策略
Redis 回收内存大致有两个机制:一是删除到达过期时间的键值对象;二是当内存达到 maxmemory 时触发内存移除控制策略,强制删除选择出来的键值对象。
删除过期键对象
当 Redis保存大量的键,对每个键都进行精准的过期删除可能会导致消耗大量的 CPU,会阻塞 Redis 的主线程,拖累 Redis 的性能,因此 Redis 采用惰性删除和定时任务删除机制实现过期键的内存回收。
惰性删除
惰性删除是指当客户端操作带有超时属性的键时,会检查是否超过键的过期时间,然后会同步或者异步执行删除操作并返回键已经过期。这样可以节省 CPU成本考虑,不需要单独维护过期时间链表来处理过期键的删除。
过期键的惰性删除策略由 db.c/expireifNeeded 函数实现,所有对数据库的读写命令执行之前都会调用 expireifNeeded 来检查命令执行的键是否过期。如果键过期,expireifNeeded 会将过期键从键值表和过期表中删除,然后同步或者异步释放对应对象的空间。
expireIfNeeded 先从过期表中获取键对应的过期时间,如果当前时间已经超过了过期时间(lua脚本执行则有特殊逻辑,详看代码注释),则进入删除键流程。删除键流程主要进行了三件事:
- 一是删除操作命令传播,通知 slave 实例并存储到 AOF 缓冲区中
- 二是记录键空间事件,
- 三是根据 lazyfree_lazy_expire 是否开启进行异步删除或者异步删除操作。
定期删除
定期删除策略由 expire.c/activeExpireCycle 函数实现。在redis事件驱动的循环中的eventLoop->beforesleep和
周期性操作 databasesCron 都会调用 activeExpireCycle 来处理过期键。但是二者传入的 type 值不同,一个是ACTIVE_EXPIRE_CYCLE_SLOW 另外一个是ACTIVE_EXPIRE_CYCLE_FAST。activeExpireCycle 在规定的时间,分多次遍历各个数据库,从 expires 字典中随机检查一部分过期键的过期时间,删除其中的过期键。
内存溢出控制策略
当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。 具体策略受maxmemory-policy参数控制,Redis支持6种策略:
- noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此 时Redis只响应读操作。
- volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
- allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性, 直到腾出足够空间为止。
- allkeys-random:随机删除所有键,直到腾出足够空间为止。
- volatile-random:随机删除过期键,直到腾出足够空间为止。
- volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
redis怎么实现限流
主流的限流算法为以下四种:
- 计数器(固定窗口)
- 滑动窗口(分割计数器)
- 漏桶算法
- 令牌桶算法
计数器法
计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。
此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松实现。
这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在一个非常严重的问题,那就是临界问题,如下图:
假设1min内服务器的负载能力为100,因此一个周期的访问量限制在100,然而在第一个周期的最后5秒和下一个周期的开始5秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制量,但是整体上10秒内已达到200的访问量,已远远超过服务器的负载能力,由此可见,计数器算法方式限流对于周期比较长的限流,存在很大的弊端。
滑动窗口算法
滑动窗口算法是将时间周期分为N个小周期,分别记录每个小周期内访问次数,并且根据时间滑动删除过期的小周期。
如下图,假设时间周期为1min,将1min再分为2个小周期,统计每个小周期的访问数量,则可以看到,第一个时间周期内,访问数量为75,第二个时间周期内,访问数量为100,超过100的访问则被限流掉了
由此可见,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
此算法可以很好的解决固定窗口算法的临界问题。
漏桶算法
漏桶算法是访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。
令牌桶算法
令牌桶算法是程序以r(r=时间周期/限流值)的速度向令牌桶中增加令牌,直到令牌桶满,请求到达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略