深入理解Redis-数据清除策略&数据持久化策略&缓存策略-面试篇

文章目录

  • 数据删除
    • 过期删除
      • 定时过期
      • 惰性过期
      • 定期清除
    • 内存淘汰
  • 数据持久化
    • AOF日志
    • RDB快照
    • 混合持久化
    • 可能的问题
  • 缓存
    • 缓存雪崩(大堆的雪下榻)
    • 缓存穿透(穿针)
    • 缓存击穿(打洞)
    • 缓存预热
    • 缓存更新

数据删除

过期删除

什么是过期删除?Redis是Key-Value数据库,我们可以设置Key的过期时间。过期策略就是指当Redis中的缓存过期了,Redis如何处理。

定时过期

定时过期是指为每个设置了过期时间的key都需要设置一个定时器,到过期时间立马清除。该策略对内存很友好,但是会占用较大的CPU资源去处理过期数据。

惰性过期

惰性过期是指只有用到该key时才会判断此key是否已经到了过期时间。这种方式的较少的占用CPU资源,但是长时间不访问的key会一直占用着大量的内存。对内存不友好。

定期清除

很多方案的策略,一般会先出现两种互补的情况,然后再出现一种折中的方案。定期清除更像是一种折中方案,通过每隔一定的时间去扫描一定数量的expire字典(里面存储了所有的设置了过期时间的key的过期数据)的一定数量的key,并清除这些过期的key。结合了前两种的优点,是一种较为平衡的方案。

可能的疑问点:定时过期和定期清除有什么区别?不都是要判断是否过期删除吗? 其实不然,定时过期是给每个key都设置了定时器,这个操作就消耗了大量的资源。此外,每当有定时器过期,cpu就要执行删除策略这期间涉及到上下文切换,内存清除。这些都是消耗资源的。而定期清除是每隔一段时间集中处理,这种情况可能会允许一些已经过期的key存在但无伤大雅,因为它不需要设置定时器用较少资源的消耗换取一定的误差。可以类比 快递配送,假设快递员每来一个快递就去送,它一天要跑很多次。而正常的处理逻辑是我每隔一段时间集中送一批。

维度 定时过期(定时删除) 定期清除(定期删除) 惰性清除(惰性删除)
定义 key 设置过期时间后,系统创建定时器,时间一到立即删除 key 系统定期扫描部分 key,发现过期就删除 key 被访问时才检查是否过期,过期则删除
触发时机 到期时立即触发 每隔一段时间批量检查 用户访问 key 时触发
实现原理 为每个 key 建立定时器或调度器 周期性从过期 key 集中区中抽样检查 每次读 key 时判断当前时间是否超过 TTL
实时性 ⭐⭐⭐⭐⭐ 非常高,准时删除 ⭐⭐ 一般,有延迟 ⭐ 最差,没人访问就不会删
资源开销(CPU) ❗❗ 高,需要大量定时器管理 ✅ 适中,可控制扫描频率和时间片 ✅ 低,仅在访问时判断时间
内存压力控制 ✅ 好,过期即删 ✅ 一般,清理延迟但可控 ❌ 差,可能堆积大量已过期但未被访问的数据
实现复杂度 ❗ 高,实现和维护复杂 中等 最简单
典型用途 高实时性任务(消息超时、令牌失效) Redis/Memcached 中常用 访问不频繁的缓存数据
缺点 - 定时器数量多,调度开销大
- 不适合大量 key
- 实时性差,不够精准
- 扫描不彻底可能泄漏
- 依赖访问触发,容易堆积过期垃圾

内存淘汰

什么是内存淘汰? 由于内存压力而触发的强制删除。它与过期删除的共同点都是执行数据删除,但不同的是它们的触发条件与针对对象不同。过期删除可以理解为时间触发的自动删除。一个是由内存压力驱动,一个是由时间驱动。其区别主要如下:

机制 面向对象 触发条件 删除范围
过期淘汰 有过期时间的 key key 到了 TTL 只删这个 key
内存淘汰 所有 key(策略不同) 内存超出 maxmemory 根据策略删多个 key

redis内存淘汰共有八种,分为两类不进行数据淘汰,进行数据淘汰。
1.不进行数据淘汰
noeviction(Redis3.0之后,默认的内存淘汰策略) :它表示当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,则会触发 OOM,但是如果没数据写入的话,只是单纯的查询或者删除操作的话,还是可以正常工作。 OOM是Out Of Memory 内存溢出 或内存不足。

2.进行数据淘汰
在数据淘汰中又分为两类分别是在有过期时间的数据淘汰所有数据范围的淘汰。
(1)在设置了过期时间的数据内淘汰:
volatile-random:随机淘汰设置了过期时间的任意键值;
volatile-ttl:优先淘汰更早过期的键值。
volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值
LRU = Least Recently Used 最近最少使用=最久未使用 LFU = Least Frequently Used 最少使用 = 频率使用最低的 大家可以根据单词判断什么意思以免面试的时候短路。

(2) 在所有数据范围淘汰:
allkeys-random:随机淘汰任意键值;
allkeys-lru:淘汰整个键值中最久未使用的键值;
allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。
Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
LFU是记录每个数据的访问数。相比于LRU 偶尔访问一次的数据就不会留在内存太久。

策略名称 策略定义 优势 劣势
noeviction 不淘汰任何数据,写操作返回错误(默认策略) 数据最安全,适用于只读缓存场景 内存满后无法写入,容易导致错误甚至系统崩溃
allkeys-lru 所有 key 中淘汰最近最少使用的 key 利用率高,适合访问频率差异明显的场景 维护访问时间有一定开销
volatile-lru 设置了过期时间的 key 中淘汰最近最少使用的 key 避免删除永久数据,更安全 不淘汰无 TTL 的 key,可能导致内存耗尽
allkeys-random 所有 key 中随机淘汰一个 key 实现简单,性能好 不智能,可能删除热点 key,命中率下降
volatile-random 有过期时间的 key 中随机淘汰一个 key 不影响永久数据,适合部分短期缓存场景 淘汰不精准,命中率下降
volatile-ttl 有过期时间的 key 中,淘汰 TTL 最短的 key 尽可能保留生命周期长的数据 只对 TTL 设置合理时有效,不支持无过期 key
allkeys-lfu 所有 key 中淘汰访问频率最少的 key 精准保留高频数据,适合热点数据长期存在 要维护访问次数计数,额外内存 & 计算开销
volatile-lfu 设置了 TTL 的 key 中淘汰访问频率最少的 key 精准淘汰低频短期数据 不适合所有 key 都是永久数据的场景

数据持久化

数据持久化是指将内存的数据保存到硬盘中,以便在系统重启宕机的异常情况,数据仍然能够恢复。

AOF日志

AOF日志是指Redis执行命令时,会将命令写到一个文件中,在系统重启时对文件内命令读取并执行,通过这种方式恢复原貌。

redis中日志如何写回磁盘呢?第一种是always 是指每次写操作命令之后,就将AOF日志写回到磁盘中去。第二种是Everysec 顾名思义,每秒写一次,每次写操作执行后先写到内核缓存中,每隔一秒将缓冲区内容写到硬盘中去。这样避免了always的经常访问硬盘的麻烦。第三种是no,就是操作系统控制写回的实际。

RDB快照

RDB快照是指将某一瞬间的内存数据写回到硬盘中去,这种方式是直接将真实数据写回到磁盘,而不是命令。
可能的疑问点:在执行快照的时候,能不能修改数据。答案:能,在执行快照的主线程会创建一个子线程,子线程将主线程的内存数据复制到RDB日志中,若快照和修改数据同时发生,主线程会先复制一份内存数据,子线程拿到这部分内存数据写到硬盘中去。主线程则修改内存数据。即子线程拿到的是主线程未修改之前的数据。

混合持久化

顾名思义,两者结合,文件中前半部分是RDB的全量数据后半部分是AOF的增量命令。其结构如下:

[AOF 文件]
→ RDB 内容(全量数据)
→ AOF 命令(增量修改)

这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

可能的问题

假如一个key对应value占用的内存很大对持久化有什么影响?
无论AOF还是RDB 在写回硬盘时,都会创建一个子线程,这个子线程需要复制主线程的内存,而此时恰好主线程的内存很大。那么在复制过程可能阻塞。创建完子线程后,如果主线程修改了这个大key,又会发生写时复制,此时又会涉及到复制内存等操作,可能也会导致主线程阻塞。客户端这边也会很久很久没响应。如果直接del 会发生后续的操作也无法执行。
如何避免?
设计阶段就将大key分为多个分片,删除时不要用del 要用unlink ,unlink不会阻塞因为是异步的。这里具体解释下del是单线程执行,如果线程执行了del后续的操作都会阻塞。而unlink是将字典的key删除,然后内存的清空操作交给后台的线程慢慢清理。

策略名称 定义 优势 劣势
RDB(快照) 定期将内存数据的全量快照保存为 .rdb 文件 - 启动恢复速度快
- 文件紧凑,便于备份
- 对运行性能影响小(后台 fork)
- 可能丢失最近几秒数据
- fork 子进程占内存
- 不适合频繁变更数据
AOF(追加日志) 将每条写命令以日志形式追加到 .aof 文件中 - 数据更完整(几乎无丢失)
- 可配置刷盘策略
- 支持重写优化文件体积
- 启动恢复慢(命令重放)
- 文件可能变大
- 写入频繁时影响性能
混合持久化(Hybrid AOF) AOF 重写时,先写入 RDB 快照,再追加 AOF 命令;文件格式为:RDB + 增量 AOF - 兼顾 RDB 的启动快 & AOF 的完整性
- 恢复速度大幅提升
- 文件比传统 AOF 小
- 文件格式更复杂
- 仅 AOF 重写后生效
- 调试/查看不如纯 AOF 直观

面试可能在以下场景让推荐策略。

场景 推荐策略
数据一致性要求高(几乎不丢数据) AOF 或混合持久化
启动速度优先、数据量大 RDB 或混合持久化
频繁变更 + 快速恢复 混合持久化(最佳折中)

缓存

缓存雪崩(大堆的雪下榻)

缓存雪崩是指:大量缓存在同一时间集中失效,导致请求无法命中缓存,全部打到数据库或后端服务,从而造成系统压力剧增甚至崩溃的现象。假设你有一个电商网站,所有商品信息都缓存到了 Redis 中,如果到了 12:00 这个整点,上百万个缓存同时过期,则会发生以下情况
用户刷新页面,请求全部落空;Redis 没有命中,所有请求打到 MySQL;
短时间内涌入的请求把数据库直接压垮;整个服务接口响应变慢甚至宕机;

如何解决?
1.给缓存设置随机时间避免同一时刻全部过期。
2.使用本地缓存兜底(如 Caffeine、Ehcache),这样即使Redis雪崩,也有数据兜底。
3.对热点key设置永不过期 ,这样可以避免热点数据访问磁盘。
4.使用 Hystrix、Sentinel、Resilience4j 等组件;限流:限制请求速率,保护数据库;降级:返回默认值或提示“稍后再试”;
面试可以这样回答
事前:尽量保证整个 Redis 集群的高可用性,发现机器宕机尽快补上,选择合适的内存淘汰策略。
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉, 通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
事后:利用 Redis 持久化机制保存的数据尽快恢复缓存

缓存穿透(穿针)

缓存穿透是指查询一个一定不存在的数据,由于缓存不命中,接着查询数据库也无法查询出结果,因此也不会写入到缓存中,这将会导致每个查询都会去请求数据库,造成缓存穿透。

解决方式
1.最常用的方式是布隆过滤器,顾名思义是给数据过滤一下,判断到底是否存在的一个装置。如果不存在,则不会访问硬盘。它是将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压 力。

2.缓存空对象
当硬盘没有命中后,将空对象也缓存起来,空对象也设置一个TTL,之后再访问这个key,会直接返回空而不用访问数据库。
但这明显存在弊端,首先,内存压力增加,若缓存的空值越来越多,其占用的内存也越大,那有效值的空间会越少。其次,内存与硬盘数据的一致性存在问题。

3.适用场景
缓存空对象,由于是将不存在的缓存起来,它适用于数据会频繁变化比如业务搜索,有一些key确实查不到的场景。无法预测搜索词或者数据可能确实存在过,但现在没有。空值比例不高,但请求频繁。
布隆过滤器,由于布隆过滤器不存数据,只存可能存在的信息,它适用于数据相对固定实时性低,如登录系统。此外它适合当前值条件,可以判断一定不存在的数据过滤掉。

方案 场景 为什么适合
布隆过滤器 海量合法 key 可预知,例如用户 ID、商品 ID、短链 ID 因为可以提前构建过滤器,快速拦截非法 key;高性能、低内存、适合大并发
缓存空对象 合法请求但结果为 null,或无法预知 key 集 因为是动态响应结果,缓存 null 能防止重复查库,减少压力

其实我理解的是 布隆过滤器像是提前构建好了将所有可能的数据存进去,所以在面对经常更新的数据,可能会判断失误,适合变化不大的。而缓存空对象却相反,实时更新,它可以实时缓存。而且即使查询的是不存在的,也会定位空。

缓存击穿(打洞)

缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开一个洞。

比如常见的电商项目中,某些货物成为“爆款”了,可以对一些主打商品的缓存直接设置为永不过期。即便某些商品自己发酵成了爆款,也是直接设为永不过期就好了。mutex key互斥锁基本上是用不上的,有个词叫做大道至简。

缓存预热

缓存预热是指在系统启动或上线之前,将常用/热点数据提前加载进缓存,避免系统刚启动时因缓存全为空而引发大量请求打到数据库。

解决方式:1.直接写个缓存刷新页面,上线时手工操作下; 2.数据量不大,可以在项目启动的时候自动进行加载; 3.定时刷新缓存;

缓存更新

缓存更新是指:当数据库中的数据发生变化时,保证缓存中对应的数据也能同步更新,避免“缓存和数据库不一致”问题。

这个一致性问题的解决方案又是一大块明天干!记录的有简有详,可以理解为是否重点。兄弟们晚安。

你可能感兴趣的:(redis,缓存,redis,面试,java,数据库)