Redis

文章目录


1 Redis架构——特点、数据结构、内部编码

2 Redis数据结构(简介-命令-内部编码-应用场景)

3 键、数据库管理——键重命名、键过期、键迁移、切换 / 清除数据库

4 Redis持久化——RDB、AOF

5 Redis 数据淘汰策略

# 重点补充——高效性、单线程、一致性、缓存击穿 / 穿透 / 雪崩

---------------------------------------------------------------------------------


1 Redis架构

1.1 Redis 特点

  • 基于键值对的数据结构服务器

    Redis 中的值不仅可以是字符串,还可以是具体的数据结构,这样不仅能应用于多种场景开发,也可以提高开发效率。它主要提供五种数据结构:字符串、哈希、列表、集合、有序集合,同时在字符串的基础上演变出了 Bitmaps 和 HyperLogLog 两种数据结构,Redis 3.2 还加入了有关 GEO 地理信息定位的功能。

  • 丰富的功能

    ① 提供了键过期功能,可以实现缓存
    ② 提供了发布订阅功能,可以实现消息系统
    ③ 支持 Lua 脚本,可以创造新的 Redis 命令。
    ④ 提供了简单的事务功能,能在一定程度上保证事务特性。
    ⑤ 提供了流水线功能,客户端能将一批命令一次性传到 Redis,减少网络开销。

  • 简单稳定

    Redis 的简单主要体现在三个方面:
    源码很少,早期只有 2 万行左右,在 3.0 版本由于添加了集群特性,增加到了 5 万行左右,相对于很多 NoSQL 数据库来说代码量要少很多。
    ② 采用单线程模型,使得服务端处理模型更简单,也使客户端开发更简单。
    不依赖底层操作系统的类库,自己实现了事件处理的相关功能。虽然 Redis 比较简单,但也很稳定。

  • 客户端语言多

    Redis 提供了简单的 TCP 通信协议,很多编程语言可以方便地接入 Redis,例如 Java、PHP、Python、C、C++ 等。

  • 持久化

    通常来说数据放在内存中是不安全的,一旦发生断电或故障数据就可能丢失,因此 Redis 提供了两种持久化方式 RDB 和 AOF 将内存的数据保存到硬盘中

  • 高性能

    Redis 使用了单线程架构和 IO 多路复用模型来实现高性能的内存数据库服务。

    每次客户端调用都经历了发送命令、执行命令、返回结果三个过程,因为 Redis 是单线程处理命令的,所以一条命令从客户端到达服务器不会立即执行,所有命令都会进入一个队列中,然后逐个被执行客户端的执行顺序可能不确定,但是可以确定不会有两条命令被同时执行,不存在并发问题

    通常来说单线程处理能力要比多线程差,Redis 快的原因:
    ① 纯内存访问,Redis 将所有数据放在内存中。
    ② 非阻塞 IO,Redis 使用 epoll 作为 IO 多路复用技术的实现,再加上 Redis 本身的事件处理模型将 epoll 中的连接、读写、关闭都转换为时间,不在网络 IO 上浪费过多的时间。
    ③ 单线程避免了线程切换和竞争产生的消耗。单线程的一个问题是对于每个命令的执行时间是有要求的,如果某个命令执行时间过长会造成其他命令的阻塞,对于 Redis 这种高性能服务来说是致命的,因此 Redis 是面向快速执行场景的数据库。


1.2 Redis 数据结构

可以使用 type 命令查看当前键的数据类型结构,它们分别是:string、hash、list、set、zset,但这些只是 Redis 对外的数据结构。实际上每种数据结构都有自己底层的内部编码实现,这样 Redis 会在合适的场景选择合适的内部编码,string 包括了 raw、int 和 embstr,hash 包括了 hashtable 和 ziplist,list 包括了 linkedlist 和 ziplist,set 包括了 hashtable 和 intset,zset 包括了 skiplist 和 ziplist。可以使用 object encoding 查看内部编码。


1.3 Redis 为什么要使用内部编码?——不影响外部的条件下,多种内部编码适应不同场景

可以改进内部编码,而对外的数据结构和命令没有影响

多种内部编码实现可以在不同场景下发挥各自的优势,例如 ziplist 比较节省内存,但在列表元素较多的情况下性能有所下降,这时 Redis 会根据配置选项将列表类型的内部实现转换为 linkedlist。


2 Redis数据结构(简介-命令-内部编码-应用场景)

2.1 string 4

  • Q1:string 简介

    字符串类型是 Redis 最基础的数据结构,键都是字符串类型,而且其他几种数据结构都是在字符串类型的基础上构建的。字符串类型的值可以实际可以是字符串(简单的字符串、复杂的字符串如 JSON、XML)、数字(整形、浮点数)、甚至二进制(图片、音频、视频),但是值最大不能超过 512 MB。


  • Q2:string 命令

    设置值

    set key value [ex seconds] [px millseconds] [nx|xx]
    
    • ex seconds:为键设置秒级过期时间,跟 setex 效果一样
    • px millseconds:为键设置毫秒级过期时间
    • nx:键必须不存在才可以设置成功,用于添加,跟 setnx 效果一样。由于 Redis 的单线程命令处理机制,如果多个客户端同时执行,则只有一个客户端能设置成功,可以用作分布式锁的一种实现。
    • xx:键必须存在才可以设置成功,用于更新

    获取值

    get key,如果不存在返回 nil

    批量设置值

    mset key value [key value...]
    

    批量获取值

    mget key [key...]
    

    批量操作命令可以有效提高开发效率,假如没有 mget,执行 n 次 get 命令需要 n 次网络时间 + n 次命令时间,使用 mget 只需要 1 次网络时间 + n 次命令时间。Redis 可以支持每秒数万的读写操作,但这指的是 Redis 服务端的处理能力,对于客户端来说一次命令处理命令时间还有网络时间。因为 Redis 的处理能力已足够高,对于开发者来说,网络可能会成为性能瓶颈。

    计数

    incr key
    

    incr 命令用于对值做自增操作,返回结果分为三种:① 值不是整数返回错误。② 值是整数,返回自增后的结果。③ 值不存在,按照值为 0 自增,返回结果 1。除了 incr 命令,还有自减 decr、自增指定数字 incrby、自减指定数组 decrby、自增浮点数 incrbyfloat。


  • Q3:string 内部编码

    - int8 个字节的长整形 
    - embstr:小于等于 39 个字节的字符串 
    - raw:大于 39 个字节的字符串 
    

  • Q4:string 应用场景

    缓存功能

    Redis 作为缓存层,MySQL 作为存储层,首先从 Redis 获取数据,如果失败就从 MySQL 获取并将结果写回 Redis 并添加过期时间。

    计数

    Redis 可以实现快速计数功能,例如视频每播放一次就用 incy 把播放数加 1。

    共享 Session

    一个分布式 Web 服务将用户的 Session 信息保存在各自服务器,但会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问负载到不同服务器上,用户刷新一次可能会发现需要重新登陆。为解决该问题,可以使用 Redis 将用户的 Session 进行集中管理,在这种模式下只要保证 Redis 是高可用和扩展性的,每次用户更新或查询登录信息都直接从 Redis 集中获取。

    限速

    例如为了短信接口不被频繁访问会限制用户每分钟获取验证码的次数或者网站限制一个 IP 地址不能在一秒内访问超过 n 次。可以使用键过期策略和自增计数实现。


2.2 hash 4

  • Q1:hash 简介

    哈希类型指键值本身又是一个键值对结构,哈希类型中的映射关系叫 field-value,这里的 value 是指 field 对于的值而不是键对于的值。


  • Q2:hash 命令

    设置值

    hset key field value,如果设置成功会返回 1,反之会返回 0,此外还提供了 hsetnx 命令,作用和 setnx 类似,只是作用于由键变为 field。

    获取值

    hget key field,如果不存在会返回 nil。

    删除 field

    hdel key field [field...],会删除一个或多个 field,返回结果为删除成功 field 的个数。

    计算 field 个数

    hlen key
    

    批量设置或获取 field-value

    hmget key field [field...] 
    hmset key field value [field value...]
    

    判断 field 是否存在

    hexists key field,存在返回 1,否则返回 0。

    获取所有的 field

    hkeys key,返回指定哈希键的所有 field。

    获取所有 value

    hvals key,获取指定键的所有 value。

    获取所有的 field-value

    hgetall key,获取指定键的所有 field-value。


  • Q3:hash 内部编码

    ziplist 压缩列表:当哈希类型元素个数和值小于配置值(默认 512 个和 64 字节)时会使用 ziplist 作为内部实现,使用更紧凑的结构实现多个元素的连续存储,在节省内存方面比 hashtable 更优秀。

    hashtable 哈希表:当哈希类型无法满足 ziplist 的条件时会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度都为 O(1)。


  • Q4:hash 应用场景

    缓存用户信息,每个用户属性使用一对 field-value,但只用一个键保存。

    优点:简单直观,如果合理使用可以减少内存空间使用。

    缺点:要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,hashtable 会消耗更多内存。


2.3 list 4

  • Q1:list 简介

    list 是用来存储多个有序的字符串,列表中的每个字符串称为元素,一个列表最多可以存储 232-1 个元素。可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发中有很多应用场景。

    list 有两个特点

    • ① 列表中的元素是有序的,可以通过索引下标获取某个元素或者某个范围内的元素列表。
    • ② 列表中的元素可以重复

  • Q2:list 命令

    添加

    从右边插入元素:rpush key value [value...]

    从左到右获取列表的所有元素:lrange 0 -1

    从左边插入元素:lpush key value [value...]

    向某个元素前或者后插入元素:linsert key before|after pivot value,会在列表中找到等于 pivot 的元素,在其前或后插入一个新的元素 value。

    查找

    获取指定范围内的元素列表:lrange key start end,索引从左到右的范围是 0N-1,从右到左是 -1-N,lrange 中的 end 包含了自身。

    获取列表指定索引下标的元素:lindex key index,获取最后一个元素可以使用 lindex key -1

    获取列表长度:llen key

    删除

    从列表左侧弹出元素:lpop key

    从列表右侧弹出元素:rpop key

    删除指定元素:lrem key count value,如果 count 大于 0,从左到右删除最多 count 个元素,如果 count 小于 0,从右到左删除最多个 count 绝对值个元素,如果 count 等于 0,删除所有。

    按照索引范围修剪列表:ltrim key start end,只会保留 start ~ end 范围的元素。

    修改

    修改指定索引下标的元素:lset key index newValue

    阻塞操作

    阻塞式弹出:blpop/brpop key [key...] timeout,timeout 表示阻塞时间。

    当列表为空时,如果 timeout = 0,客户端会一直阻塞,如果在此期间添加了元素,客户端会立即返回。

    如果是多个键,那么brpop会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回。

    如果多个客户端对同一个键执行 brpop,那么最先执行该命令的客户端可以获取弹出的值。


  • Q3:list 内部编码

    ziplist 压缩列表:跟哈希的 zipilist 相同,元素个数和大小小于配置值(默认 512 个和 64 字节)时使用。

    linkedlist 链表:当列表类型无法满足 ziplist 的条件时会使用linkedlist。

    Redis 3.2 提供了 quicklist 内部编码,它是以一个 ziplist 为节点的 linkedlist,它结合了两者的优势,为列表类提供了一种更为优秀的内部编码实现。


  • Q4:list 应用场景

    消息队列

    Redis 的 lpush + brpop 即可实现阻塞队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式地抢列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。

    文章列表

    每个用户有属于自己的文章列表,现在需要分页展示文章列表,就可以考虑使用列表。因为列表不但有序,同时支持按照索引范围获取元素。每篇文章使用哈希结构存储。

    lpush + lpop = 栈、lpush + rpop = 队列、lpush + ltrim = 优先集合、lpush + brpop = 消息队列。


2.4 set 4

  • Q1:set 简介

    集合类型也是用来保存多个字符串元素,和列表不同的是集合不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。一个集合最多可以存储 232-1 个元素。Redis 除了支持集合内的增删改查,还支持多个集合取交集、并集、差集。


  • Q2:set 命令

    添加元素

    sadd key element [element...],返回结果为添加成功的元素个数。

    删除元素

    srem key element [element...],返回结果为成功删除的元素个数。

    计算元素个数

    scard key,时间复杂度为 O(1),会直接使用 Redis 内部的遍历。

    判断元素是否在集合中

    sismember key element,如果存在返回 1,否则返回 0。

    随机从集合返回指定个数个元素

    srandmember key [count],如果不指定 count 默认为 1。

    从集合随机弹出元素

    spop key,可以从集合中随机弹出一个元素。

    获取所有元素

    smembers key
    

    求多个集合的交集/并集/差集

    sinter key [key...]
    sunion key [key...]
    sdiff key [key...]
    

    保存交集、并集、差集的结果

    sinterstore/sunionstore/sdiffstore destination key [key...]
    

    集合间运算在元素较多情况下比较耗时,Redis 提供这三个指令将集合间交集、并集、差集的结果保存在 destination key 中。


  • Q3:set 内部编码

    intset 整数集合:当集合中的元素个数小于配置值(默认 512 个时),使用 intset。

    hashtable 哈希表:当集合类型无法满足 intset 条件时使用 hashtable。当某个元素不为整数时,也会使用 hashtable。


  • Q4:set 应用场景

    set 比较典型的使用场景是标签,例如一个用户可能与娱乐、体育比较感兴趣,另一个用户可能对例时、新闻比较感兴趣,这些兴趣点就是标签。这些数据对于用户体验以及增强用户黏度比较重要。

    sadd = 标签、spop/srandmember = 生成随机数,比如抽奖、sadd + sinter = 社交需求。


2.5 zset 4

  • Q1:zset 简介

    有序集合保留了集合不能有重复成员的特性,不同的是可以排序。但是它和列表使用索引下标作为排序依据不同的是,他给每个元素设置一个分数(score)作为排序的依据。有序集合提供了获取指定分数和元素查询范围、计算成员排名等功能。


  • Q2:zset 命令

    添加成员

    zadd key score member [score member...],返回结果是成功添加成员的个数

    Redis 3.2 为 zadd 命令添加了 nx、xx、ch、incr 四个选项:

    • nx:member 必须不存在才可以设置成功,用于添加。
    • xx:member 必须存在才能设置成功,用于更新。
    • ch:返回此次操作后,有序集合元素和分数变化的个数。
    • incr:对 score 做增加,相当于 zincrby。

    zadd 的时间复杂度为 O(logn),sadd 的时间复杂度为 O(1)。

    计算成员个数

    zcard key,时间复杂度为 O(1)。

    计算某个成员的分数

    zscore key member ,如果不存在则返回 nil。

    计算成员排名

    zrank key member,从低到高返回排名。

    zrevrank key member,从高到低返回排名。

    删除成员

    zrem key member [member...],返回结果是成功删除的个数。

    增加成员的分数

    zincrby key increment member
    

    返回指定排名范围的成员

    zrange key start end [withscores],从低到高返回

    zrevrange key start end [withscores], 从高到底返回

    返回指定分数范围的成员

    zrangebyscore key min max [withscores] [limit offset count],从低到高返回

    zrevrangebyscore key min max [withscores] [limit offset count], 从高到底返回

    返回指定分数范围成员个数

    zcount key min max
    

    删除指定分数范围内的成员

    zremrangebyscore key min max
    

    交集和并集

    zinterstore/zunionstore destination numkeys key [key...] [weights weight [weight...]] [aggregate sum|min|max]
    
    • destination:交集结果保存到这个键
    • numkeys:要做交集计算键的个数
    • key:需要做交集计算的键
    • weight:每个键的权重,默认 1
    • aggregate sum|min|max:计算交集后,分值可以按和、最小值、最大值汇总,默认 sum。

  • Q3:zset 内部编码

    ziplist 压缩列表:当有序集合元素个数和值小于配置值(默认128 个和 64 字节)时会使用 ziplist 作为内部实现。

    skiplist 跳跃表:当 ziplist 不满足条件时使用,因为此时 ziplist 的读写效率会下降。


  • Q4:zset 应用场景

    有序集合的典型使用场景就是排行榜系统,例如用户上传了一个视频并获得了赞,可以使用 zadd 和 zincrby。如果需要将用户从榜单删除,可以使用 zrem。如果要展示获取赞数最多的十个用户,可以使用 zrange。


3 键、数据库管理

3.1 键重命名

rename key newkey

如果 rename 前键已经存在,那么它的值也会被覆盖。为了防止强行覆盖,Redis 提供了 renamenx 命令,确保只有 newkey 不存在时才被覆盖。由于重命名键期间会执行 del 命令删除旧的键,如果键对应值比较大会存在阻塞的可能。


3.2 键过期设置

expire key seconds:键在 seconds 秒后过期。

如果过期时间为负值,键会被立即删除,和 del 命令一样。persist 命令可以将键的过期时间清除。

对于字符串类型键,执行 set 命令会去掉过期时间,set 命令对应的函数 setKey 最后执行了 removeExpire 函数去掉了过期时间。setex 命令作为 set + expire 的组合,不单是原子执行并且减少了一次网络通信的时间。

🚩补充:Redis三种删除过期key的策略

  • 被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key
  • 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key
  • 当前已用内存超过maxmemory限定时,触发主动清理策略

3.3 键迁移

  • move

    move 命令用于在 Redis 内部进行数据迁移,move key db 把指定的键从源数据库移动到目标数据库中。

  • dump + restore

    可以实现在不同的 Redis 实例之间进行数据迁移,分为两步:

    dump key ,在源 Redis 上,dump 命令会将键值序列化,格式采用 RDB 格式。

    restore key ttl value,在目标 Redis 上,restore 命令将序列化的值进行复原,ttl 代表过期时间, ttl = 0 则没有过期时间。

    整个迁移并非原子性的,而是通过客户端分步完成,并且需要两个客户端

  • migrate

    实际上 migrate 命令就是将 dump、restore、del 三个命令进行组合,从而简化操作流程。migrate 具有原子性,支持多个键的迁移,有效提高了迁移效率。实现过程和 dump + restore 类似,有三点不同:

    ① 整个过程是原子执行,不需要在多个 Redis 实例开启客户端

    ② 数据传输直接在源 Redis 和目标 Redis 完成。

    ③ 目标 Redis 完成 restore 后会发送 OK 给源 Redis,源 Redis 接收后根据 migrate 对应选项来决定是否在源 Redis 上删除对应键。


3.4 切换数据库

select dbIndex,Redis 中默认配置有 16 个数据库,例如 select 0 将切换到第一个数据库,数据库之间的数据是隔离的。


3.5 清除数据库

用于清除数据库,flushdb 只清除当前数据库,flushall 会清除所有数据库。如果当前数据库键值数量比较多,flushdb/flushall 存在阻塞 Redis 的可能性。


4 Redis 持久化

4.1 RDB 持久化(bgsave)

4.1.1 RDB 持久化原理

RDB 持久化是**把当前进程数据生成快照保存到硬盘**的过程,触发 RDB 持久化过程分为手动触发和自动触发。

手动触发分别对应 save 和 bgsave 命令:

- save:阻塞当前 Redis 服务器,直到 RDB 过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。 
- bgasve:Redis 进程执行 fork 操作创建子进程,RDB 持久化过程由子进程负责,完成后自动结束。阻塞只发生在 fork 阶段,一般时间很短。bgsave 是针对 save 阻塞问题做的优化,因此 Redis 内部所有涉及 RDB 的操作都采用 bgsave 的方式,而 save 方式已经废弃。 

除了手动触发外,Redis 内部还存在自动触发 RDB 的持久化机制,例如:

- 使用 save 相关配置,如 save m n,表示 m 秒内数据集存在 n 次修改时,自动触发 bgsave。 
- 如果从节点执行全量复制操作,主节点自动执行 bgsave 生成 RDB 文件并发送给从节点。 
- 执行 debug reload 命令重新加载 Redis 时也会自动触发 save 操作。 
- 默认情况下执行 shutdown 命令时,如果没有开启 AOF 持久化功能则自动执行 bgsave。 

4.1.2 bgsave 的原理

① 执行 bgsave 命令,Redis 父进程判断当前是否存在正在执行的子进程,如 RDB/AOF 子进程,如果存在 bgsave 命令直接返回。

② 父进程执行 fork 操作创建子进程,fork 操作过程中父进程会阻塞。

③ 父进程 fork 完成后,bgsave 命令返回并不再阻塞父进程,可以继续响应其他命令。

④ 子进程创建 RDB 文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。

⑤ 进程发送信号给父进程表示完成,父进程更新统计信息。

4.1.3 RDB 持久化的优、缺点

优点
RDB 是一个紧凑压缩的二进制文件,代表 Redis 在某个时间点上的数据快照。非常适合于备份,全量复制等场景。例如每 6 个消时执行 bgsave 备份,并把 RDB 文件拷贝到远程机器或者文件系统中,用于灾难恢复。

Redis 加载 RDB 恢复数据远远快于 AOF 的方式。

缺点
RDB 方式数据无法做到实时持久化/秒级持久化,因为 bgsave 每次运行都要执行 fork 操作创建子进程,属于重量级操作,频繁执行成本过高。针对 RDB 不适合实时持久化的问题,Redis 提供了 AOF 持久化方式。

RDB 文件使用特定二进制格式保存,Redis 版本演进过程中有多个格式的 RDB 版本,存在老版本 Redis 服务无法兼容新版 RDB 格式的问题。


4.2 AOF 持久化

4.2.1 AOF 持久化原理

AOF 持久化以独立日志的方式记录每次写命令,重启时再重新执行 AOF 文件中的命令达到恢复数据的目的。AOF 的主要作用是解决了数据持久化的实时性,目前是 Redis 持久化的主流方式。

开启 AOF 功能需要设置:appendonly yes,默认不开启。保存路径同 RDB 方式一致,通过 dir 配置指定。

AOF 的工作流程操作:命令写入 append、文件同步 sync、文件重写 rewrite、重启加载 load:

  • 所有的写入命令会追加到 aof_buf 缓冲区中。
  • AOF 缓冲区根据对应的策略向硬盘做同步操作。
  • 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
  • 当服务器重启时,可以加载 AOF 文件进行数据恢复。

4.2.2 AOF 命令写入的原理

AOF 命令写入的内容直接是文本协议格式,采用文本协议格式的原因:

  • 文本协议具有很好的兼容性。
  • 开启 AOF 后所有写入命令都包含追加操作,直接采用协议格式避免了二次处理开销。
  • 文本协议具有可读性,方便直接修改和处理。

AOF 把命令追加到缓冲区的原因:

Redis 使用单线程响应命令,如果每次写 AOF 文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。先写入缓冲区中还有另一个好处,Redis 可以提供多种缓冲区同步硬盘策略,在性能和安全性方面做出平衡。


4.2.3 AOF 文件同步的原理?

Redis 提供了多种 AOF 缓冲区文件同步策略,由参数 appendfsync 控制,不同值的含义如下:

  • always:命令写入缓冲区后调用系统 fsync 操作同步到 AOF 文件,fsync 完成后线程返回。每次写入都要同步 AOF,性能较低,不建议配置。
  • everysec:命令写入缓冲区后调用系统 write 操作,write 完成后线程返回。fsync 同步文件操作由专门线程每秒调用一次。是建议的策略,也是默认配置,兼顾性能和数据安全。
  • no:命令写入缓冲区后调用系统 write 操作,不对 AOF 文件做 fsync 同步,同步硬盘操作由操作系统负责,周期通常最长 30 秒。由于操作系统每次同步 AOF 文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但安全性无法保证。

4.2.4 AOF 文件重写的原理?

文件重写是把 Redis 进程内的数据转化为写命令同步到新 AOF 文件的过程,可以降低文件占用空间,更小的文件可以更快地被加载。

重写后 AOF 文件变小的原因:

  • 进程内已经超时的数据不再写入文件。
  • 旧的 AOF 文件含有无效命令,重写使用进程内数据直接生成,这样新的 AOF 文件只保留最终数据写入命令。
  • 多条写命令可以合并为一个,为了防止单条命令过大造成客户端缓冲区溢出,对于 list、set、hash、zset 等类型操作,以 64 个元素为界拆分为多条。

AOF 重写分为手动触发和自动触发,手动触发直接调用 bgrewriteaof 命令,自动触发根据 auto-aof-rewrite-min-sizeauto-aof-rewrite-percentage 参数确定自动触发时机。

重写流程:

① 执行 AOF 重写请求,如果当前进程正在执行 AOF 重写,请求不执行并返回,如果当前进程正在执行 bgsave 操作,重写命令延迟到 bgsave 完成之后再执行。

② 父进程执行 fork 创建子进程,开销等同于 bgsave 过程。

③ 父进程 fork 操作完成后继续响应其他命令,所有修改命令依然写入 AOF 缓冲区并同步到硬盘,保证原有 AOF 机制正确性。

④ 子进程根据内存快照,按命令合并规则写入到新的 AOF 文件。每次批量写入数据量默认为 32 MB,防止单次刷盘数据过多造成阻塞。

⑤ 新 AOF 文件写入完成后,子进程发送信号给父进程,父进程更新统计信息。

⑥ 父进程把 AOF 重写缓冲区的数据写入到新的 AOF 文件并替换旧文件,完成重写。


4.2.5 AOF 重启加载的原理?

AOF 和 RDB 文件都可以用于服务器重启时的数据恢复。Redis 持久化文件的加载流程:

① AOF 持久化开启且存在 AOF 文件时,优先加载 AOF 文件。

② AOF 关闭时且存在 RDB 文件时,记载 RDB 文件。

③ 加载 AOF/RDB 文件成功后,Redis 启动成功。

④ AOF/RDB 文件存在错误导致加载失败时,Redis 启动失败并打印错误信息。


5 Redis 数据淘汰策略

5.1 8种淘汰策略

语法:

  • volatile-xxx从已设置过期时间的数据集中挑选进行淘汰
  • allkeys-xxx:当内存不足以容纳新写入数据时,从所有数据集中挑选进行淘汰

8种淘汰规则:

  • volatile-lru(最少使用过期):从已设置过期时间的数据集中,挑选最近最少使用的数据淘汰;
  • volatile-ttl(即将过期):从已设置过期时间的数据集中,挑选将要过期的数据淘汰;
  • volatile-random(任意过期):从已设置过期时间的数据集中,任意选择数据淘汰;
  • 🚩allkeys-lru(最少使用):当内存不足以容纳新写入数据时,移除最近最少使用的key;
  • 🚩allkeys-random(随机):从数据集中任意选择数据淘汰;
  • no-eviction(禁止淘汰):禁止淘汰数据,也就是说当内存不足时,新写入操作会报错。
  • --------------4.0版本后新增--------------------
  • volatile-lfu:从已设置过期时间的数据集中,挑选最不经常使用的数据淘汰(注意lfu和lru的区别);
  • 🚩allkeys-lfu(使用频率):当内存不足以容纳新写入数据时,移除最不经常使用的key。

5.2 怎么挑选淘汰策略?

  • 如果数据呈现幂律分布,一部分数据访问频率高,一部分数据访问频率低,则可以使用allkeys-lru或allkeys-lfu
  • 如果数据呈现平等分布,所有的数据访问频率都相同,则使用allkeys-random

5.3 MySQL里有2000w数据,redis中只存50w数据,如何保证redis中数据都是热点数据?

  1. 限定Redis内存:计算20w数据大约占用的内存(例如268435456byte),设置为redis最大内存

    maxmemory 268435456

  2. 设置淘汰策略:设置后 Redis 会根据对应的淘汰规则,留下热数据到内存。

    maxmemory-policy allkeys-lru


# 重点补充

Q1:Redis怎么保证它的高效?——纯内存/非阻塞IO(多路复用/单线程/时间分类器)

  1. redis是纯内存数据库,一般都是简单的存取操作,线程占用时间很多,时间的花费主要集中在io上,所以读取 速度快。
  2. 再说一下IO,Redis使用的是非阻塞IO,IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
  3. Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
  4. 另外,数据结构也帮了不少忙,Redis全程使用hash结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。
  5. 还有一点,Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大

Q2:Redis为什么是单线程还效率高?——(socket(队列)-时间分派器-对应事件处理器)

Redis自己开发了文件事件处理器 file event handler。

文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个Socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了 Redis 内部的线程模型的简单性。

多个 Socket 可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个 Socket,会将 Socket 放入一个队列中排队,每次从队列中取出一个 Socket 给事件分派器,事件分派器把 Socket 给对应的事件处理器

然后一个 Socket 的事件处理完之后,IO多路复用程序才会将队列中的下一个 Socket 给事件分派器。文件事件分派器会根据每个 Socket 当前产生的事件,来选择对应的事件处理器来处理。

Q3:如何保证Redis和数据库缓存一致性?

  • 读数据到缓存流程:即读数据时若发现缓存没有,则返回数据库数据的同时,也将其写入缓存
    在这里插入图片描述

  • 为什么存在一致性问题?

    不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举一个例子:
    1、如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
    2、如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
    因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题

  • 解决方案:异步更新缓存(基于订阅binlog的同步机制)
    MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

    • 读Redis:热数据基本都在Redis
    • 写MySQL:增删改都是操作MySQL
    • 更新Redis数据:MySQ的数据操作binlog,来更新到Redis

    Redis更新
    1)数据操作主要分为两大块:
    一个是全量(将全部数据一次写入到redis)
    一个是增量(实时更新),这里说的是增量,指的是mysql的update、insert、delate变更数据。
    2)读取binlog后分析,利用消息队列,推送更新各台的redis缓存数据。这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

    这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

    当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

Q4:怎么缓解Redis缓存雪崩?

【场景】
查询热点数据的时候会先从缓存加载,如果缓存没有命中则会检索数据库获取数据。往往我们还会给热点缓存数据设置一个过期时间。那么我的问题是,假设在某一时间点热点缓存全部过期失效了,这样所有的请求都会直接进入数据库,一瞬间就会把数据库压垮,如果是你会怎么解决这个问题?

【缓存雪崩概念】
因为缓存同一时间大面积的失效,或者缓存服务暂时不能提供服务等,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。这一现象被称之为 缓存雪崩。

【措施?】

  • 缓存预热:先尽可能加载缓存

    数据加热的含义就是在正式部署之前,先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key。

  • 加上互斥锁:控制线程访问量,保证缓存总是有数据

    可以在第一个查询数据的请求上使用一个互斥锁来锁住它,其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后将数据放到redis缓存起来。后面的线程进来发现已经有缓存了,就直接走缓存。

  • 过期时间均匀分布:使得过期时间各不相同

    给缓存的时效时间加上随机因子,即给缓存设置不同的过期时间,让缓存失效的时间点尽量均匀。

  • 构建高可用的缓存系统

    把Redis设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如 Redis Sentinel 和 Redis Cluster 都实现了高可用。

Q5:缓存击穿、穿透、雪崩的区别和处理方法?

  • 缓存穿透:都没有

    缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

    解决方案:
    接口层增加校验:如用户鉴权校验,id做基础校验,id<=0的直接拦截;

  • 缓存击穿:数据库有

    缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

    解决方案

    • 设置热点数据永远不过期。
    • 接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 服务 不可用时候,进行熔断,失败快速返回机制。
    • 布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,
    • 互斥锁
  • 缓存雪崩:

    缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

    解决方案

    • 过期时间均匀化:缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
    • 热点数据分开缓存:如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
    • 设置热点数据永远不过期
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值