Redis是一种单线程单进程的“key-value”类型数据的分布式NoSQL数据库,官网给出每秒内可达100000+查询次数。

为什么查询速度快?

  1. 完全基于内存缓存,绝大多数请求都是直接通过内存操作完成的,查找和操作的时间复杂度都是O(1);
  2. 数据结构简单,对数据的操作也简单;
  3. 采用单线程,避免了不必要的上下文切换和竞争,也不存在多线程和多进程切换而消耗CPU;什么是上下文切换?
  4. 使用多路I/O复用模型,非阻塞IO;

    数据结构

    基本数据类型:String,List,Hash,Set,SortedSet。
    高级类型:HyperLogLog,Geo,Pub/Sub。
    Redis Module:BloomFilter(布隆过滤器)、RedisSearch、Redis-ML

    Redis集群 - Redis Cluster

    使用 Redis Cluster 实现集群部署,实现高可用的主从复制和读写分离。Redis Cluster 由多个 master 节点支持,每个 master 节点下可以有多个 slave 节点。当某个 master 节点宕掉后,会推选其下的一个 slave 节点代替。支持横向扩容更多的 master 节点。
    更多 Redis Cluster 知识见:https://www.yuque.com/xiong.zheng/golk3r/oh40gc

    持久化

    参考文章:
    https://segmentfault.com/a/1190000016021217
    https://blog.csdn.net/ll594317566/article/details/109215575
    Redis 提供了 RDB 和 AOF 两种持久化方式。

    RDB持久化

    RDB持久化方式能够在指定时间间隔内将内存中的数据集快照写入磁盘,也是默认的持久化方式。生成二进制文件默认名:dump.rdb。在 Redis 重启后,RDB程序可以通过载入 RDB文件来还原数据库状态。

    RDB文件保存过程

    当 Redis 需要保存 dump.rdb 文件时,服务器执行了以下操作:

  5. redis 调用 fork,同时拥有了父进程和子进程;

  6. 父进程继续处理 client 端请求,子进程负责把内存中的数据集写入到临时文件;
  7. 当子进程将快照写入临时文件完成后,临时文件替换掉原来的RDB文件,子进程退出。

    RDB三种主要触发机制

    1. save 命令

    save命令执行同步操作,将内存中的数据集持久化保存到 RDB 文件中。
    由于save命令是同步的,会占用 redis 的主进程。如果redis中的数据集非常大时,save命令的执行速度会非常慢,从而阻塞所有客户端的请求。
    因此很少在生产环境直接使用save命令,可以使用bgsave命令代替。

    2. bgsave 命令

    bgsave命令执行异步操作,将内存中的数据集持久化保存到 RDB 文件中。
    redis 调用 fork 生成一个子进程来保存快照,主进程继续提供服务给客户端调用。
命令 save bgsave
IO类型 同步 异步
阻塞? 是(阻塞发生在fock(),通常非常快)
复杂度 O(n) O(n)
优点 不会消耗额外的内存 不阻塞客户端命令
缺点 阻塞客户端命令 需要fock子进程,消耗内存

3. 自动生成

除了手动执行savebgsave命令外,redis也提供了自动生成RDB文件的方式。
可以通过redis的配置文件进行设置,让其在“N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动进行数据集保存操作。

  1. # RDB自动持久化规则
  2. # 当 900 秒内有至少有 1 个键被改动时,自动进行数据集保存操作
  3. save 900 1
  4. # 当 300 秒内有至少有 10 个键被改动时,自动进行数据集保存操作
  5. save 300 10
  6. # 当 60 秒内有至少有 10000 个键被改动时,自动进行数据集保存操作
  7. save 60 10000
  8. # RDB持久化文件名
  9. dbfilename dump-<port>.rdb
  10. # 数据持久化文件存储目录
  11. dir /var/lib/redis
  12. # bgsave发生错误时是否停止写入,通常为yes
  13. stop-writes-on-bgsave-error yes
  14. # rdb文件是否使用压缩格式
  15. rdbcompression yes
  16. # 是否对rdb文件进行校验和检验,通常为yes
  17. rdbchecksum yes

RDB的优点

  1. 整个redis数据库只保存在一个文件中,非常方便进行备份。比如你可以设置每天保存过去30天的数据,即使出了问题也可以根据需求恢复到不同版本的数据集。
  2. 方便备份,很容易将一个一个RDB文件移动到其他存储介质上
  3. RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
  4. RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。

    RDB的缺点

  5. 不可控、丢失数据。如果你希望在redis意外停止工作(例如电源中断)的情况下丢失的数据最少的话,那么RDB不适合你。虽然你可以配置不同的save时间点(例如每隔5分钟并且对数据集有100个写的操作),是Redis要完整的保存整个数据集是一个比较繁重的工作,你通常会每隔5分钟或者更久做一次完整的保存,万一在Redis意外宕机,你可能会丢失几分钟的数据。

  6. 耗时、耗性能。RDB 需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候,fork的过程是非常耗时的,可能会导致Redis在一些毫秒级内不能响应客户端的请求。如果数据集巨大并且CPU性能不是很好的情况下,这种情况会持续1秒,AOF也需要fork,但是你可以调节重写日志文件的频率来提高数据集的耐久度。

    AOF持久化

    redis会将每一个收到的写命令都通过write函数追加到文件中(默认是 appendonly.aof)。
    配置文件中打开AOF方式:appendonly yes
    打开AOF持久化后,当 redis 执行改变数据集的命令(如:set)时,这个命令就会被追加到AOF文件的末尾。当redis 重启后,程序可以通过执行 AOF 文件中的命令来在内存中重建数据集。

    AOF持久化的三种策略

    1. appendonly yes // 启用aof持久化方式
    2. # appendfsync always // 每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全的持久化,不推荐使用
    3. appendfsync everysec // 每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐
    4. # appendfsync no // 完全依赖os,性能最好,持久化没保证

    1. always

    每次有新的写入命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全。

    2. everysec

    每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。
    推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。

    3. no

    从不 fsync :将数据交给操作系统来处理,由操作系统来决定什么时候同步数据。更快,也更不安全的选择。
命令 优点 缺点
always 不丢失数据 IO开销大,一般SATA磁盘只有几百TPS
everysec 每秒进行与fsync,最多丢失1秒数据 可能丢失1秒数据
no 不用管 不可控

AOF重写

作用:①减少磁盘占用量;②加速数据恢复

因为AOF持久化方式时不断将命令追加到文件尾部,随着写入的命令不断增加,AOF文件的体积也会变得越来越大。
为了处理这个问题,redis 支持一种有趣的特性:在不打断客户端连接的情况下,对AOF文件进行重建。手动执行bgrewriteaof命令,redis 将生成一个新的AOF文件,该文件包含了重建当前数据集所需要的最少命令。
Redis 2.2 需要自己手动执行 bgrewriteaof 命令; Redis 2.4 则可以通过配置自动触发 AOF 重写。

AOF相关配置

  1. # 开启AOF持久化方式
  2. appendonly yes
  3. # AOF持久化文件名
  4. appendfilename appendonly-<port>.aof
  5. # 每秒把缓冲区的数据同步到磁盘
  6. appendfsync everysec
  7. # 数据持久化文件存储目录
  8. dir /var/lib/redis
  9. # 是否在执行重写时不同步数据到AOF文件
  10. # 这里的 yes,就是执行重写时不同步数据到AOF文件
  11. no-appendfsync-on-rewrite yes
  12. # 触发AOF文件执行重写的最小尺寸
  13. auto-aof-rewrite-min-size 64mb
  14. # 触发AOF文件执行重写的增长率
  15. auto-aof-rewrite-percentage 100
配置名 含义
auto-aof-rewrite-min-size 触发AOF文件执行重写的最小尺寸
auto-aof-rewrite-percentage 触发AOF文件执行重写的增长率
统计名 含义
aof_current_size AOF文件当前尺寸(字节)
aof_base_size AOF文件上次启动和重写时的尺寸(字节)

AOF重写自动触发机制,需要同时满足下面两个条件:

  1. aof_current_size > auto-aof-rewrite-min-size (AOF文件当前字节 > 触发AOF文件执行重写的最小尺寸)
  2. (aof_current_size - aof_base_size) * 100 / aof_base_size > auto-aof-rewrite-percentage (增长率 > 出发AOF重写的增长率)

    AOF的优点

  3. 使用AOF 会让你的Redis更加耐久: 你可以使用不同的fsync策略:无fsync,每秒fsync,每次写的时候fsync。使用默认的每秒fsync策略,Redis的性能依然很好(fsync是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失1秒的数据。

  4. AOF文件是一个只进行追加的日志文件,所以不需要写入seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用redis-check-aof工具修复这些问题。
  5. Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
  6. AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

    AOF的缺点

  7. 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。

  8. 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。

    淘汰机制

    当 redis 内存超出物理内存时,内存的数据开始和磁盘产生频繁的交换,redis 的性能急剧下降。在生产环境中,时不允许出现交换行为的, 为了限制使用最大内存,redis 配置文件提供 maxmemory参数来内存超出期望大小,当实际内存超出maxmemory时,redis 会根据淘汰机制腾出新空间。
    Redis基础 - 图1

  9. noeviction:redis 默认的淘汰策略,此策略不会淘汰数据,当内存不足写入新数据时,直接报异常,redis只响应读操作。

  10. volatile-lru:对设置了过期时间的键值对,使用 LRU 算法淘汰数据。
  11. volatile-lfu:对设置了过期时间的键值对,使用 LFU 算法淘汰数据。
  12. volatile-ttl:对设置了过期时间的键值对,根据过期时间的先后顺序,越早过期的越先被淘汰。
  13. volatile-random:对设置了过期时间的键值对,进行随机淘汰。
  14. allkeys-lru:对所有数据使用 LRU 算法淘汰。
  15. allkeys-lfu:对所有数据使用 LFU 算法淘汰。
  16. allkeys-random:对所有数据随机淘汰。

显然,volatile-xxx策略只会针对带过期时间的key进行淘汰,而allkeys-xxx策略会对所有的key进行淘汰。

LRU 算法

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

redis 会记录每个数据最近一次访问时间戳(由键值对数据结构RedisObject中的lru字段记录),在淘汰数据时,redis 会随机取出 N 个数据,把它们放在一个候选结合里,然后比较这 N 个数据的 lru 字段,把 lru
字段值最小的数据淘汰。
redis 提供了一个配置参数maxmemory-samples,这个参数就是 redis 选出来的数据个数 N 。

  1. config set maxmemory-samples 100

当需要再次淘汰数据时,redis 需要挑选数据进入第一次淘汰时创建的候选集合。挑选标准是:能进入候选集合的数据的 lru 字段值必须小于候选集合中最小的 lru 值。当有新数据进入候选数据集,候选数据集中的数据个数达到 maxmemory-samples,redis 就把候选数据集中 lru 字段值最小的数据淘汰。
这样一来,redis 缓存不用为所有的数据维护一个大链表,也不用在每次数据访问时都移动链表项,提升了缓存的性能。
LRU算法有一个问题就是,当应用对大量的数据进行一次全体读取,每个数据都会被读取,而且只会被读取一次。此时,因为这些被查询的数据刚刚被访问过,所以 lru 字段值都很大。而其他的热点数据就会被淘汰掉。

LFU 算法

Redis 从 4.0 版本开始增加了 LFU 淘汰策略。与 LRU 策略相比,LFU 策略中会从两个维度来筛选并淘汰数据:
1、数据访问的时效性(访问时间离当前时间的远近)
2、数据的被访问次数。
LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。
应用对大量的数据进行一次全体读取,因为这些数据不会被再次访问,所以它们的访问次数不会再增加。因此,LFU 策略会优先把这些访问次数低的数据淘汰出缓存。就解决了上面提到的LRU存在的问题。

TTL 算法

Redis 数据集数据结构中保存了键值对过期时间的表,即 redisDb.expires。与 LRU 数据淘汰机制类似,TTL 数据淘汰机制中会先从过期时间的表中随机挑选几个键值对,取出其中 ttl 比较小的键值对淘汰。同样,TTL淘汰策略并不是面向所有过期时间的表中最快过期的键值对,而只是随机挑选的几个键值对。