发布于 

7.Redis

一、Redis基础

1.1 Redis有哪些优缺点?

1.1.1 优点

  • 读写性能优异
  • 支持数据持久化,支持AOF和RDB两种持久化方式。
  • 支持事务,Redis的所有操作都是原子性的,同时还支持对几个操作合并后的原子性执行。
  • 数据结构丰富,除了支持String类型的value外,还支持Hash、Set、ZSet、List等数据机构。
  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。

1.1.2 缺点

  • 数据库容量受到物理内存限制,不能用作海量数据的高性能读写,只适用于较小数据量的高性能操作和运算上。
  • Redis不具备自动容错和恢复功能
  • 无法支持复杂查询。
  • 数据容易丢失。
  • Redis较难支持在线扩容。

1.2 Redis为什么这么快?

  1. Redis是基于内存的,内存的访问速度是磁盘的上千倍;
  2. Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环IO 多路复用
  3. Redis 内置了多种优化过后的数据结构实现,性能非常高。

1.3 为什么要用Redis?(为什么要用缓存?)

  1. 高性能

    假如用户第一次访问数据库中的某些数据的话,是从硬盘中读取的,这个过程是比较慢。但是,如果用户访问的数据属于高频数据并且不会经常改变的话,可以将该用户访问的数据存在缓存中。这样保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。

  2. 高并发

    直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存而不用经过数据库。进而,也就提高了系统整体的并发。

1.4 说一说Redis和Memcached的区别和共同点?

1.4.1 共同点

  1. 都是基于内存的数据库,一般都用来当做缓存使用。
  2. 都有过期策略。
  3. 两者性能都非常高。

1.4.2 区别

  1. 数据类型支持不同, Memcached 仅支持简单的 key-value 结构的数据记录,Redis 支持的数据类型要丰富得多。最为常用的数据类型主要有五种:String、Hash、List、Set 和 Sorted Set。
  2. 内存管理机制不同,在 Redis 中,并不是所有的数据都一直存储在内存中的。这是和 Memcached 相比一个最大的区别。当物理内存用完时,Redis 可以将一些很久没用到的 value 交换到磁盘。
  3. 数据持久化支持不同,Redis 虽然是基于内存的存储系统,但是它本身是支持内存数据的持久化的,而且提供两种主要的持久化策略:RDB 快照和 AOF 日志。而 memcached 是不支持数据持久化操作的。
  4. 集群的管理不同,Memcached 本身并不支持分布式,因此只能在客户端通过像一致性哈希这样的分布式算法来实现 Memcached 的分布式存储。相较于 Memcached 只能采用客户端实现分布式存储,Redis 更偏向于在服务器端构建分布式存储。

1.5 说一说Redis中常用的缓存读写策略?

1.5.1 旁路缓存模式

适用于请求比较多的场景。

旁路缓存模式中服务端需要同时维系DB和Cache,并且以DB的结果为主。

写:

  • 先更新DB;
  • 然后直接删除Cache。
先更新DB再删除缓存
先更新DB再删除缓存

写:

  • 从Cache中读取数据,读取到就直接返回;
  • Cache中读取不到的话,就从DB中读取数据返回;
  • 再把数据放到Cache中。
写

在写数据的过程中,可以先删除Cache,然后再更新DB吗?

不行!因为这样可能会造成DB和Cache中的数据不一致的问题。

比如,请求1先把Cache中的A数据删除 -> 请求2从DB中读取数据 -> 请求1再把DB中的A数据更新。

在写数据的过程中,可以先更新DB,然后再删除Cache吗?

理论上来说还是有可能出现数据不一致性的问题,不过概率比较小,因为Cache的写入速度比DB写入速度快很多。

比如,请求1从DB中读数据A -> 请求2更新DB中的数据A -> 请求1将数据A写入Cache。

旁路缓存模式的缺点:

  1. 首次请求的数据一定不在Cache中。

    解决方法:可以将热点数据提前放入Cache中。

  2. 写操作比较频繁的话会导致Cache中的数据会被频繁删除,这样会影响缓存的命中率。

    解决方法:

    • DB和Cache数据一致的情况下,同步更新DB和Cache,但需要加锁保证更新Cache时不存在线程安全问题。
    • DB和Cache数据不一致的情况下,同步更新DB和Cache,但是给Cache加一个比较短的过期时间。

1.5.2 读写穿透模式

该模式中服务端把Cache视为主要数据存储,从中读取数据并将数据写入其中,Cache负责将数据读取和写入DB。

写穿透:

  • 先查Cache,Cache中不存在,直接更新DB;
  • Cache中存在,则先更新Cache,然后Cache服务自己更新DB(同步更新Cache和DB)。
写穿透
写穿透

读穿透:

  • 从Cache中读取数据,读取到就直接返回;
  • 读取不到的话,先从DB加载,写入到Cache后返回响应。
读穿透
读穿透

1.5.3 异步缓存写入模式

与读写穿透模式类似,都是由Cache服务来负责Cache和DB的读写。

但是,读写穿透模式是同步更新Cache和DB,而异步缓存写入模式只是更新Cache,不直接更新DB,通过异步批量的方式来更新DB。

二、Redis数据结构

2.1 Redis常用的数据结构有哪些?

  • 5种基本数据结构:String(字符串)、List(列表)、Hash(散列)、Set(集合)、Zset(有序集合)。
  • 3种特殊数据结构:HyperLogLogs(基数统计)、Bitmap(位存储)、Geospatial(地理位置)。

2.2 String的应用场景有哪些?

String是一种二进制安全的数据结构,可以用来存储任何类型的数据,比如字符串、整数、浮点数、图片(图片的base64编码或解码或图片的路径)、序列化后的对象。

  • 常规数据的缓存,比如session、token、序列化后的对象、图片的编码或路径等。
  • 计数,比如用户单位时间的请求数、页面单位时间的访问数等。
  • 分布式锁

2.3 存储对象数据使用String还是Hash更好?

  • String存储的是序列化后的对象数据、存储的是整个对象。Hash是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或添加部分字段。如果对象中的某些字段需要经常变动或需要单独查询对象中某些字段信息,就更适合用Hash。
  • String存储相对来说更节省内存,缓存相同数量的对象数据,String消耗的内存约是Hash的一半。并且存储具有多层嵌套的对象时也方便很多。

总结:绝大多数情况下,更建议使用String来存储对象数据。

2.4 Redis数据结构的底层实现

2.4.1 String的底层实现了解吗?

Redis虽然是基于C语言编写的,但是Redis中的String类型的底层并不是C语言中的字符串,而是作者自己编写了SDS(简单动态字符串)作为底层实现的。

SDS相比于C语言的字符串有以下提升:

  1. 可以避免缓冲区溢出:C 语言中的字符串被修改(比如拼接)时,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。SDS 被修改时,会先根据 len 属性检查空间大小是否满足要求,如果不满足,则先扩展至所需大小再进行修改操作。
  2. 获取字符串长度的复杂度较低: C 语言中的字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。SDS 的长度获取直接读取 len 属性即可,时间复杂度为 O(1)。
  3. 减少内存分配次数:为了避免修改(增加/减少)字符串时,每次都需要重新分配内存(C 语言的字符串是这样的),SDS 实现了空间预分配和惰性空间释放两种优化策略。当 SDS 需要增加字符串时,Redis 会为 SDS 分配好内存,并且根据特定的算法分配多余的内存,这样可以减少连续执行字符串增长操作所需的内存重分配次数。当 SDS 需要减少字符串时,这部分内存不会立即被回收,会被记录下来,等待后续使用(支持手动释放,有对应的 API)。
  4. 二进制安全:C 语言中的字符串以空字符 \0 作为字符串结束的标识,这存在一些问题,像一些二进制文件(比如图片、视频、音频)就可能包括空字符,C 字符串无法正确保存。SDS 使用 len 属性判断字符串是否结束,不存在这个问题。

2.4.2 List的底层实现了解吗?

List 类型的底层数据结构是由压缩列表或双向链表实现的:

  • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
  • 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;

在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表

2.4.3 Hash的底层实现了解吗?

Hash 类型的底层数据结构是由压缩列表或哈希表实现的:

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的底层数据结构。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

Redis中的Hash也是采用链地址法解决hash冲突,但是并没有采用红黑树。其底层定义了2个Hash表,目的就是rehash。

在正常服务请求阶段,插入的数据,都会写入到「哈希表 1」,此时的「哈希表 2 」 并没有被分配空间。

随着数据逐步增多,触发了 rehash 操作,这个过程分为三步:

  • 给「哈希表 2」 分配空间,一般会比「哈希表 1」 大 2 倍;
  • 将「哈希表 1 」的数据迁移到「哈希表 2」 中;
  • 迁移完成后,「哈希表 1 」的空间会被释放,并把「哈希表 2」 设置为「哈希表 1」,然后在「哈希表 2」 新创建一个空白的哈希表,为下次 rehash 做准备。

渐进式rehash

为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时,影响 Redis 性能的情况,所以 Redis 采用了渐进式 rehash,也就是将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。

渐进式 rehash 步骤如下:

  • 给「哈希表 2」 分配空间;
  • 在 rehash 进行期间,每次哈希表元素正常进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上
  • 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。

rehash触发的条件是什么?

rehash触发的条件与负载因子有关,负载因子 = hash表中已保存元素的数量 / hash表的大小

  • 当负载因子 >= 1,并且Redis没有执行RDB快照或AOF重写时,就会进行rehash操作;
  • 当负载因子 >= 5时,说明此时hash冲突已经非常严重了,不论此时有没有在执行RDB快照和重写AOF,都会强制进行rehash操作;

2.4.4 Set的底层实现了解吗?

Set 类型的底层数据结构是由整数集合或哈希表实现的:

  • 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
  • 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。

2.4.5 ZSet的底层实现了解吗?

Zset 类型的底层数据结构是由压缩列表或跳表实现的:

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

2.5 压缩列表和跳表的实现原理了解吗?

2.5.1 压缩列表的原理

1、压缩列表的优缺点

优点:

  • 是一种内存紧凑型的数据结构,占用一块连续的内存空间;
  • 能充分利用CPU缓存,节省内存开销;

缺点:

  • 不能保存过多的元素,否则查询效率就会降低;
  • 新增或修改元素时,内存空间需要重新分配,可能会引发连锁更新的问题;

2、压缩列表结构

压缩列表结构
压缩列表结构

压缩列表在表头有三个字段:

  • zlbytes,记录整个压缩列表占用内存字节数;
  • zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
  • zllen,记录压缩列表包含的节点数量;
  • zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。

所以在压缩列表中查找第一个或最后一个元素,可以直接通过表头的三个字段直接定位,时间复杂度是O(1)。

在查找其他元素时,就需要遍历查找,时间复杂度是O(n),因此压缩列表不适合保存过多的元素。

3、压缩列表节点结构

压缩列表节点结构

压缩列表节点包含三部分内容:

  • prevlen,记录了「前一个节点」的长度,目的是为了实现从后向前遍历;
  • encoding,记录了当前节点实际数据的「类型和长度」,类型主要有两种:字符串和整数。
  • data,记录了当前节点的实际数据,类型和长度都由 encoding 决定;

压缩列表之所以能节省内存空间,是因为会通过prevlenencoding两个元素中的信息根据数据大小和类型进行不同的空间分配方法

4、压缩列表中的连锁更新问题了解吗?

因为压缩列表占用的是一整块连续的内存空间,所以在新增或修改元素时,如果空间不够,就会导致后续整个压缩链表元素所占用的空间都会发生变化,每个元素的空间都需要重新分配,从而引起连锁更新问题。

2.5.2跳表的原理

链表在查找元素的时候,查询效率非常低,时间复杂度是O(N)。

跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,能快速定位数据,支持平均 O(logN) 复杂度的节点查找

1、跳表的结构

跳表结构
跳表结构

跳表就能让链表拥有近乎的接近二分查找的效率的一种数据结构,其原理依然是给上面加若干层索引,优化查找速度。

1
2
3
4
5
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;

跳表结构里包含了:

  • 跳表的头尾节点,便于在O(1)时间复杂度内访问跳表的头节点和尾节点;
  • 跳表的长度,便于在O(1)时间复杂度获取跳表节点的数量;
  • 跳表的最大层数,便于在O(1)时间复杂度获取跳表中层高最大的那个节点的层数量;

跳表节点的结构里包含了:

  • 元素值
  • 元素权重值
  • 后向指针:指向前一个节点
  • level数组:用来保存每层的前向指针和跨度

2、跳表的查询过程

跳表查询过程
跳表查询过程
  • 跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断,共有两个判断条件:

    • 如果当前节点的权重 < 要查找的权重时,跳表就会访问该层上的下一个节点。

    • 如果当前节点的权重 = 要查找的权重时,并且当前节点的 SDS 类型数据「小于」要查找的数据时,跳表就会访问该层上的下一个节点。

  • 如果上面两个条件都不满足,或者下一个节点为空时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针;

  • 然后沿着下一层指针继续上述步骤查找,这就相当于跳到了下一层接着查找。

3、跳表层数设置

**跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)**。

跳表在创建节点的时候,会生成一个随机数,如果随机数 < 0.25,层数就加一层,然后继续生成随机数,知道随机数的结果大于0.25,就最终确定该节点的层数。

这种做法相当于每增加一层的概率不超过25%,层数越高,概率越小。

Redis可以通过ZSKIPLIST_MAXLEVEL 定义最大层高数。

2.5.3 为什么ZSet用跳表,不用平衡树或红黑树?

  • 从内存占用上来比较,跳表比平衡树更灵活一些。平衡树每个节点包含 2 个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为 1/(1-p),具体取决于参数 p 的大小。如果像 Redis里的实现一样,取 p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
  • 在做范围查找的时候,跳表比平衡树操作要简单。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历就可以实现。
  • 从算法实现难度上来比较,跳表比平衡树要简单得多。平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。

2.5.4 跳表各个操作的时间复杂度是多少?

  • 查找操作:时间复杂度是O(logN);
  • 插入操作:每层索引中插入的复杂度是O(1),所以整个插入的时间复杂度是O(logN);
  • 删除操作:删除操作包含两个步骤,分别是查找删除,所以删除的时间复杂度是2O(logN),忽略常熟部分,也就是O(logN);

2.6 购物车信息是用String还是Hash存储更好?

上面2.3中分析过了,由于购物车中的商品频繁需要修改和变动,所以购物车信息建议使用Hash存储:

  • 用户ID为Key
  • 商品ID为field,商品数量为Value
Hash维护简单的购物车信息
Hash维护简单的购物车信息

2.6.1 用户购物车信息的维护具体应该怎么操作?

  1. 用户添加商品就是往Hash里面添加field和value;
  2. 查询购物车信息就是遍历Hash;
  3. 更改商品数量就修改对应的value值;
  4. 删除商品就是删除Hash中对应的field;
  5. 清空购物车就直接删除对应Key;

2.7 用Redis实现一个排行榜怎么做?

可以用到Redis中的Zset数据结构,可以实现各种排行榜,比如送礼物排行榜、微信步数排行榜、段位排行榜等等。

其中一些常见的Redis命令:ZRANGE(从小到大排序)、ZREVRANGE(从大到小排序)、ZREVRANK(指定元素排序)。

2.8 Set的应用场景是什么?

Redis 中 Set 是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 HashSet

  • 存放数据不能重复的场景。比如文章点赞、动态点赞等等。
  • 需要获取多个数据源交集、并集和差集的场景。比如共同好友、共同粉丝、共同关注、好友推荐等等。
  • 需要随机获取数据源中的元素。比如抽奖系统、随机点名等等。

2.9 如何用Set实现抽奖系统?

  • SADD key member1 member2 ...:向指定集合添加一个或多个元素。
  • SPOP key count:随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的情况。
  • SRANDMEMBER key count:随机获取指定集合中指定数量的元素,适合运行重复中奖的情况。

2.10 如何用Bitmap统计活跃用户?

Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。

可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。

如果想要使用 Bitmap 统计活跃用户的话,可以使用日期(精确到天)作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1。

初始化数据:

1
2
3
4
5
6
SETBIT 20210308 1 1
(integer) 0
SETBIT 20210308 2 1
(integer) 0
SETBIT 20210309 1 1
(integer) 0

统计 20210308~20210309 总活跃用户数:

1
2
3
4
BITOP and desk1 20210308 20210309
(integer) 1
BITCOUNT desk1
(integer) 1

统计 20210308~20210309 在线活跃用户数:

1
2
3
4
BITOP or desk2 20210308 20210309
(integer) 1
BITCOUNT desk2
(integer) 2

三、Redis持久化机制

3.1 说一说Redis支持的持久化机制?

  • 快照(RDB)
  • 只追加文件(AOF)
  • RDB和AOF的混合持久化(Redis 4.0新增)

3.2 RDB持久化

3.2.1 什么是RDB持久化?

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

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

快照持久化是 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命令创建快照。

3.2.2 RDB创建快照时会阻塞主线程吗?

Redis提供了两个命令来生成RDB快照文件:

  • save:同步保存操作,会阻塞Redis主线程;
  • bgsave:fork出一个子进程,子进程执行,不会阻塞Redis主线程,默认选项。

3.2.3 RDB持久化具体流程

  1. Redis客户端执行bgsave命令 或 自动触发bgsave命令;
  2. 父进程先判断:当前是否在执行save,或bgsave/bgrewriteaof(aof文件重写命令)的子进程;
  3. 如果存在:则父进程直接返回。 如果不存在:父进程执行fork操作创建一个子进程,fork过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令;
  4. fork完毕后返回 “ Background saving started” 信息并不再阻塞父进程,并可响应其他命令;
  5. 子进程创建临时RDB文件,待数据写入完毕后,对原有文件进行原子替换;
  6. 同时子进程退出,并发送信号给父进程表示完成rdb持久化,父进程更新统计信息。

3.3 AOF持久化

3.3.1 什么是AOF持久化?

与RDB持久化相比,AOF持久化的实时性更好。

默认情况下Redis没有开启AOF,但是Redis 6.0之后默认是开启的

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。

3.3.2 AOF的基本流程?

  1. 命令追加:所有的写命令会追加到AOF缓冲区中。
  2. 文件写入:将AOF缓冲区的数据写入到AOF文件中。这一步需要调用writer函数,将数据写入到系统内核缓冲区后直接返回。(此时并没有同步到磁盘)
  3. 文件同步:AOF缓冲区根据对应的持久化方式向磁盘做同步操作。
  4. 文件重写:随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
  5. 重启加载:当Redis重启时,可以加载AOF文件进行数据恢复。
AOF 工作基本流程
AOF 工作基本流程

3.3.3 持久化方式有哪些?

Redis配置文件中有三种不同的AOF持久化方式:

  1. appendfsync always:主线程调用 write 执行写操作后,后台线程( aof_fsync 线程)立即会调用 fsync 函数同步 AOF 文件(刷盘),fsync 完成后线程返回,这样会严重降低 Redis 的性能(write + fsync)。
  2. appendfsync everysec :主线程调用 write 执行写操作后立即返回,由后台线程( aof_fsync 线程)每秒钟调用 fsync 函数(系统调用)同步一次 AOF 文件(write+fsyncfsync间隔为 1 秒)
  3. appendfsync no :主线程调用 write 执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(write但不fsyncfsync 的时机由操作系统决定)。

可以看出:这 3 种持久化方式的主要区别在于 fsync 同步 AOF 文件的时机(刷盘)

3.3.4 AOF为什么是在执行完命令后记录日志?

关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复),而 Redis AOF 持久化机制是在执行完命令之后再记录日志。

AOF 记录日志过程
AOF 记录日志过程
  • 避免额外的检查开销,AOF记录日志不会对命令进行语法检查;
  • 在命令执行完之后再记录,不会阻塞当前命令执行。

缺点:

  • 如果刚执行完命令Redis就宕机,会导致对应的修改丢失;
  • 可能会阻塞后续其他命令的执行,因为AOF记录日志是在Redis主线程中进行的。

3.3.5 AOF文件重写了解吗?

当 AOF 变得太大时,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。

AOF 重写
AOF 重写

由于 AOF 重写会进行大量的写入操作,为了避免对 Redis 正常处理命令请求造成影响,Redis 将 AOF 重写程序放到子进程里执行

AOF 文件重写期间,Redis 还会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。

3.4 Redis 4.0对持久化机制做了什么优化?

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

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

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

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

3.5 如何选择RDB和AOF?

3.5.1 RDB的优势

  • RDB文件存储的内容是经过压缩的二进制数据,保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。
  • 使用RDB文件恢复数据,直接解析还原数据即可,不需要一条条地执行命令,速度非常快。

3.5.2 AOF的优势

  • AOF的数据安全性更高,可以实时或秒级持久化数据。RDB会对机器的CPU资源和内存资源产生严重的影响。
  • RDB可能会存在版本不兼容的问题。
  • AOF以一种易于理解和解析的格式包含所有操作的日志。方便导出AOF文件进行分析。

总结:

  • Redis保存的数据丢失一些也没有影响的话,可以使用RDB;
  • 不建议单独使用AOF,因为时不时创建一个RDB快照可以进行数据库备份、更快的重启以及解决AOF引擎错误。
  • 如果保存的数据要求安全性比较高的话,建议同时开启RDB和AOF持久化或者开启混合持久化。

四、Redis线程模型

4.1 Redis单线程模型了解吗?

Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。

4.1.1 既然是单线程,那怎么监听大量的客户端连接呢?

Redis通过IO多路复用来监听来自客户端的大量连接,将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。

优点:I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗

文件事件处理器主要包含4个部分:

  • 多个socket(客户端连接)
  • IO多路复用程序(支持多个客户端连接的关键)
  • 文件事件分派器(将socket关联到相应的事件处理器)
  • 事件处理器(连接应答、命令请求、命令回复)

Redis的网络模型就是使用了IO多路复用 + 时间分派器来应对多个Socket请求,比如连接应答、命令请求、命令回复。

在Redis 6.0之后,为了更好地提高性能,命令回复处理器使用了多线程来处理回复事件,命令请求处理器中的命令转换也使用了多线程,增加命令转换速度。

文件事件处理器(file event handler)
文件事件处理器(file event handler)

4.2 Redis是单线程的,那为什么还这么快?

  • Redis的大部分操作都是在内存中完成,因此Redis读取速度只跟计算机内存或者网络带宽有关;
  • Redis采用单线程可以避免多线程之间的竞争,避免了多线程切换带来的时间和性能开销;
  • Redis采用IO多路复用处理请求;

4.2.1 解释一下什么是IO多路复用?

IO多路复用其实就是用单个线程同时监听多个Socket,并且在某个Socket可读写时通知,从而避免无效等待,充分利用CPU资源。

目前的IO多路复用都是采用epoll模式实现的,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不用挨个遍历找是哪个Socket就绪了,提升了性能。

4.3 Redis 6.0之前为什么不使用多线程?

虽然Redis是单线程模型,但是实际上Redis在4.0之后的版本中就已经加入了对多线程的支持。

  1. 单线程编程容易,并且更容易维护;
  2. Redis的性能瓶颈不在CPU,而在于内存和网络;
  3. 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能;

4.4 Redis 6.0之后为什么引入了多线程

引入多线程主要是为了提高网络IO读写性能。但是对于命令的执行,Redis仍然采用单线程来处理

Redis 6.0的多线程默认是禁用的,只使用主线程。

五、Redis内存管理

5.1 为什么Redis要给缓存设置过期时间?

因为内存是有限的,如果缓存中的数据一直保存的话,会造成内存溢出,所以设置过期时间有助于环节内存消耗。

5.1.1 设置过期时间除了缓解内存消耗还有什么用?

很多时候,业务场景对于某些数据的需求只在某段时间内存在,比如说短信验证码只在一分钟内有效、用户登录的token只在一天内有效等等。如果使用传统的数据库来处理的话,一般还需要自己判断是否过期,这样会导致数据库性能差很多,而且很麻烦。

5.2 Redis如何判断数据是否过期?

Redis通过一个过期字典(可以看做是hash表)来保存数据过期时间。过期字典的key指向Redis数据库中的某个key,过期字典的value是一个long long型整数,这个整数保存了key所指向的数据库key的过期时间。

Redis过期字典
Redis过期字典

过期字典是存储在redisDb这个结构中:

1
2
3
4
5
6
7
typedef struct redisDb {
...

dict *dict; //数据库键空间,保存着数据库中所有键值对
dict *expires // 过期字典,保存着键的过期时间
...
} redisDb;

5.3 过期数据的删除策略了解吗?

Q:加入设置了一批key只能存活1分钟,那么一分钟后Redis是怎么对这批key进行删除的呢?

常用的过期数据删除策略就两个:

  1. 惰性删除:只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但是可能会造成太多过期key没有被删除。
  2. 定期删除:每隔一段时间抽取一批key执行删除过期key操作。并且,Redis底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。

惰性删除对CPU更友好。定期删除对内存更友好。所以Redis采用的是惰性删除+定期删除

但是以上两种删除策略还是可能存在遗漏很多过期key的情况,这样同样会造成内存溢出的情况。

怎么解决这个问题呢?

Redis内存淘汰机制

5.4 Redis的内存淘汰机制了解吗?

Q:MySQL中有2000w条数据,Redis中只有20w条数据,如何保证Redis中的数据都是热点数据?

Redis提供了6中数据淘汰策略:

  1. volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
  2. volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
  3. volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
  4. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(最常用)
  5. allkeys-random:从数据集中任意选择数据淘汰。
  6. no-eviction:禁止驱除数据,当内存不足时,不准新写入数据。

Redis 4.0之后增加了两种淘汰策略:

  1. volatile-lfu:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰。
  2. allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。

六、Redis事务

6.1 什么是Redis事务?

Redis事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。

Redis 事务实际开发中使用的非常少,功能比较鸡肋,不建议在日常开发中使用,不要将其和我们平时理解的关系型数据库的事务混淆了。

6.2 如何使用Redis事务?

  1. 开始事务,使用MULTI
  2. 命令入队,批量操作Redis命令,按照先进先出顺序
  3. 执行事务,使用EXEC

6.3 Redis事务支持原子性吗?

不支持!Redis事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常正常执行。并且Redis事务是不支持回滚操作的,因此,Redis是不支持原子性。

6.4 Redis事务支持持久性吗?

支持!Redis支持三种持久化方式:RDB、AOF和混合持久化。

6.5 Redis事务支持回滚吗?

Redis 中并没有提供回滚机制,虽然 Redis 提供了 DISCARD 命令,但是这个命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果。

七、Redis性能优化

7.1 使用批量操作减少网络传输

一个Redis命令的执行可以简化为4步:

  1. 发送命令;
  2. 命令排队;
  3. 命令执行;
  4. 返回结果。

其中第一步和第四步耗费时间之和成为RTT(往返时间),也即数据在网络上传输的时间。

使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少RTT。

7.2 大量key集中过期的问题

对于过期的key,Redis采用的是定期删除+惰性删除策略。

那么在定期删除的过程中,如果遇到大量key集中过期问题怎么解决呢?

  1. 给key设置随机过期时间。
  2. 开启lazy-free(惰性删除/延迟释放)。让Redis采用异步方式延迟释放key使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。

7.3 Redis bigkey

7.3.1 什么是bigkey?

简单来说,如果一个key对应的value所占用的内存较大,那么这个key就可以看做是bigkey。

那具体要占多大才算大呢?

String类型的value超过10kb,其他类型的value包含的元素超过5000个。

7.3.2 bigkey有什么危害?

  • 客户端超时阻塞:由于Redis执行命令是单线程处理,然后在操作大key时会比较耗时,阻塞其他命令的执行;
  • 引发网络阻塞:每次获取大key产生的网络流量较大,如果一个key的大小是1MB,每次访问量是1000,那么每秒会产生1000MB的流量,这对普通千兆网卡的服务器是灾难性的
  • 阻塞工作线程:如果使用del命令删除大key时,会阻塞工作线程,这样没法处理后续命令;
  • 内存分布不均,集群模型在slot分片均匀情况下,会出现数据和查询倾斜情况,部分有大key的Redis节点占用内存多,QPS也会比较大。

7.3.3 如何发现bigkey?

  1. 使用Redis自带的--bigkeys命令来查找,最好在从节点或者业务低峰阶段进行扫描,以免影响正常运行的业务;
  2. 使用scan命令查找bigkey,该命令会对数据库进行扫描,然后用type命令获取返回的每一个key的类型。
  3. 使用RdbTools工具分析RDB文件,找到其中的bigkey。比如可以将大于10kb的所有key输出到一个表格中;

7.3.4 如何避免大Key呢?

  • 在设计阶段把大key拆分成一个个小key;
  • 定期检查Redis中是否存在大key,删除的时候不要用del命令删除,因为会阻塞主线程,而是使用unlink命令删除大key。

7.3.5 为什么要用unlink命令删除大key呢?

因为del命令删除是一个同步命令,其删除操作会阻塞主线程的操作,而unlink命令会异步地删除指定的值。

unlink会先将要删除的键添加到一个待删除的列表中,并且立即返回,不会阻塞主线程的操作。然后Redis服务器会在后台异步地删除待删除列表中的值。

7.4 Redis内存碎片

7.4.1 什么是内存碎片?

简单来说,内存碎片就是不可用的空闲内存。

举个例子:操作系统为你分配了 32 字节的连续内存空间,而你存储数据实际只需要使用 24 字节内存空间,那这多余出来的 8 字节内存空间如果后续没办法再被分配存储其他数据的话,就可以被称为内存碎片。

内存碎片
内存碎片

7.4.2 为什么会有Redis内存碎片?

  1. Redis存储数据的时候想操作系统申请的内存空间可能会大于实际需求的存储空间。(要多了)
  2. 频繁修改Redis中的数据。

7.4.3 如何查看Redis内存碎片信息?

可以使用info memory命令查看Redis内存相关信息。

Redis内存碎片率的计算公式:

mem_fragmentation_ratio (内存碎片率)= used_memory_rss (操作系统实际分配给 Redis 的物理内存空间大小)/ used_memory(Redis 内存分配器为了存储数据实际申请使用的内存空间大小)

多大的内存碎片率才需要清理呢?

通常情况下,我们认为 mem_fragmentation_ratio > 1.5 的话才需要清理内存碎片。 mem_fragmentation_ratio > 1.5 意味着你使用 Redis 存储实际大小 2G 的数据需要使用大于 3G 的内存。

7.4.4 如何清理Redis内存碎片?

Redis4.0-RC3 版本以后自带了内存整理,可以避免内存碎片率过大的问题。

直接通过 config set 命令将 activedefrag 配置项设置为 yes 即可。

八、Redis生产问题

8.1 缓存穿透

一句话:穿透穿透,就是把缓存和数据库都穿透了。

8.1.1 什么是缓存穿透?

简单来说,就是大量请求的key是不合理的,根本不存在于缓存中,也不存在与数据库中

这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,甚至会直接宕机。

缓存穿透
缓存穿透

举个例子:某个黑客故意制造一些非法的 key 发起大量请求,导致大量请求落到数据库,结果数据库上也没有查到对应的数据。也就是说这些请求最终都落到了数据库上,对数据库造成了巨大的压力。

8.1.2 如何解决缓存穿透?

最基本的就是要做好参数校验,一些不合法的参数请求直接抛出异常给客户端。

1. 缓存无效key

如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,这种方式可以解决请求的 key 变化不频繁的情况。

2. 布隆过滤器

布隆过滤器是一种概率型数据结构,它的特点是高效的插入和查询。通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。

注:布隆过滤器不支持删除操作。

具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

加入布隆过滤器之后的缓存处理流程图
加入布隆过滤器之后的缓存处理流程图

但是,需要注意的是布隆过滤器可能会存在误判的情况。

总结: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

布隆过滤器的原理:

  1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(可能会有多个,因为有几个哈希函数得到几个哈希值)。
  2. 根据得到的哈希值,在位数组中把对应下标的值置为1.

当需要判断一个元素是否存在于布隆过滤器的时候:

  1. 对给定元素再次进行相同的哈希计算;
  2. 得到值后判断位数组中的每个元素是否都为1,如果值都为1,那说明这个值在布隆过滤器中,只要有一个值不为1,说明该元素不在。

所以,一定会出现不同的字符串可能哈希计算出的位置相同,所以会导致说某个元素存在,但其实不存在。

布隆过滤器为什么不支持删除操作?

在位数组中,每个位置只有0和1两种状态,如果一个位置已经被置为1,但是不确定它被设置了多少次,也不知道被多少个key通过hash计算映射而来,同样也不知道具体被哪个hash函数映射而来,所以删除操作会非常痛苦和麻烦。

需要删除的时候怎么办?

可以通过两个布隆过滤器解决,添加元素放在第一个布隆过滤器中,删除元素放在第二个布隆过滤器中。

如果我要判断key是否存在,首先检查第二个布隆过滤器是否删除过key,如果删除过就往第一个布隆过滤器中添加;如果没删除过,就查是否之前添加过。

8.2 缓存击穿

一句话:击穿击穿,就是打穿了缓存,对数据库定点打击。

8.2.1 什么是缓存击穿?

请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。

缓存击穿
缓存击穿

举个例子 :秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成了巨大的压力。

8.2.2 如何解决缓存击穿?

  • 设置热点数据永不过期或过期时间较长
  • 针对热点数据提前预热,将其存入缓存中,并设置合理的过期时间。比如秒杀场景中,数据在秒杀结束前都不会过期。
  • 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。

8.3 缓存穿透和缓存击穿有什么区别?

缓存穿透中,请求的key既不存在于缓存中,也不在数据库中。

缓存击穿中,请求的key对应的是热点数据,存在于数据库中,但是不存在于缓存中。

8.4 缓存雪崩

一句话:雪崩雪崩,就是缓存崩了,导致请求像雪崩一样落在数据库上。

8.4.1 什么是缓存雪崩?

缓存在同一时间大面积失效,导致大量的请求直接落到数据库上,对数据库造成巨大压力。

缓存雪崩
缓存雪崩

举个例子 :数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。

8.4.2 如何解决缓存雪崩?

1. 针对Redis服务不可用的情况

  1. 采用Redis集群,避免单机出问题整个缓存服务都无法使用的情况。
  2. 限流,避免同时处理大量的请求。

2. 针对热点缓存失效的情况

  1. 设置不同的失效时间,比如随机设置缓存失效时间。
  2. 缓存永不失效(不推荐)。
  3. 设置二级缓存。

8.5 缓存击穿和缓存雪崩有什么区别?

两者比较类似,缓存雪崩的原因是缓存中大量或所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在于缓存中。

8.6 怎么保证Redis和Mysql数据一致性?

常规的方法有两种:

  1. 先更新数据库,再删除缓存;
  2. 先删除缓存,再更新数据库。

第一种方式下,如果缓存更新失败,也会导致数据不一致;第二种方式下,理想情况是可以保证数据一致性的,但是在极端情况下,删除缓存的操作并不是原子操作,所以其他线程此时来访问redis,还是会出现数据不一致的情况。

所以如果要保证Redis和MySQL的数据一致性,有两种情况,分别是强一致性情况和允许延迟一致性情况。

对于要求强一致性的业务,可以采用redisson提供的读写锁来实现双写一致性,保证同一时间只运行一个请求更新缓存;

  • 读锁可以使其他线程共享读操作;
  • 写锁可以拒绝其他线程的写操作;

对于允许延迟一致性的业务,可以采用异步的方案同步数据,以下两种都属于先更新数据库,再删除缓存的方法:

  • 使用MQ消息中间件,更新数据后,通知缓存删除,如果删除失败,就继续读取数据重新删除,这就是重试机制,借助重试机制删除缓存。当删除成功,就把数据从消息队列中移除;
  • 利用canel中间件,可以不修改业务代码,通过读取数据库的binlog日志数据,再去更新缓存;

8.6.1 什么是延迟双删?

其实就是“先删除缓存,再更新数据库”的具体实现数据一致性的方法,其主要步骤有四个:

  1. 删除Redis缓存数据;
  2. 更新数据库数据;
  3. 延时sleep;
  4. 删除Redis缓存数据;

8.6.2 为什么要删除缓存数据,而不是更新?

在复杂的业务场景下,如果缓存的内容涉及到关联多个数据库表,那么为了修改缓存数据,需要查询多个关联表,再进行计算和修改更新,此时的更新缓存就显得没有必要了,因为直接删除会更快。如果缓存都更新后,却没有用上,那更是浪费时间,所以删除更能提高缓存的利用效率,减少复杂度。

8.6.3 为什么延迟双删需要sleep一段时间呢?

如果事务1进行数据库A=10进行+1操作,当有其他事务2在该时间段内进行查询操作时,因为这时候事务A对数据库的写操作还没结束,这时候事务2读到的是旧数据A=10。

如果事务1在写入数据库后,此时A=11,然后直接进行Redis的删除,这时候事务2在事务1删除Redis后更新了Redis,使A还等于10,那么这样就导致了数据库和缓存的数据不一致。

后面事务以后每次读取时读到的都是A=10的脏数据,直到Redis过期。

出问题的原因是什么,就是因为有事务2在事务1写入数据库的时间段内读取了数据库后,更新了缓存,导致其他时间段内都不会发生不一致的问题。

所以事务1才需要延时一段时间之后,对Redis进行再删除操作,避免其他事务对Redis的更新影响数据一致性问题。

延时时间 = 事务2读数据的时间 + 事务2更新缓存的时间

这时候事务1在第二次删除Redis前,事务2已经更新了Redis,保证了事务1一定能删除掉事务2的脏数据。这样就保证了最终的Redis和MySQL数据的一致性

九、Redis集群

Redis中提供的集群方案总共有三种:主从复制、哨兵模式、分片集群。

9.1 什么是主从复制?

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower);

数据的复制是单向的,只能由主节点到从节点。Master 以写为主,Slave 以读为主。默认情况下,每台 Redis 服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

9.2 主从复制的作用?

  1. 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  2. 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复。
  3. 负载均衡:由主节点提供写服务、从节点提供读服务,分担服务器负载,提高Redis服务器的并发量。
  4. 集群基础:主从复制还是哨兵和集群能够实施的基础。

9.3 一主二从

主从复制,读写分离! 80% 的情况下都是在进行读操作!减缓服务器的压力!架构中经常使用! 一主二从!主机可以写,从机不能写只能读!主机中的所有信息和数据,都会自动被从机保存!

Slave 启动成功连接到 master 后会发送一个 sync 同步命令 Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master 将传送 整个数据文件到 slave,并完成一次完全同步。

9.3.1 数据的同步方式

  1. 全量复制:用于初次复制或其它无法进行部分复制的情况,将主节点中的所有数据都发送给从节点。当数据量过大的时候,会造成很大的网络开销。
  2. 增量复制:只复制新增的数据,也就是 master 变化不大,被更改的指令都保存在内存时,会触发增量复制。

9.4 哨兵模式

哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个 独立的进程,作为进程,它会独立运行。

其原理是哨兵通过发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例。 如果故障了根据投票数自动将从节点转换为主节点

通过哨兵模式,可以监控主从服务器,并提供主从节点故障转移功能。

哨兵模式
哨兵模式

9.4.1 优点

  1. 哨兵集群,基于主从复制模式,拥有所有主从配置的优点。
  2. 主从可以切换,故障可以转移,系统可用性更好。
  3. 哨兵模式就是主从模式升级,手动到自动。

9.4.2 缺点

  1. Redis不好在线扩容,一旦集群容量达到上线,扩容十分麻烦。
  2. 哨兵模式配置十分麻烦。

9.4.3 Redis的哨兵模式是怎么知道有多少节点的呢?

可以通过哨兵的自动发现机制实现的。

一般情况下,哨兵节点每10秒向主从节点发送INFO命令,以此获取主从节点的信息。在第一次执行的时候,哨兵仅知道我们给出的主节点信息,通过对主节点执行INFO命令就可以获取其从节点列表。这样周期性的循环,就可以不断地发现新加入的节点。

9.4.4 Redis哨兵之间是怎么知道彼此的呢?

因为哨兵可以通过INFO命令发现主节点和从节点的信息,而Redis提供了一种发布订阅的消息通信模式,哨兵们就是通过一个约定好的频道发布/订阅信息进行通信:

  • 每隔2秒,每个哨兵就会通过它监控的主节点、从节点向频道发送一条hello消息;

  • 每个哨兵会通过它监控的主节点、从节点订阅频道的消息,以此来接收其他哨兵发布的消息。

9.4.5 Redis哨兵是怎么判断节点的服务状态的呢?

主要是基于心跳机制监测服务状态,每隔1秒,想集群中的每个实例发送ping命令:

  • 主观下线:如果某个哨兵发现某个实例没有在规定时间响应,就认为该实例主观下线;
  • 客观下线:若超过指定数量(一般是哨兵数量的一半)的哨兵都认为该实例主观下线,那么就可以认为该实例客观下线;

9.4.6 Redis哨兵是怎么推举出新的主节点呢?

  • 判断每个从节点与主节点断联时间的长短,时间越长就说明该从节点的数据与最新的数据差异较大,排除该节点;
  • 判断从节点的slave-priority值,也即从节点的优先级,值越小,优先级越高;
  • 如果优先级相同,就会判断offset值,值越大说明与主节点数据越接近,其优先级越高;
  • 判断从节点运行id的大小,越小优先级越高;

9.5 集群出现脑裂该怎么解决?

脑裂其实就是由于网络问题,集群节点之间失去联系。主从数据不同步;哨兵重新平衡选举,产生两个主节点,这就是集群的脑裂。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。

解决方法可以在redis中设置两个参数:

  1. 设置主节点最少连接的从节点个数,如果从节点数小于这个数,就禁止主节点写数据;
  2. 设置主从复制和同步的延迟时间,当超过一定时间时,就禁止主节点写数据。

9.6 分片集群有什么用?

当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 分片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。

集群中有多个主节点,每个主节点保存不同数据,并且还可以给每个主节点设置多个从节点,继续增大集群的并发能力。同时每个主节点之间通过ping检测彼此健康状态,有点类似于哨兵模式。当客户端请求访问集群时,会被转发到正确的节点。

分片集群中不再需要哨兵,由主节点就可以起到哨兵的作用。

9.6.1 分片集群中数据怎么存储和读取的?

一个分片集群共有16384个哈希槽,集群中每个主节点绑定了一定范围内的哈希槽,key通过CRC16校验后对16384取模来决定放在哪个槽存储。

比如有三个主节点,如图所示:

分片集群
分片集群

读取的过程也是一样的。


本站由 Cccccpg 使用 Stellar 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。