Redis 基本数据类型、高性能、数据一致性、过期策略、淘汰策略、持久化、缓存穿透、缓存击穿、缓存雪崩、缓存预热

114 阅读10分钟

前言

Redis 常用命令大全

Redis 一般都用来当作缓存使用,大部分 key 都会设置过期时间。

Redis 中有一个过期字典用来保存所有 key 的过期时间。当有查询请求进来会检查该 key 是否存在于过期字典中:

  • 不在: 正常读取键值。
  • 在:获取并判断该 key 的过期时间是否过期。若是已经过期则结束,若是还未过期则正常键值。

Redis 执行过程

  1. 客户端输入指令。
  2. 指令转为 redis 协议,发送给服务端。
  3. 服务端接收到协议后将其转为命令。
  4. 判断命令、授权信息。
  5. 执行命令并记录相关信息和数据统计。

基本数据类型及其应用场景

string - 字符串

  • 应用场景

    • key-value 缓存,一个键最大能存储512MB。
    • 数值计算。
  • 内部数据类型

    • int:数字。
    • embstr:字符串。
    • raw:当字符串长度大于44字节时,会变为 raw 类型存储。
  • 示例

    127.0.0.1:6379> set name xyc_neil
    OK
    127.0.0.1:6379> get name
    "xyc_neil"
    

hash - 哈希

  • 应用场景

    • 用户信息
    • 商品详情
  • 内部数据类型

    • hashmap
      • 存储
        • 先将键值进行 hash 计算,得到存储键值对应的数组索引,再根据数组索引进行数据存储,有小概率会出现完全不相同的键值进行 hash 计算后,得到相同的 hash 值,这种情况称为 hash 冲突。
        • hash 冲突一般通过链表的形式解决,相同的 hash 值会对应一个链表结构,每次 hash 冲突时,就把新元素插入链表的尾部。
      • 查询
        • 通过 hash 获得数组的索引值,根据索引值找到对应的元素。
        • 判断元素和查找的键值是否相等,相等则成功返回数据,否则需要查看 next 指针是否还有对应其他元素,如果没有,则返回 null,如果有 nest 指针的话,重复此步骤。
  • 示例:

    127.0.0.1:6379> hset userId name xyc_neil
    (integer) 1
    127.0.0.1:6379> hgetall userId
    1) "name"
    2) "xyc_neil"
    

list - 列表

  • 应用场景

    • 消息队列
    • 限流器
  • 内部数据类型

    • quicklist:快速列表是一个双向链表,双向链表中的每个节点是一个 ziplist(压缩表),在插入方向的头节点判断是否能插入,不能的话就新增一个 quicklistNode(快速列表节点)。
  • 示例

    127.0.0.1:6379> lpush list xyc_neil
    (integer) 1
    127.0.0.1:6379> lpop list
    "xyc_neil"
    

set - 集合

  • 应用场景

    • 微博关注
    • 抽奖信息
  • 内部数据类型

    • intset:元素都是整数的时候
    • hashtable
      • 元素超过一定个数的时候,默认是512个
      • 元素非整数的时候
  • 示例

    127.0.0.1:6379> sadd set xyc_neil
    (integer) 1
    127.0.0.1:6379> spop set
    "xyc_neil"
    

zset - 有序集合

  • 应用场景

    • 成绩排名
    • 热词排序
  • 内部数据类型

    • ziplist
      • 保存的元素个数小于128个
      • 元素的长度都小于64字节
    • skiplist
  • 示例

    127.0.0.1:6379> zadd zset 100 xyc_neil 60 zhangsan 80 lisi
    (integer) 3
    127.0.0.1:6379> zrange zset 0 -1 withscores
    1) "zhangsan"
    2) "60"
    3) "lisi"
    4) "80"
    5) "xyc_neil"
    6) "100"
    

高性能

  • 完全基于内存。
  • 整个结构类似于HashMap,查找和操作复杂度为O(1)
  • 读写模型是单线程的,避免了多线程的上下文切换和线程竞争造成的开销。
  • 采用多路复用的高效非阻塞IO模型

数据一致性

  • 场景:并发更新导致数据库与缓存不一致。
  • 解决:
    • 延迟双删:先删除缓存,更再新数据库,过一会再删除一遍缓存。

过期策略

定时删除

  • 概念:在给 key 设置过期时间的同时,设置一个定时器,当 key 过期了,定时器马上把该 key 删除。
  • 优点:内存空间回收非常快。
  • 缺点:对 CPU 不友好。

惰性删除

  • 概念:当有请求操作 key 的时候,才检查这个 key 是否过期,如果过期则删除(可以配置异步删除或者同步删除),否则返回对应的数据。
  • 优点:对 CPU 比较友好。
  • 缺点:内存空间浪费。

定期删除

  • 概念:每隔一段时间随机取出一些设置过期时间的 key 进行检查和删除。如果本轮已过期数量过多,则继续重复步骤,如果已过期数量较少,则停止。定期删除循环流程的时间上限25ms。
  • 定期删除是定时删除和惰性删除的一个折中方案,减少CPU占用、内存也不会浪费太多。

Redis 默认采用定期删除 + 惰性删除。

淘汰策略

不淘汰数据

  • no-enviction:禁止驱逐数据。当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,采用no-enviction策略可以保证数据不被丢失。

淘汰数据

  • 在设置过期时间的数据中进行淘汰

    • volatile-random:随机淘汰设置了过期时间的任意键值。
    • volatile-ttl:优先淘汰更早过期的键值。
    • volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值。
    • volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值。
  • 在所有数据中进行淘汰

    • allkeys-random:随机淘汰任意键值。
    • allkeys-lru:淘汰整个键值中最久未使用的键值。
    • allkeys-lfu:淘汰整个键值中最少使用的键值。

Redis3.0之前默认采用volatile-lru,Redis3.0之后默认采用no-enviction。

持久化

RDB 持久化

  • 概念:快照方式,将某一个时刻的内存数据,以二进制的方式写入磁盘。

  • 触发方式:

    • 自动触发:
      • 配置 sava m n,以下是默认开启:
        • save 900 1  -- 900s内存在1个写操作。
        • save 300 10  -- 300s内存在10个写操作。
        • save 60 10000 -- 60s内存在10000个写操作。
      • 主从同步的时候会自动触发 bgsave
    • 手动触发:
      • save:阻塞当前 Redis,直到 RDB 持久化过程完成为止,若内存比较大则会造成长时间阻塞。
      • bgsave:Redis 进程执行 fork 操作创建子进程,由子进程完成持久化,阻塞时间很短。
  • 优点:

    • 使用单独的子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能。
    • RDB 文件存储的是压缩的二进制文件,占用内存更小,更适合作为备份文件、灾难恢复,同时 RDB 文件的加载速度远超于 AOF 文件。
  • 缺点:

    • RDB 是间隔一段时间进行持久化,如果 Redis 服务意外挂掉了,则会丢失一段时间内的数据。
    • 每次都要创建子进程,频繁创建成本过高,甚至可能导致 Redis 短暂的停止服务,备份时需要将数据写入到一个临时文件,内存是原来的两倍。
    • RDB 文件可读性很差。

AOF 持久化

  • 概念:文件追加方式,记录所有的操作命令,并以文本的形式追加到文件中。

  • 触发方式:

    • 自动触发:
      • always:每条命令都写入磁盘,最多丢失一条数据。
      • everysec:每秒钟写入一次磁盘,最多丢失一秒的数据。
      • no:不设置写入磁盘规则,采用默认30s写入一次磁盘。
    • 手动触发:
      • 执行bgrewriteaof命令。
  • 优点:

    • 保存的数据更加完整,AOF 普遍采用每秒保存一次的策略,即使发生了意外情况,最多只会丢失1s的数据。
    • AOF 采用的是命令追加的写入方式,所以不会出现文件损坏的问题。
    • AOF 文件可读性很好,它是把所有Redis键值操作命令,以文件的方式写入了磁盘。即使不小心使用 flushall 命令删除了所有键值信息,只要在 AOF 文件删除最后的 flushall 命令,重启Redis即可恢复之前误删的数据。
  • 缺点:

    • 对于相同的数据集来说,AOF 文件要大于 RDB 文件,数据恢复时也需要重新执行指令,在重启时恢复数据的时间往往会慢很多。
    • 高并发场景下,AOF 性能会比较差。
  • AOF 机制重写:当 AOF 文件比上一次重写时的文件大小增长100%并且文件大小不小于64MB时会对整个 AOF 文件进行重写。基于 copy-on-write (写时复制),全量遍历内存中数据,然后逐个序列到 AOF 文件中。重写过程中,对于新的变更操作将仍然被写入到原 AOF文件中,同时这些新的变更操作也会被 收集起来,当内存数据被全部写入到新的 AOF 文件之后,收集的新的变更操作也将会一并追加到新的 AOF 文件中,此后将会重命名新的 AOF 文件为 appendonly.aof, 此后所有的操作都将被写入新的 AOF 文件。如果在重写过程中,出现故障,将不会影响原 AOF 文件的正常工作,可以通过 bgrewriteaof 指令人工干预。

混合型持久化

  • 概念:Redis4.x之后新增的方式,结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的持久化数据以 RDB 的形式写入文件的开头,再将后续的操作命令以AOF的格式存入文件,这样既能保证 Redis 重启时的速度,又能降低数据丢失的风险。

  • 优点:

    • 结合了 RDB 和 AOF 持久化的优点,开头为RDB格式,使得Redis可以快速重启,同时结合AOF的优点,降低了大量数据丢失的风险。
  • 缺点:

    • AOF 文件添加了 RDB 格式的内容,使得 AOF 文件的可读性变差。
    • 兼容性差,如果开启混合存储,那么此混合存储文件,就不能用在Redis4.x之前的版本。

缓存穿透

  • 场景:大量用户访问的数据,既不在缓存中,也不在数据库中。
    • 业务误操作:缓存中的数据和数据库中的数据都被误删除。
    • 黑客恶意攻击:故意大量访问某些读取不存在数据的业务。
  • 解决:
    • 做 IP 限流与黑名单。
    • 缓存空值或者默认值。
    • 使用布隆过滤器。
      • 只能添加不能删除,想删除的话可以给该 key 设置标识,查到该标识就认为是没有缓存。
      • bitmap 结构有极少概率出现不存在的 key 也命中。

缓存击穿

  • 场景:热点 key 过期,DB 有数据,大量请求同时到达 DB。
  • 解决:
    • 热点 key 不设置过期时间,由后台更新缓存。
    • 获取数据串行化:
      1. 查找缓存。
      2. 缓存命中就返回,否则抢锁。
      3. 抢锁不成功的 sleep,抢到锁的操作 DB,并写入缓存。
      4. sleep 后的请求重复以上步骤。

缓存雪崩

  • 场景:大量热点 key 同时过期,DB 有数据,大量请求同时到达 DB。
  • 解决:
    • 热点 key 的过期时间随机错开。
    • 热点 key 不设置过期时间,由后台更新缓存。
    • 获取数据串行化:
      1. 查找缓存。
      2. 缓存命中就返回,否则抢锁。
      3. 抢锁不成功的 sleep,抢到锁的操作 DB,并写入缓存。
      4. sleep 后的请求重复以上步骤。

缓存预热

  • 场景:缓存预热并不是一个问题,而是使用缓存时的一个优化方案,它可以提高前台用户的使用体验。缓存预热指的是在系统启动的时候,先把查询结果预存到缓存中,以便用户后面查询时可以直接从缓存中读取,以节约用户的等待时间。
  • 解决:
    • 把需要缓存的方法写在系统初始化的方法中,这样系统在启动的时候就会自动的加载数据并缓存数据。
    • 把需要缓存的方法挂载到某个接口上,手动触发缓存预热。
    • 设置定时任务,定时自动进行缓存预热。