分布式缓存:Redis

Redis

数据类型

基本

  • String

    • 是什么

      字符串

    • 数据结构

      自己构建了一种 简单动态字符串(Simple Dynamic String,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。

    • 使用场景

      缓存 Session、Token、图片地址、序列化后的对象(相比较于 Hash 存储更节省内存)。

  • List

    • 是什么

      列表

    • 数据结构

      Redis中List实际是一种特殊的双向链表,他是zipList和linkedList组成的一种quickList,数据量小时只使用zipList

      • 优势

        List是使用了一种特殊双向链表,其中,zipList兼顾了数组连续内存的查询效率高的优点,而linkedList有了双向链表中插入和删除的优点

    • 使用场景

      最新文章、最新动态。

  • Hash

    • 是什么

      哈希

    • 数据结构

      Redis中的Hash使用HashTable和zipList的数据结构

      • 优势

        zipList保证了数据连续性,不同于普通数组,每个节点的内存占比可以不同,在数据较少时保证了查询效率和内存占用,HashTable保证了在数据量比较大时查询效率的保证

    • 使用场景

      用户信息、商品信息、文章信息、购物车信息。

  • Set

    • 是什么

      集合

    • 数据结构

      Redis中Set的数据结构使用了HashTable和intset(数组)

      在Redis7.2之前,当一个集合满足以下两个条件时,Redis 会选择使用intset编码:
      集合对象保存的所有元素都是整数值
      集合对象保存的元素数量小于等于512个(默认)

      • 优势

        intset 是一种紧凑的数组结构,它只保存整型数据,并且按照从小到大的顺序存储元素。intset 的使用可以节省内存,因为它避免了 hashtable 所需的额外空间开销,并且因为是连续存储,所以访问速度也很快。数据量大时使用HashTable,内存大占用高(初始化的哈希槽数组),但是查询速度快

    • 使用场景

      网站 UV 统计(数据量巨大的场景还是 HyperLogLog更适合一些)、文章点赞、动态点赞等场景。

  • Sorted-Set

    • 是什么

      有序集合

    • 数据结构

      Redis中Sorted-Set主要有ZipList和skipList组成

      • 优势

        优势:
        结构简单
        查询快速,查询效率为n(logn)
        内存占用低
        Sorted-Set使用skipList而不使用B+Tree、平衡树、红黑树的原因:
        redis本身的查询发生在内存中,所以发挥不了B+Tree在数据库中减少IO次数的优势;(B+Tree)
        由于本身的数据结构的复杂性,所以当进行添加和删除时涉及到树的平衡和调整带来的性能损耗(平衡树)
        同存储数量的情况下,SkipList内存占用小于B+Tree的内存占用(B+Tree)
        Sorted-Set在使用范围查询情况下,查询效率要高(红黑树)
        SkipList实现方式更加简单(三种树)

    • 使用场景

      各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。

特殊

  • Bitmap

    • 是什么

      位图

    • 数据结构

      利用了基本数据类型中的String进行实现

    • 应用场景

      签到、行为统计

  • HyperLogLog

    • 是什么

      基数统计

    • 应用场景

      热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计、

  • Geospatial

    • 是什么

      地理坐标

    • 数据结构

      使用了基本数据类型中的Sorted-Set实现

    • 应用场景

      附近的人

持久化机制

快照(RDB)

  • 什么是RDB

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

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

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

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

只追加文件(AOF)

  • 什么是 AOF 持久化?

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

    只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。

  • AOF 工作基本流程

    AOF 持久化功能的实现可以简单分为 5 步:

    命令追加(append):所有的写命令会追加到 AOF 缓冲区中。
    文件写入(write):将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用write函数(系统调用),write将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。
    文件同步(fsync):AOF 缓冲区根据对应的持久化方式( fsync 策略)向硬盘做同步操作。这一步需要调用 fsync 函数(系统调用), fsync 针对单个文件操作,对其进行强制硬盘同步,fsync 将阻塞直到写入磁盘完成后返回,保证了数据持久化。
    文件重写(rewrite):随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
    重启加载(load):当 Redis 重启时,可以加载 AOF 文件进行数据恢复。
    Linux 系统直接提供了一些函数用于对文件和设备进行访问和控制,这些函数被称为 系统调用(syscall)。

    这里对上面提到的一些 Linux 系统调用再做一遍解释:

    write:写入系统内核缓冲区之后直接返回(仅仅是写到缓冲区),不会立即同步到硬盘。虽然提高了效率,但也带来了数据丢失的风险。同步硬盘操作通常依赖于系统调度机制,Linux 内核通常为 30s 同步一次,具体值取决于写出的数据量和 I/O 缓冲区的状态。
    fsync:fsync用于强制刷新系统内核缓冲区(同步到到磁盘),确保写磁盘操作结束才会返回。
    著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/redis/redis-persistence.html

  • AOF持久化的方式

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

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

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

    为了兼顾数据和写入性能,可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能受到的影响较小。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

    著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/redis/redis-persistence.html

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

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

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

    这样也带来了风险(我在前面介绍 AOF 持久化的时候也提到过):

    如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
    可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)。
    著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/redis/redis-persistence.html

  • AOF的文件重写

    当 AOF 变得太大时,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
    由于 AOF 重写会进行大量的写入操作,为了避免对 Redis 正常处理命令请求造成影响,Redis 将 AOF 重写程序放到子进程里执行。

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

    著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/redis/redis-persistence.html

    • 缺点

      Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次。

  • AOF的检验机制

    AOF 校验机制是 Redis 在启动时对 AOF 文件进行检查,以判断文件是否完整,是否有损坏或者丢失的数据。这个机制的原理其实非常简单,就是通过使用一种叫做 校验和(checksum) 的数字来验证 AOF 文件。这个校验和是通过对整个 AOF 文件内容进行 CRC64 算法计算得出的数字。如果文件内容发生了变化,那么校验和也会随之改变。因此,Redis 在启动时会比较计算出的校验和与文件末尾保存的校验和(计算的时候会把最后一行保存校验和的内容给忽略点),从而判断 AOF 文件是否完整。如果发现文件有问题,Redis 就会拒绝启动并提供相应的错误信息。AOF 校验机制十分简单有效,可以提高 Redis 数据的可靠性。
    著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/redis/redis-persistence.html

RDB 和 AOF 的混合持久化

  • 比起单独使用RDB和AOF优势和缺点

    如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

    著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/redis/redis-persistence.html

如何选择RDB和AOF

  • RDB的优势

    RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。AOF 文件存储的是每一次写命令,类似于 MySQL 的 binlog 日志,通常会比 RDB 文件大很多。当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。不过, Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次。
    使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。也就是说,与 AOF 相比,恢复大数据集的时候,RDB 速度更快。
    著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/redis/redis-persistence.html

  • AOF的优势

    RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。生成 RDB 文件的过程是比较繁重的, 虽然 BGSAVE 子进程写入 RDB 文件的工作不会阻塞主线程,但会对机器的 CPU 资源和内存资源产生影响,严重的情况下甚至会直接把 Redis 服务干宕机。AOF 支持秒级数据丢失(取决 fsync 策略,如果是 everysec,最多丢失 1 秒的数据),仅仅是追加命令到 AOF 文件,操作轻量。
    RDB 文件是以特定的二进制格式保存的,并且在 Redis 版本演进中有多个版本的 RDB,所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。
    AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析,你也可以直接操作 AOF 文件来解决一些问题。比如,如果执行FLUSHALL命令意外地刷新了所有内容后,只要 AOF 文件没有被重写,删除最新命令并重启即可恢复之前的状态。
    著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/redis/redis-persistence.html

Redis 内存管理

Redis 给缓存数据设置过期时间有什么用

内存是有限且珍贵的,如果不对缓存数据设置过期时间,那内存占用就会一直增长,最终可能会导致 OOM 问题。通过设置合理的过期时间,Redis 会自动删除暂时不需要的数据,为新的缓存数据腾出空间。

Redis 是如何判断数据是否过期的呢

Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
在查询一个 key 的时候,Redis 首先检查该 key 是否存在于过期字典中(时间复杂度为 O(1)),如果不在就直接返回,在的话需要判断一下这个 key 是否过期,过期直接删除 key 然后返回 null。

过期 key 删除策略

  • 惰性删除

    只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。

    • 为什么 key 过期之后不立马把它删掉呢?这样不是会浪费很多内存空间吗?

      因为不太好办到,或者说这种删除方式的成本太高了。假如我们使用延迟队列作为删除策略,这样存在下面这些问题:

      队列本身的开销可能很大:key 多的情况下,一个延迟队列可能无法容纳。
      维护延迟队列太麻烦:修改 key 的过期时间就需要调整期在延迟队列中的位置,并且,还需要引入并发控制。
      著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/redis/redis-questions-01.html

  • 定期删除

    周期性地随机从设置了过期时间的 key 中抽查一批,然后逐个检查这些 key 是否过期,过期就删除 key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。

    • 为什么定期删除不是把所有过期 key 都删除

      这样会对性能造成太大的影响。如果我们 key 数量非常庞大的话,挨个遍历检查是非常耗时的,会严重影响性能。Redis 设计这种策略的目的是为了平衡内存和性能。

  • 延迟队列

    把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。

  • 定时删除

    每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。

Redis 采用的删除策略

Redis 采用的是 定期删除+惰性/懒汉式删除 结合的策略,这也是大部分缓存框架的选择。定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,结合起来使用既能兼顾 CPU 友好,又能兼顾内存友好。

  • 大量 key 集中过期怎么办?

    如果存在大量 key 集中过期的问题,可能会使 Redis 的请求延迟变高。可以采用下面的可选方案来应对:

    尽量避免 key 集中过期,在设置键的过期时间时尽量随机一点。
    对过期的 key 开启 lazyfree 机制(修改 redis.conf 中的 lazyfree-lazy-expire参数即可),这样会在后台异步删除过期的 key,不会阻塞主线程的运行。
    著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/redis/redis-questions-01.html

Redis 内存淘汰策略

  • volatile-lru(least recently used)

    从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。

  • volatile-ttl

    volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。

  • volatile-random

    从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。

  • allkeys-lru(least recently used)

    从数据集(server.db[i].dict)中移除最近最少使用的数据淘汰。

  • allkeys-random

    从数据集(server.db[i].dict)中任意选择数据淘汰。

  • no-eviction

    no-eviction(默认内存淘汰策略):禁止驱逐数据,当内存不足以容纳新写入数据时,新写入操作会报错。

  • volatile-lfu(least frequently used)

    volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰。

  • allkeys-lfu(least frequently used)

    allkeys-lfu(least frequently used):从数据集(server.db[i].dict)中移除最不经常使用的数据淘汰。

内存碎片

什么是内存碎片

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

危害

Redis 内存碎片虽然不会影响 Redis 性能,但是会增加内存消耗。

为什么会产生内存碎片

1、Redis 存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。Redis 使用 zmalloc 方法(Redis 自己实现的内存分配方法)进行内存分配的时候,除了要分配 size 大小的内存之外,还会多分配 PREFIX_SIZE 大小的内存

2、频繁修改 Redis 中的数据也会产生内存碎片。
当 Redis 中的某个数据删除时,Redis 通常不会轻易释放内存给操作系统。

Redis 事务

什么是Redis事务

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

Redis 事务支持原子性吗

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

Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。

著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/redis/redis-questions-02.html

Redis 事务支持持久性吗?

Redis 事务的持久性也是没办法保证的。

如何解决 Redis 事务的缺陷

Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。

一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。

不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, 严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。

如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。

Redis 性能优化

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

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

大Key

  • 什么是大Key

    简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:

    String 类型的 value 超过 1MB
    复合类型(List、Hash、Set、Sorted Set 等)的 value 包含的元素超过 5000 个(不过,对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)

  • bigkey 是怎么产生的

    bigkey 通常是由于下面这些原因产生的:

    程序设计不当,比如直接使用 String 类型存储较大的文件对应的二进制数据。
    对于业务的数据规模考虑不周到,比如使用集合类型的时候没有考虑到数据量的快速增长。
    未及时清理垃圾数据,比如哈希中冗余了大量的无用键值对。

  • 危害

    客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
    网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
    工作线程阻塞:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。

  • 大量 key 集中过期问题

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

热Key

  • 什么是 hotkey

    如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 hotkey(热 Key)。例如在 Redis 实例的每秒处理请求达到 5000 次,而其中某个 key 的每秒访问量就高达 2000 次,那这个 key 就可以看作是 hotkey。

  • 危害

    处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。l

慢查询命令

Redis 慢查询统计的是命令执行这一步骤的耗时,慢查询命令也就是那些命令执行时间较长的命令。

常见阻塞原因

O(n) 命令

SAVE 创建 RDB 快照

AOF

  • AOF 日志记录阻塞

  • AOF 刷盘阻塞

  • AOF 重写阻塞

大 Key

  • 查找大 key

  • 删除大 key

清空数据库

集群扩容

Swap(内存交换)

CPU 竞争

网络问题

线程模型

Redis的单线程模型

Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型

  • 单线程,怎么监听大量的客户端连接

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

  • Redis6.0 之前为什么不使用多线程

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

Redis6.0 之后为何引入了多线程?

Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。

虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。

著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/redis/redis-questions-01.html

Redis 后台线程

我们虽然经常说 Redis 是单线程模型(主要逻辑是单线程完成的),但实际还有一些后台线程用于执行一些比较耗时的操作:

通过 bio_close_file 后台线程来释放 AOF / RDB 等过程中产生的临时文件资源。
通过 bio_aof_fsync 后台线程调用 fsync 函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘( AOF 文件)。
通过 bio_lazy_free后台线程释放大对象(已删除)占用的内存空间

哨兵模式

Sentinel

  • 什么是sentinel

    Sentinel(哨兵) 只是 Redis 的一种运行模式 ,不提供读写服务,默认运行在 26379 端口上,依赖于
    Redis 工作。Redis Sentinel 的稳定版本是在 Redis 2.8 之后发布的。
    Redis Sentinel 实现 Redis 集群高可用,只是在主从复制实现集群的基础下,多了一个 Sentinel 角色来
    帮助我们监控 Redis 节点的运行状态并自动实现故障转移。
    当 master 节点出现故障的时候, Sentinel 会帮助我们实现故障转移,自动根据一定的规则选出一个
    slave 升级为 master,确保整个 Redis 系统的可用性。整个过程完全自动,不需要人工介入。

  • sentinel的作用

    根据 Redis Sentinel 官方文档的介绍,sentinel 节点主要可以提供 4 个功能:
    监控:监控所有 redis 节点(包括 sentinel 节点自身)的状态是否正常。
    故障转移:如果一个 master 出现故障,Sentinel 会帮助我们实现故障转移,自动将某一台 slave 升级为 master,确保整个 Redis 系统的可用性。
    通知 :通知 slave 新的 master 连接信息,让它们执行 replicaof 成为新的 master 的 slave。
    配置提供 :客户端连接 sentinel 请求 master 的地址,如果发生故障转移,sentinel 会通知新的
    master 链接信息给客户端。
    Redis Sentinel 本身设计的就是一个分布式系统,建议多个 sentinel 节点协作运行。这样做的好处是:
    多个 sentinel 节点通过投票的方式来确定 sentinel 节点是否真的不可用,避免误判(比如网络问
    题可能会导致误判)。
    Sentinel 自身就是高可用。
    如果想要实现高可用,建议将哨兵 Sentinel 配置成单数且大于等于 3 台。
    一个最简易的 Redis Sentinel 集群如下所示(官方文档中的一个例子),其中:
    M1 表示 master,R2、R3 表示 slave;
    S1、S2、S3 都是 sentinel;quorum 表示判定 master 失效最少需要的仲裁节点数。这里的值为 2 ,也就是说当有 2 个
    sentinel 认为 master 失效时,master 才算真正失效
    如果 M1 出现问题,只要 S1、S2、S3 其中的两个投票赞同的话,就会开始故障转移工作,从 R2 或者 R3 中重新选出一个作为 master。

  • Sentinel 如何检测节点是否下线?

    Redis Sentinel 中有两个下线(Down)的概念:
    主观下线(SDOWN) :sentinel 节点认为某个 Redis 节点已经下线了(主观下线),但还不是很确
    定,需要其他 sentinel 节点的投票。
    客观下线(ODOWN) :法定数量(通常为过半)的 sentinel 节点认定某个 Redis 节点已经下线(客
    观下线),那它就算是真的下线
    也就是说,主观下线 当前的 sentinel 自己认为节点宕机,客观下线是 sentinel 整体达成一致认为节点
    宕机。

  • Sentinel 如何选择出新的 master?

    slave 必须是在线状态才能参加新的 master 的选举,筛选出所有在线的 slave 之后,通过下面 3 个维度
    进行最后的筛选(优先级依次降低):

    1. slave 优先级 :可以通过 slave-priority 手动设置 slave 的优先级,优先级越高得分越高,优先级
      最高的直接成为新的 master。如果没有优先级最高的,再判断复制进度。
    2. 复制进度 :Sentinel 总是希望选择出数据最完整(与旧 master 数据最接近)也就是复制进度最快
      的 slave 被提升为新的 master,复制进度越快得分也就越高。
    3. runid(运行 id) :通常经过前面两轮筛选已经成果选出来了新的 master,万一真有多个 slave 的优先级和复制进度一样的话,那就 runid 小的成为新的 master,每个 redis 节点启动时都有一个 40 字节随机字符串作为运行 id。
  • 如何从 Sentinel 集群中选择出 Leader

    这就需要用到分布式领域的 共识算法 了。简单来说,共识算法就是让分布式系统中的节点就一个问题达 成共识。在 sentinel 选举 leader 这个场景下,这些 sentinel 要达成的共识就是谁才是 leader 。
    大部分共识算法都是基于 Paxos 算法改进而来,在 sentinel 选举 leader 这个场景下使用的是 Raft 算 法。这是一个比 Paxos 算法更易理解和实现的共识算法—Raft 算法。更具体点来说,Raft 是 MultiPaxos 的一个变种,其简化了 Multi-Paxos 的思想,变得更容易被理解以及工程实现。

  • Sentinel 可以防止脑裂吗?

    还是上面的例子,如果 M1 和 R2、R3 之间的网络被隔离,也就是发生了脑裂,M1 和 R2 、 R3 隔离在 了两个不同的网络分区中。这意味着,R2 或者 R3 其中一个会被选为 master,这里假设为 R2。
    但是!这样会出现问题了!!
    如果客户端 C1 是和 M1 在一个网络分区的话,从网络被隔离到网络分区恢复这段时间,C1 写入 M1 的 数据都会丢失,并且,C1 读取的可能也是过时的数据。这是因为当网络分区恢复之后,M1 将会成为 slave 节点。
    面对这两个配置进行解释:
    min-replicas-to-write 1:用于配置写 master 至少写入的 slave 数量,设置为 0 表示关闭该功
    能。3 个节点的情况下,可以配置为 1 ,表示 master 必须写入至少 1 个 slave ,否则就停止接受
    新的写入命令请求。
    min-replicas-max-lag 10 :用于配置 master 多长时间(秒)无法得到从节点的响应,就认为这
    个节点失联。我们这里配置的是 10 秒,也就是说 master 10 秒都得不到一个从节点的响应,就会
    认为这个从节点失联,停止接受新的写入命令请求。
    不过,这样配置会降低 Redis 服务的整体可用性,如果 2 个 slave 都挂掉,master 将会停止接受新的写
    入命令请求。

集群模式

Redis Cluster

  • 什么是Redis Cluster

    简单来说,Redis 切片集群 就是部署多台 Redis 主节点(master),这些节点之间平等,并没有主从之
    说,同时对外提供读/写服务。缓存的数据库相对均匀地分布在这些 Redis 实例上,客户端的请求通过路 由规则转发到目标 master 上。
    为了保障集群整体的高可用,我们需要保证集群中每一个 master 的高可用,可以通过主从复制给每个 master 配置一个或者多个从节点(slave)。

  • 为什么需要Redis Cluster

    1. 缓存的数据量太大 :实际缓存的数据量可以达到几十 G,甚至是成百上千 G;
    2. 并发量要求太大 :虽然 Redis 号称单机可以支持 10w 并发,但实际项目中,不可靠因素太多,就
      比如一些复杂的写/读操作就可能会让这个并发量大打折扣。而且,就算真的可以实际支持 10w 并发,达到瓶颈了,可能也没办法满足系统的实际需求。
  • 解决的问题

    主从复制和 Redis Sentinel 这两种方案本质都是通过增加主库(master)的副本(slave)数量的方式 来提高 Redis 服务的整体可用性和读吞吐量,都不支持横向扩展来缓解写压力以及解决缓存数据量过大的问题。
    对于这两种方案来说,如果写压力太大或者缓存数据量太大的话,我们可以考虑提高服务器硬件的配
    置。不过,提高硬件配置成本太高,能力有限,无法动态扩容缩容,局限性太大。从本质上来说,靠堆
    硬件配置的方式并没有实质性地解决问题,依然无法满足高并发场景下分布式缓存的要求。

  • 优势

    Redis Cluster 的动态扩容和缩容是其最大的优势。
    可以横向扩展缓解写压力和存储压力,支持动态扩容和缩容;
    具备主从复制、故障转移(内置了 Sentinel 机制,无需单独部署 Sentinel 集群)等开箱即用的功
    能。

  • 最基本的架构

    为了保证高可用,Redis Cluster 至少需要 3 个 master 以及 3 个 slave,也就是说每个 master 必须有 1 个 slave。master 和 slave 之间做主从复制,slave 会实时同步 master 上的数据。
    不同于普通的 Redis 主从架构,这里的 slave 不对外提供读服务,主要用来保障 master 的高可用,当master 出现故障的时候替代它。
    如果 master 只有一个 slave 的话,master 宕机之后就直接使用这个 slave 替代 master 继续提供服务。假设 master1 出现故障,slave1 会直接替代 master1,保证 Redis Cluster 的高可用。

  • 是如何分片的?

    Redis Cluster 并没有使用一致性哈希,采用的是 哈希槽分区 ,每一个键值对都属于一个 hash slot(哈
    希槽) 。
    Redis Cluster 通常有 16384 个哈希槽 ,要计算给定 key 应该分布到哪个哈希槽中,我们只需要先对每 个 key 计算 CRC-16(XMODEM) 校验码,然后再对这个校验码对 16384(哈希槽的总数) 取模,得到的 值即是 key 对应的哈希槽。
    哈希槽的计算公式如下:
    HASH_SLOT = CRC16(key) mod NUMER_OF_SLOTS

    创建并初始化 Redis Cluster 的时候,Redis 会自动平均分配这 16384 个哈希槽到各个节点,不需要我 们手动分配
    假设集群有 3 个 Redis 节点组成,每个节点负责整个集群的一部分数据,哈希槽可能是这样分配的(这
    里只是演示,实际效果可能会有差异):
    Node 1 : 0 - 5500 的 hash slots
    Node 2 : 5501 - 11000 的 hash slots
    Node 3 : 11001 - 16383 的 hash slots

    客户端连接 Redis Cluster 中任意一个 master 节点即可访问 Redis Cluster 的数据,当客户端发送命令
    请求的时候,需要先根据 key 通过上面的计算公示找到的对应的哈希槽,然后再查询哈希槽和节点的映
    射关系,即可找到目标节点。

    如果哈希槽确实是当前节点负责,那就直接响应客户端的请求返回结果,如果不由当前节点负责,就会 返回 -MOVED 重定向错误,告知客户端当前哈希槽是由哪个节点负责,客户端向目标节点发送请求并更 新缓存的哈希槽分配信息。

    • 客户端计算哈希错误的原因

      这是因为 Redis Cluster 内部可能会重新分配哈希槽比如扩容缩容的时候(后文中有详细介绍到 Redis
      Cluster 的扩容和缩容问题),这就可能会导致客户端缓存的哈希槽分配信息会有误。

    • 哈希槽分区机制的优点

      解耦了数据和节点之间的关系,提升了集群的横向扩展性和容错性。

    • 为什么哈希槽是 16384 个

      哈希槽太大会导致心跳包太大,消耗太多带宽;
      哈希槽总数越少,对存储哈希槽信息的 bitmap 压缩效果越好;
      Redis Cluster 的主节点通常不会扩展太多,16384 个哈希槽已经足够用

  • 扩容缩容期间可以提供服务吗?

    为了保证 Redis Cluster 在扩容和缩容期间依然能够对外正常提供服务,Redis Cluster 提供了重定向机
    制,两种不同的类型:
    ASK 重定向
    MOVED 重定向

    • ASK 重定向

      从客户端的角度来看,ASK 重定向是下面这样的:

      1. 客户端发送请求命令,如果请求的 key 对应的哈希槽还在当前节点的话,就直接响应客户端的请
        求。
      2. 如果客户端请求的 key 对应的哈希槽当前正在迁移至新的节点,就会返回 -ASK 重定向错误,告知
        客户端要将请求发送到哈希槽被迁移到的目标节点。
      3. 客户端收到 -ASK 重定向错误后,将会临时(一次性)重定向,自动向目标节点发送一条 ASKING
        命令。也就是说,接收到 ASKING 命令的节点会强制执行一次请求,下次再来需要重新提前发送
        ASKING 命令。
      4. 客户端发送真正的请求命令。
      5. ASK 重定向并不会同步更新客户端缓存的哈希槽分配信息,也就是说,客户端对正在迁移的相同哈
        希槽的请求依然会发送到原节点而不是目标节点
    • MOVED 重定向

      如果客户端请求的 key 对应的哈希槽应该迁移完成的话,就会返回 -MOVED 重定向错误,告知客户端当 前哈希槽是由哪个节点负责,客户端向目标节点发送请求并更新缓存的哈希槽分配信息。

  • 节点是怎么进行通信的

    Redis Cluster 是一个典型的分布式系统,分布式系统中的各个节点需要互相通信。既然要相互通信就要 遵循一致的通信协议,Redis Cluster 中的各个节点基于 Gossip 协议 来进行通信共享信息,每个 Redis 节点都维护了一份集群的状态信息。
    Redis Cluster 的节点之间会相互发送多种 Gossip 消息:
    MEET :在 Redis Cluster 中的某个 Redis 节点上执行 CLUSTER MEET ip port 命令,可以向指定
    的 Redis 节点发送一条 MEET 信息,用于将其添加进 Redis Cluster 成为新的 Redis 节点。
    PING/PONG :Redis Cluster 中的节点都会定时地向其他节点发送 PING 消息,来交换各个节点状
    态信息,检查各个节点状态,包括在线状态、疑似下线状态 PFAIL 和已下线状态 FAIL。
    FAIL :Redis Cluster 中的节点 A 发现 B 节点 PFAIL ,并且在下线报告的有效期限内集群中半数以
    上的节点将 B 节点标记为 PFAIL,节点 A 就会向集群广播一条 FAIL 消息,通知其他节点将故障节
    点 B 标记为 FAIL 。
    ……
    有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。Redis Cluster 相当于是内置了
    Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议互相探测健康状态,在故障时可以自动切换。


分布式缓存:Redis
https://xsinxcos.github.io/2024/09/20/Redis/
作者
xsinxcos(涿)
发布于
2024年9月20日
许可协议