一. 双写一致
1. 定义(关键词:缓存同步更新)
执行写操作时,修改DB数据的同时,Redis中的数据也要更新。
(补充1)双写一致业务场景:
Redis做“只读缓存”,DB先修改,缓存需要同步更新。
(补充2)若Redis做“读写缓存”:
若缓存数据可以进行“增删改”,则需要指定“写回策略”。
→ 同步直写策略:写缓存时,也同步写DB;
→ 异步写回策略:写缓存时,不同步写DB,等到缓存数据淘汰时,再写回DB,有效减少写回次数。(但若出现缓存宕机,最新数据就会丢失。)
2. 读写操作 过程详解
- 读操作:负责读取数据、更新缓存
“先缓存,后DB;未缓存,再DB”,并触发Redis数据更新。
在处理好缓存的穿透、击穿、雪崩之后,读数据操作基本不会出现问题。
- 写操作:负责删除缓存、更新DB
常见错误步骤一:简单的“先删除缓存,再修改DB”
情况举例:写线程1率先执行。若在缓存已删除且DB未修改时,读线程2趁机执行,则会触发 “缓存未命中→读取DB数据(旧)→缓存更新(旧)” ,在此之后,写线程1才完成DB修改。
导致后果:
读取的是旧数据,缓存中也是旧数据,唯有数据库是新数据。
常见错误步骤二:简单的“先修改DB,再删除缓存”
情况举例:读线程1率先执行,且刚好缓存过期。若在数据已读取(旧DB数据) 且缓存未更新时,写线程2趁机执行,则会触发“DB更新→删除缓存(空删)”,在此之后,读线程1才完成缓存更新,并且是旧数据。
导致后果:
读取的是旧数据,缓存中也是旧数据,唯有数据库是新数据。
写操作错误总结:
以上两种错误的原因皆为 “读写线程交织” 导致 “读写不协同”;简单来说,就是写线程的“删除缓存”总是在读线程“更新缓存”之前进行,导致缓存最终滞留脏数据。
反言之,若能①妥善处理脏数据再次进入,或②读写线程不发生重叠,又或③能够保证读写协同,则不会出现不一致错误。
综上,通过对读写过程的分析,我们得出结论:搭载“只读缓存”的系统在执行写操作时,必须采用特定方案解决双写一致问题,同步缓存与DB数据。
2. 解决方案
(1)延时双删
问题一:为何双删?
从写操作两个示例的后果可以看出,最终都是 “旧缓存+新DB” 的情况。并且以上我们总结过,“读写交织”本不是错,但是极易出现 “删完又进脏数据” 的情况。
所以,我们在写操作完成DB修改后,再次删除缓存!避免缓存中滞留脏数据;
在下一次读操作的时候,线程会向DB读取最新数据,并完成缓存更新!
问题二:为何延时?
以写操作的 “常见错误步骤一” 举例:线程具有 “独立运行、状态不可预测” 的特点,所以我们无法确认,读线程2(B事务)的缓存更新一定早于写线程1(A事务)的DB更新。
如果 写线程1执行“立即双删” 策略,则无法确保脏数据进入缓存。
所以,延时的作用:等待读取到旧DB数据的线程全部结束,脏数据已经全部进入缓存,再执行删除。
① 优势:简单。
② 劣势:延时的时间无法精确把控,时间过短可能仍有“带旧数据的读线程”未结束,功亏一篑;时间过长会导致大量新一批的读线程(C、D事务)从缓存中获取到脏数据。(延时双删的天然痛点,无法解决!该方法实际不予使用。)
(2)普通互斥锁
暴力的将读写分离。读写操作不存在重叠,自然不需要考虑双写一致性问题,但是系统效率将大幅下降。所以实际不会使用此方法。
(3)Redission读写锁(强一致)
读线程加“读锁readLock”,也叫共享锁:允许其他读线程,阻止任何写线程。(可读、不可写)
写线程加“写锁writeLock”,也叫独占锁:阻止任何读写线程。(不可读、不可写)
① 优势:允许“读线程”并发,在保证读写分离强一致的前提下,最大限度保障系统性能。
② 劣势:写操作仍然会阻塞所有线程,影响系统性能。
(4)异步通知(弱一致)
异步通知 将不同的模块 归于 不同的服务,各服务在MQ的作用下协同运行,数据的最终一致性则依赖MQ的可靠性!
所以,异步通知在确保一致性(MQ可靠)的前提下,仍可获得优秀的性能(无锁),不过会稍有延迟(消息传输耗时)。
- 基于MQ:写操作更新DB数据 → 发送消息至MQ → 缓存服务持续监听MQ → 通知缓存删除(下次读操作更新)。
- 基于阿里Canal中间件:Canal伪装为MySQL的从节点,读取日志文件binlog,达到监听DB数据修改的效果;监听成功后,通知缓存更新。
基于Canal的方法无需修改业务代码,是目前较推荐的双写一致解决策略。
① 优势:确保双写一致,极大的保障系统性能。
② 劣势:在大并发情况下会出现短暂延迟(大多数系统是可接受的)。
二. 持久化
1. 相关概念(关键词:缓存写入磁盘)
(1)为什么要持久化
Redis是基于内存的数据库。
- 优点:cpu读取内存速度快,一秒钟可以进行数十万次,可以直接和cpu速度相近,读取极快。
- 缺点:断电数据丢失。
为了防止其数据断电丢失,就需要将数据存入硬盘中,这样在断电后也可以访问到数据库当中的数据。
(2)持久化定义
以上将内存的数据写入到磁盘中,防止服务器宕机内存数据丢失,就是Redis的持久化。
(3)持久化方式
Redis支持两种持久化方式:RDB和AOF。
2. RDB(内存快照)
(1)简介
Redis Database Backup file,Redis数据备份文件。
RDB原理:按照一定的时间将内存中所有数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。Redis实例重启后,从磁盘读取快照文件,恢复数据。
RDB持久化方式Redis是 默认开启 的,有多种触发的方式。
(2)主动备份(客户端命令方式)
① save:Redis 主进程 执行RDB,会阻塞所有命令。
② bgsave:开启 子进程 执行RDB,避免主进程受到影响。(推荐)
(3)内部触发(Redis服务端方式)
① 配置redis.conf文件:管理员在redis.conf中设置save配置选项,Redis会在save选项条件满足之后自动触发一次BGSAVE命令,如果管理员设置了多个save配置选项,当 任意 save条件被满足,Redis都会触发一次BGSAVE命令。
② shutdown:当Redis 通过shutdown指令接受到关闭服务器的请求时,会触发一次SAVE命令,阻塞所有的客户端,不再执行客户端发送的任何命令,在SAVE命令执行完毕后关闭服务器。
3. AOF(命令记录)
(1)简介
Append Only File,日志追加文件。
AOF原理:将Redis执行过的所有写命令记录到日志文件AOF末尾。当Redis重启时,会从头到尾执行一次AOF文件所包含的所有写命令,恢复AOF文件记录的Redis数据集。
AOF持久化方式Redis是默认关闭的,需要手动开启并且做一些配置。
(2)AOF配置
配置文件:redis.conf。(同RDB服务端触发)
① 启动配置 / AOF文件名:
② 刷盘策略(命令记录频率):推荐使用everysec。
(3)优化(AOF重写)
- AOF记录的是命令文本,RDB生成的内存快照dump.rdb是二进制文件,所以AOF文件体积会远大于RDB。
- 并且,一个key的所有相关命令会被AOF全部记录,但只有最后一条写命令有实际意义。
① 客户端 手动重写优化:bgrewriteaof命令(bg rewrite aof)
② 服务端 自动触发重写:配置redis.conf。
4. 优缺点对比
注意:RDB和AOF不互斥。若系统既要求启动速度,又要求安全性,往往同时开启RDB和AOF,两者结合使用(混合持久化)。
5. 混合持久化
通过 AOF 后台重写(bgrewriteaof 命令)完成。
子进程先将当前全量数据以 RDB 方式写入新的 AOF 文件;
然后再将 AOF 重写缓冲区的增量命令以 AOF 方式写入到文件;
完成后,通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
① 优点:系统数据获得更快的恢复速度与更强的安全性。
② 缺点:AOF 文件里面的 RDB 部分不再是 AOF 格式,可读性差。
三. 数据过期策略
1. 定义(关键词:过期删除规则)
Redis对数据设置有效时间,数据过期以后,就需要将数据从内存中删除。
我们可以按照不同的规则进行删除,具体的删除规则也称之为数据过期策略。
两种常用的数据过期策略:惰性删除、定期删除。
(回顾:在避免缓存击穿时,有一种策略叫做“逻辑过期”,即不删除过期数据,在首次命中过期缓存key时会获取互斥锁,开启新线程进行缓存重建,期间所有对key的请求皆返回过期数据。)
2. 惰性删除
在Redis中,为每个key都设置一个过期时间。
(1)不请求 key 时,则不去管该key是否过期:一直存在于内存;
(2)请求 key 时,则检测 key 是否过期:过期则删除,未过期则返回key的数据。
① 优势:CPU友好,不必频繁清理key。
② 劣势:内存不友好,内存中可能存在大量过期key。
3. 定期删除
每隔一段时间,我们就对一些key进行检测,过期则删除。
(1)SLOW模式:定时任务模式。清理的默认频率10hz(每秒进行10次清理),每次清理耗时不超过25ms。可以通过redis.conf修改执行频率。
(2)FAST模式:不定频模式。清理频率更快,但两次清理间隔不应低于2ms,每次清理耗时不超过1ms。
注意:无论是SLOW还是FAST模式,单次清理的耗时大概率是不足以覆盖所有缓存数据的(所以定义为“隔一段时间,检测一些key”)。单次清理时间耗尽必须停止,下次再接着检测。
① 优势:内存友好,定期删除过期key;可以通过限制清理的时常与频率降低对CPU的影响。
② 劣势:难以确定删除操作的执行时常与频率。(过长则占用CPU,过短则无法及时释放内存。)
4. Redis的过期删除策略
“惰性删除 + 定期删除” 两种策略配合使用。
四. 数据淘汰策略
1. 定义(关键词:缓存多、内存满)
当Redis中的内存不够用时,此时再向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删掉,这种数据的删除规则被称之为内存的淘汰策略。
2. Redis支持的8种数据淘汰策略
(1)noeviction【默认策略】
内存满时,不淘汰任何key,也不允许写入新数据,并直接报错。
(2)allkeys-random
所有key进行随机淘汰。
(3)volatile-ttl
剩余TTL越少,越先被淘汰。
(4)volatile-random
对于设置了TTL的key,进行随机淘汰。
(5)allkeys-lru【使用较多 / 推荐使用】
所有key采用LRU算法淘汰。
(6)volatile-lru
对于设置了TTL的key,采用LRU算法淘汰。
(7)allkeys-lfu
所有key采用LFU算法淘汰。
(8)volatile-lfu
对于设置了TTL的key,采用LFU算法淘汰。
补充1(单词解释):
① eviction 驱逐;no eviction不驱逐,不淘汰。
② volatile 易变的,代表着“有 生存时间 / 过期时间 的key”。
③ ttl = time to live生存时间。
补充2(LRU与LFU):
① LRU:Least Recently Used 最近最少使用,淘汰最长时间没有被使用的key。
② LFU:Least Frequently Used 最少频率使用,淘汰一段时间内使用次数最少的key。
③ LRU在全内存寻找“最长时间没被访问”的数据,一定程度上可以代表热点数据的保留;LFU仅寻找“某时间段内最少访问”的数据,时间段存在不确定性,所以LFU不能准确的代表热点数据的保留情况。
注意:应结合业务场景选择合适的数据淘汰策略。
3. 常见业务场景
- 存在冷热数据:优先保留“热点数据”,选用allkeys-lru。
- 无明显冷热数据:随机淘汰,选用allkeys-random。
- 存在置顶数据:置顶数据通常是热点key或重要数据,它们不设过期时间,淘汰策略选用volatile-lru。(这种策略组合也可以一定程度上预防缓存击穿问题)
- 存在 “短时高频”访问数据:选用allkeys-lfu / volatile-lfu。