持久化

  • AOF 文件的内容是操作命令;
  • RDB 文件的内容是二进制数据
  • AOF

    • Redis 中 AOF 持久化功能默认是不开启的,需要我们修改 redis.conf

    • Redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里

      这保存写操作命令到日志的持久化方式,是 Redis 里的 AOF(Append Only File) 持久化功能,注意只会记录写操作命令,读操作命令是不会被记录的

    • Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的

      • 避免额外的检查开销
      • 不会阻塞当前写操作命令的执行,因为当写操作命令执行成功后,才会将命令记录到 AOF 日志
    • 风险:

      • 执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险
      • 写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前写操作命令的执行,但是可能会给「下一个」命令带来阻塞风险
    • 执行过程:

      • Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区;
      • 通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘;
      • 具体内核缓冲区的数据什么时候写入到硬盘,由内核决定
        • 写回硬盘策略:
          • Always,「总是」,每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
          • Everysec,「每秒」,每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,每隔一秒将缓冲区里的内容写回到硬盘;
          • No,不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘
    • AOF重写机制

      • AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大 Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制
      • 机制是在重写时,读取当前数据库中的所有键值对,将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件
      • 妙处在于:尽管某个键值对被多条写命令反复修改,最终也只需要根据这个「键值对」当前的最新状态,然后用一条命令去记录键值对
    • AOF后台重写

      • 写入 AOF 日志的操作是在主进程完成的,因为写入的内容不多,不太影响命令操作
      • Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的
        • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,避免阻塞主进程
        • 子进程带有主进程的数据副本
      • 重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」
        • AOF 缓冲区:用于临时存储写命令,定期或根据策略写入 AOF 文件。
        • AOF 重写缓冲区:用于在 AOF 重写过程中临时存储新的写命令,确保重写期间的数据不丢失
  • RDB

    • Redis 提供两个命令生成 RDB 文件,分别是 savebgsave,区别是否在「主线程」里执行:

      • 执行了 save 命令,会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
      • 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,可以避免主线程的阻塞
    • Redis 通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令

      • save 900 1
        save 300 10
        save 60 10000
        

        满足上面条件的任意一个,就会执行 bgsave,它们的意思分别是:

        • 900 秒之内,对数据库进行了至少 1 次修改;
        • 300 秒之内,对数据库进行了至少 10 次修改;
        • 60 秒之内,对数据库进行了至少 10000 次修改
    • Redis 快照是全量快照,每次执行快照,是把内存中的「所有数据」都记录到磁盘中

      RDB 快照的缺点,在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多

    • 执行快照,数据能被修改吗

      • 执行 bgsave 过程,Redis 依然可以继续处理操作命令的,也就是数据是能被修改
      • 主线程(父进程)要修改共享数据里的某一块数据(比如键值对 A)时,就会发生写时复制,于是这块数据的物理内存就会被复制一份(键值对 A',然后主线程在这个数据副本(键值对 A')进行修改操作。与此同时,bgsave 子进程可以继续把原来的数据(键值对 A)写入到 RDB 文件
      • bgsave 快照过程中,主线程修改了共享数据,发生了写时复制后,RDB 快照保存的是原本的内存数据,而主线程刚修改的数据,是没办法在这一时间写入 RDB 文件的,只能交由下一次的 bgsave 快照
  • RDB和AOF合体

    • RDB 恢复速度快的优点和有 AOF 丢失数据少的优点

    • 开启混合持久化功能,可以在 Redis 配置文件将下面这个配置项设置成 yes:

      aof-use-rdb-preamble yes
      
    • AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件

      使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据

  • Redis大key

    • 对AOF持久化影响
      • 当使用 Always 策略的时候,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的
    • 对AOF重写和RDB影响
      • 都会分别通过 fork() 函数创建子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程)
        • 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
        • 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程
    • 其他影响
      • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,很久很久都没有响应。
      • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
      • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
      • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多
    • 避免:
      • 设计阶段,就把大 key 拆分成一个一个小 key

删除和淘汰策略

  • 过期删除策略

    • Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除

    • 设置过期时间

      • expire <key> <n>:设置 key 在 n 秒后过期

        设置 key 在 100 秒后过期;

        expire key 100 
        
      • pexpire <key> <n>:设置 key 在 n 毫秒后过期

        设置 key2 在 100000 毫秒(100 秒)后过期。

        pexpire key2 100000
        
      • expireat <key> <n>:设置 key 在某个时间戳(精确到秒)之后过期

        key3 在时间戳 1655654400 后过期(精确到秒);

        expireat key3 1655654400 
        
      • pexpireat <key> <n>:设置 key 在某个时间戳(精确到毫秒)之后过期

        key4 在时间戳 1655654400000 后过期(精确到毫秒)

        pexpireat key4 1655654400000
        

        在设置字符串时,同时对 key 设置过期时间

        • set <key> <value> ex <n> :设置键值对的时候,同时指定过期时间(精确到秒);
        • set <key> <value> px <n> :设置键值对的时候,同时指定过期时间(精确到毫秒);
        • setex <key> <n> <valule> :设置键值对的时候,同时指定过期时间(精确到秒)
      • 查看某个 key 剩余的存活时间,可以使用 TTL <key> 命令

        # 设置键值对的时候,同时指定过期时间位 60 秒
        > setex key1 60 value1
        OK
        
        # 查看 key1 过期时间还剩多少
        > ttl key1
        (integer) 56
        > ttl key1
        (integer) 52
        
      • 取消 key 的过期时间,使用 PERSIST <key> 命令。

        # 取消 key1 的过期时间
        > persist key1
        (integer) 1
        
        # 使用完 persist 命令之后,
        # 查下 key1 的存活时间结果是 -1,表明 key1 永不过期 
        > ttl key1 
        (integer) -1
        
    • 常见三种过期删除策略,并不是redis会用

      • 定时删除:
        • 在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作
        • 优点:
          • 保证过期 key 会被尽快删除,内存可以被尽快地释放
        • 缺点:
          • 过期 key 比较多的情况下,删除过期 key 可能会占用相当一部分 CPU 时间
      • 惰性删除:
        • 不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key
        • 优点:
          • 每次访问时,才会检查 key 是否过期,只会使用很少的系统资源
        • 缺点:
          • 如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费
      • 定期删除:
        • 每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key
        • 优点:
          • 通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用
        • 缺点:
          • 内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少
    • Redis过期删除

      • Redis 选择「惰性删除+定期删除」这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡

      • Redis 实现惰性删除

        • int expireIfNeeded(redisDb *db, robj *key) {
              // 判断 key 是否过期
              if (!keyIsExpired(db,key)) return 0;
              ....
              /* 删除过期键 */
              ....
              // 如果 server.lazyfree_lazy_expire 为 1 表示异步删除,反之同步删除;
              return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
          dbSyncDelete(db,key);
          }
          
        • Redis 在访问或者修改 key 之前,都会调用 expireIfNeeded 函数对其进行检查,检查 key 是否过期:

          • 如果过期,则删除该 key,至于选择异步删除,还是选择同步删除,根据 lazyfree_lazy_expire 参数配置决定(Redis 4.0版本开始提供参数),然后返回 null 客户端;
          • 如果没有过期,不做任何处理,然后返回正常的键值对给客户端
      • Redis实现定期删除

        • do {
              //已过期的数量
              expired = 0
              //随机抽取的数量
              num = 20;
              while (num--) {
                  //1. 从过期字典中随机抽取 1 个 key
                  //2. 判断该 key 是否过期,如果已过期则进行删除,同时对 expired++
              }
          
              // 超过时间限制则退出
              if (timelimit_exit) return;
          
            /* 如果本轮检查的已过期 key 的数量,超过 25%,则继续随机抽查,否则退出本轮检查 */
          } while (expired > 20/4); 
          
        • 每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key

          • 在 Redis 中,默认每秒进行 10 次过期检查一次数据库
            • 可通过 Redis 的配置文件 redis.conf 进行配置
          • 数据库每轮抽查时,会随机选择 20 个 key 判断是否过期
  • 内存淘汰策略

    配置文件 redis.conf 中,可以通过参数 maxmemory <bytes> 来设定最大运行内存,只有在 Redis 的运行内存达到了我们设置的最大运行内存,才会触发内存淘汰策略

    • 不进行数据淘汰的策略

      • noeviction(Redis3.0之后,默认的内存淘汰策略) :当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,会报错通知禁止写入
    • 进行数据淘汰的策略

      • 在设置了过期时间的数据中进行淘汰:
        • volatile-random:随机淘汰设置了过期时间的任意键值;
        • volatile-ttl:优先淘汰更早过期的键值。
        • volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
        • volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
      • 在所有数据范围内进行淘汰:
        • allkeys-random:随机淘汰任意键值;
        • allkeys-lru:淘汰整个键值中最久未使用的键值;
        • allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值
    • 查看redis使用的内存淘汰策略

      • 使用 config get maxmemory-policy ,查看当前 Redis 的内存淘汰策略 :

        127.0.0.1:6379> config get maxmemory-policy
        1) "maxmemory-policy"
        2) "noeviction"
        
  • LRU算法和LFU算法

    • LRU
      • Least Recently Used 翻译为最近最少使用,会选择淘汰最近最少使用的数据
      • 传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素
    • LFU
    • Least Frequently Used 翻译为最近最不常用,LFU 算法是根据数据访问次数来淘汰数据的,核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”

缓存异常

Redis 是内存数据库,我们可以将数据库的数据缓存在 Redis 里,相当于数据缓存在内存,内存的读写速度比硬盘快好几个数量级,这样大大提高了系统性能

在业务刚上线的时候,我们最好提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制刚好也适合干这个事情

  • 缓存雪崩

    • 为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存
    • 大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,就是缓存雪崩
    • 原因及解决方案:
      • 大量数据同时过期
        • 均匀设置过期时间
          • 给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间,对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数
        • 互斥锁:
          • 当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存
          • 实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象
        • 后台更新缓存:
          • 让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新 缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为当系统内存紧张的时候,有些缓存数据会被“淘汰”
      • Redis故障宕机:
        • 服务熔断或请求限流机制
          • 启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误
          • 为了减少对业务的影响,我们可以启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制
        • 构建 Redis 缓存高可靠集群
          • 主从节点的方式构建 Redis 缓存高可靠集群
  • 缓存击穿

    • 业务通常会有几个数据被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据
    • 缓存中的某个热点数据过期,大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,缓存击穿
    • 解决方案:
      • 互斥锁方案:
        • 保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值
      • 不给热点数据设置过期时间:
        • 后台异步更新缓存
        • 在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间
  • 缓存穿透

    • 当用户访问的数据,既不在缓存中,也不在数据库中,就是缓存穿透的问题

    • 发送原因

      • 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
      • 黑客恶意攻击,故意大量访问某些读取不存在数据的业务
    • 解决方案:

      • 非法请求的限制
        • API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误
      • 缓存空值或者默认值
        • 可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用
      • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:
    • 布隆过滤器工作过程:

      • 布隆过滤器由「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。

        布隆过滤器通过 3 个操作完成标记:

        1. 使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值;

        2. 将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。

        3. 将每个哈希值在位图数组的对应位置的值设置为 1;

数据库缓存一致性