1、出现故障

1. 缓存雪崩

如果缓存集中在一段时间内失效,所有的查询都落在数据库上,造成了缓存雪崩。
解决办法:
发生前:尽量保证整个 Redis 集群的高可用性,发现机器宕机尽快补上,如Redis的哨兵模式和集群模式。选择合适的内存淘汰策略。 例如:

  1. 在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期;
  2. 做二级缓存,或者双缓存策略:Cache1 为原始缓存,Cache2 为拷贝缓存,Cache1 失效时,可以访问 Cache2,Cache1 缓存失效时间设置为短期,Cache2 设置为长期。
  3. 数据预热:可以通过缓存 reload 机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀;

发生时:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉, 通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
发生后:利用 Redis 持久化机制保存的数据尽快恢复缓存。

2. 缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决办法:
1、缓存空对象:如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
2、布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储
系统的查询压力;
缓存空对象带来的问题:

  1. 空值做了缓存,意味着缓存中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
  2. 缓存和存储的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如:过期时间设置为 5分钟,如果此时存储添加了这个数据,那此段时间就会出现缓存和存储数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。

可以从适用场景和维护成本两方面对这两汇总方法进行一个简单比较
适用场景:缓存空对象适用于1、数据命中不高 2、数据频繁变化且实时性较高 ;而布隆过滤器适用1、 数据命中不高 2、数据相对固定即实时性较低
维护成本:缓存空对象的方法适合1、代码维护简单 2、需要较多的缓存空间 3、数据会出现不一致的现象;布隆过滤器适合 1、代码维护较复杂 2、缓存空间要少一些

3. 缓存预热

缓存预热是指系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户会直接查询事先被预热的缓存数据!
如果不进行预热,那么Redis初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
缓存预热解决方案:

  • 数据量不大的时候,工程启动的时候进行加载缓存动作;
  • 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
  • 数据量太大的时候,优先保证热点数据进行提前加载到缓存。
    4. 缓存更新
    除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种: 定时删除和惰性删除 。
    (1)定时去清理过期的缓存;
    (2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
    两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!
    5. 缓存击穿
    key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
    解决
    ①、设置热点数据永不过期
    对于某个需要频繁获取的信息,缓存在Redis中,并设置其永不过期。当然这种方式比较粗暴,对于某些业务场景是不适合的。
    ②、定时更新
    比如这个热点数据的过期时间是1h,那么每到59minutes时,通过定时任务去更新这个热点key,并重新设置其过期时间。
    ③、互斥锁
    这是解决缓存穿透比较常用的方法。
    互斥锁简单来说就是在Redis中根据key获得的value值为空时,先锁上,然后从数据库加载,加载完毕,释放锁。若其他线程也在请求该key时,发现获取锁失败,则睡眠一段时间(比如100ms)后重试。
    6. 缓存降级
    缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。 有些服务是无法降级的(如加入购物车、结算)。
    在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
    一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
    警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
    错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
    严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询, 而是直接返回默认值给用户。

2、持久化

什么是?

持久化(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘、数据库、XML文件中等)。

  • 应用层:如果关闭(shutdown)你的应用然后重新启动则先前的数据依然存在。
  • 系统层:如果关闭(shutdown)你的系统(电脑)然后重新启动则先前的数据依然存在。

    为什么?

    Redis是内存数据库,为了保证效率所有的操作都是在内存中完成。数据都是缓存在内存中,当你重启系统或者关闭系统,之前缓存在内存中的数据都会丢失再也不能找回。因此为了避免这种情况,Redis需要实现持久化将内存中的数据存储起来。

    如何实现?

    Redis官方提供了不同级别的持久化方式:

  • RDB持久化:能够在指定的时间间隔能对你的数据进行快照存储。

  • AOF持久化:记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾。Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
  • 不使用持久化:如果你只希望你的数据在服务器运行的时候存在,你也可以选择不使用任何持久化方式。
  • 同时开启RDB和AOF:当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开 启)。 如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

    RDB持久化

    RDB(Redis Database)持久化是把当前内存数据生成快照保存到硬盘的过程。

    分类

    触发RDB持久化过程分为手动触发自动触发
    (1)手动触发
    手动触发对应save命令,会阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。
    (2)自动触发
    自动触发对应bgsave命令,Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。
    在redis.conf配置文件中可以配置:save ,表示xx秒内数据修改xx次时自动触发bgsave。如果想关闭自动触发,可以在save命令后面加一个空串,即:save “”
    其他常见的触发bgsave,如

  • 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。

  • 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则 自动执行bgsave。

    bgsave工作机制

    缓存、持久化、内存淘汰 - 图1
    (1)执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进 程,如RDB/AOF子进程,如果存在,bgsave命令直接返回。
    (2)父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通 过info stats命令查看latestfork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒
    (3)父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。
    (4)子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的 时间,对应info统计的rdb_last_save_time选项。
    (5)进程发送信号给父进程表示完成,父进程更新统计信息,具体见 info Persistence下的rdb
    *相关选项。

    生成 RDB 期间,Redis 可以同时处理写请求。

    可以的,Redis 使用操作系统的多进程写时复制技术 COW(Copy On Write) 来实现快照持久化,保证数据一致性。
    Redis 在持久化时会调用 glibc 的函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。
    当主线程执行写指令修改数据的时候,这个数据就会复制一份副本, bgsave 子进程读取这个副本数据写到 RDB 文件。
    这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。

    优点
  • RDB 是紧凑的二进制文件,比较适合备份,全量复制等场景

  • RDB 恢复数据远快于 AOF

    缺点
  • RDB 无法实现实时或者秒级持久化;fork耗时

  • 新老版本无法兼容 RDB 格式。

    AOF持久化

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

    工作机制

    开启AOF功能需要配置:appendonly yes,默认不开启。
    AOF文件名 通过appendfilename配置设置,默认文件名是appendonly.aof。保存路径同 RDB持久化方式一致,通过dir配置指定。
    AOF的工作流程操作:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load)。
    缓存、持久化、内存淘汰 - 图2
    (1)所有的写入命令会追加到aof_buf(缓冲区)中。
    (2)AOF缓冲区根据对应的策略向硬盘做同步操作。
    AOF为什么把命令追加到aof_buf中?Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。先写入缓冲区aof_buf中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。
    (3)随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
    (4)当Redis服务器重启时,可以加载AOF文件进行数据恢复。

    AOF重写(rewrite)机制

    AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,
    但体积更小
    AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文
    件进行任伺读 入、分析或者写入操作。
    在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创
    建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器
    会将重写缓冲区中的所有内容 追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态
    一致。最后,服务器用新的AOF文件替换旧的 AOF文件,以此来完成AOF文件重写操作。
    缓存、持久化、内存淘汰 - 图3
    重写的目的:

  • 减小AOF文件占用空间;

  • 更小的AOF 文件可以更快地被Redis加载恢复。

AOF重写可以分为手动触发和自动触发:

  • 手动触发:直接调用bgrewriteaof命令。
  • 自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机。

auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认 为64MB。
auto-aof-rewrite-percentage:代表当前AOF文件空间 (aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值。
自动触发时机
当aof_current_size>auto-aof-rewrite-minsize 并且(aof_current_size-aof_base_size) /aof_base_size >= auto-aof-rewritepercentage。
其中aof_current_size和aof_base_size可以在info Persistence统计信息中查看。
AOF文件重写后为什么会变小?
(1)旧的AOF文件含有无效的命令,如:del key1, hdel key2等。重写只保留最终数据的写入命令。
(2)多条命令可以合并,如lpush list a,lpush list b,lpush list c可以直接转化为lpush list a b c。

AOF文件数据恢复

缓存、持久化、内存淘汰 - 图4
数据恢复流程说明:
(1)AOF持久化开启且存在AOF文件时,优先加载AOF文件。
(2)AOF关闭或者AOF文件不存在时,加载RDB文件。
(3)加载AOF/RDB文件成功后,Redis启动成功。
(4)AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。

优点
  • 可以更好地保护数据不丢失;
  • appen-only 模式写入性能比较高;
  • 适合做灾难性的误删除紧急恢复。

    缺点
  • 对于相同的数据集来说,AOF 文件要比 RDB 快照大;

  • 数据恢复(load)时AOF比RDB慢,通常RDB 可以提供更有保证的最大延迟时间,不合适做冷备。
  • AOF 开启后,会对写的 QPS 有所影响,相对于 RDB 来说 写 QPS 要下降;(QPS(Query Per Second):每秒请求数,就是说服务器在一秒的时间内处理了多少个请求。)
    对比
  1. AOF 文件比 RDB 更新频率高,优先使用 AOF 还原数据;
  2. AOF比 RDB 更安全也更大;
  3. RDB 性能比 AOF 好;
  4. 如果两个都配了优先加载 AOF。
    如何选择
    1、如果是数据不那么敏感,且可以从其他地方重新生成补回的,那么可以关闭持久化。
    2、如果是数据比较重要,不想再从其他地方获取,且可以承受数分钟的数据丢失,比如缓存等,那么可以只使用RDB。
    3、如果是用做内存数据库,要使用Redis的持久化,建议是RDB和AOF都开启,或者定期执行bgsave做快照备份,RDB方式更适合做数据的备份,AOF可以保证数据的不丢失。
    4.0添加了新的混合持久化方式。
    新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据。
    优势:混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。
    劣势:兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差。

    3、如何实现数据尽可能少丢失又能兼顾性能呢?

    混合持久化。在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

    4、内存淘汰机制

    是什么?
    Redis内存淘汰策略是指当缓存内存不足时,通过淘汰旧数据处理新加入数据选择的策略。
    如何配置最大内存?
    (1)通过配置文件配置
    修改redis.conf配置文件

    maxmemory 1024mb //设置Redis最大占用内存大小为1024M

注意:maxmemory默认配置为0,在64位操作系统下redis最大内存为操作系统剩余内存,在32位操作系统下redis最大内存为3GB。
(2)通过动态命令配置
Redis支持运行时通过命令动态修改内存大小:

127.0.0.1:6379> config set maxmemory 200mb //设置Redis最大占用内存大小为200M
127.0.0.1:6379> config get maxmemory //获取设置的Redis能使用的最大内存大小
1) “maxmemory”
2) “209715200”

淘汰策略的8种分类
  • noeviction

默认策略,对于写请求直接返回错误,不进行淘汰。

  • allkeys-lru

lru(less recently used), 最近最少使用。从所有的key中使用近似LRU算法进行淘汰。

  • volatile-lru

lru(less recently used), 最近最少使用。从设置了过期时间的key中使用近似LRU算法进行淘汰。

  • allkeys-random

从所有的key中随机淘汰。

  • volatile-random

从设置了过期时间的key中随机淘汰。

  • volatile-ttl

ttl(time to live),在设置了过期时间的key中根据key的过期时间进行淘汰,越早过期的越优先被淘汰。

  • allkeys-lfu

lfu(Least Frequently Used),最少使用频率。从所有的key中使用近似LFU算法进行淘汰。从Redis4.0开始支持。

  • volatile-lfu

lfu(Least Frequently Used),最少使用频率。从设置了过期时间的key中使用近似LFU算法进行淘汰。从Redis4.0开始支持。
注意:当使用volatile-lru、volatile-random、volatile-ttl这三种策略时,如果没有设置过期的key可以被淘汰,则和no eviction一样返回错误。

LRU算法

LRU(Least Recently Used),即最近最少使用,其核心思想是:如果一个数据在最近一段时间没有被用到,那么将来被使用到的可能性也很小,所以就可以被淘汰掉。

LRU在Redis中的实现

Redis使用的是近似LRU算法,它跟常规的LRU算法还不太一样。近似LRU算法通过随机采样法淘汰数据,每次随机出5个(默认)key,从里面淘汰掉最近最少使用的key。
可以通过maxmemory-samples参数修改采样数量, 如:maxmemory-samples 10
maxmenory-samples配置的越大,淘汰的结果越接近于严格的LRU算法,但因此耗费的CPU也很高。
Redis为了实现近似LRU算法,给每个key增加了一个额外增加了一个24bit的字段,用来存储该key最后一次被访问的时间

Redis3.0对近似LRU的优化

Redis3.0对近似LRU算法进行了一些优化。新算法会维护一个候选池(大小为16),池中的数据根据访问时间进行排序,第一次随机选取的key都会放入池中,随后每次随机选取的key只有在访问时间小于池中最小的时间才会放入池中,直到候选池被放满。当放满后,如果有新的key需要放入,则将池中最后访问时间最大(最近被访问)的移除。
当需要淘汰的时候,则直接从池中选取最近访问时间最小(最久没被访问)的key淘汰掉就行。

LFU算法

LFU(Least Frequently Used),是Redis4.0新加的一种淘汰策略,它的核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。热点数据和冷点数据。