缓存知识点总结

缓存知识点总结

本文件, 系统地对缓存的核心概念作出梳理, 阐述其工作原理, 罗列出常见模式, 指出典型问题, 说明一致性策略, 并介绍最佳实践, 适用于后端开发, 适用于架构设计, 也适用于面试复习。

目录一、缓存基础概念1.1 什么是缓存

缓存, 也就是Cache, 是一种技术或者组件, 用于临时存储高频访问的数据, 其核心思想是, 用空间去换时间, 也就是通过把数据放到访问速度更快的存储介质当中的方式, 来减少对慢速存储的访问次数, 像数据库、磁盘、远程服务这些, 进而提升系统的整体性能。

1.2 缓存的核心思想

缓存的本质是 数据冗余与就近访问,遵循以下两个基本原则:

百分之八十的请求向百分之二十的数据集中, 把这百分之二十的热点数据进行缓存便能获取显著收益, 这就是二八定律, 也就是80/20法则。关于缓存的位置处于1.3。

缓存可以存在于计算机体系的各个层级,形成 多级缓存体系:

复制代码
CPU L1/L2/L3 缓存 → 内存 → 本地缓存 → 分布式缓存 → 数据库 → 文件系统
        快 ←------------------------------------------------------------------------------------------------------------------------→ 慢
        贵 ←------------------------------------------------------------------------------------------------------------------------→ 便宜
        小 ←------------------------------------------------------------------------------------------------------------------------→ 大

二、缓存的作用与优势2.1 主要作用作用说明

高性能

其对基于内存的缓存予以依赖, 于读写速度方面, 相较于磁盘数据库有着极大超越(Redis 每秒约能处理 10 万次请求, MySQL 每秒至多处理数千次请求)。

高并发

缓存可承受远高于数据库的并发请求,是系统应对高并发的利器

降低后端压力

减少数据库、远程服务的访问频次,保护下游系统

降低网络开销

本地缓存避免了跨网络调用

节约成本

减少数据库负载可降低对昂贵硬件资源的依赖

2.2 缓存的代价

缓存并非"银弹",引入缓存也带来以下成本:

法则如下, 要不要引入缓存, 得在性能所获收益, 跟一致性以及复杂度成本之间去权衡。对于数据一致性有着极高要求的情形, 就像金融账户余额这种状况, 应当谨慎地运用缓存。

三、进行缓存操作时所依据的工作原理, 其中包含3.1所述的缓存命中以及未命中的情况, 还涵盖了3.2所涉及的缓存命中率。

缓存命中率,也就是Cache Hit Ratio, 等于命中次数, 除以命中次数加上未命中次数。

一个用于衡量缓存效果的核心指标是命中率, 一般而言期望其能在百分之八十以上达到。存在着一些影响命中率的关键因素:

3.3 缓存读写基本流程

读流程(Read-):

复制代码
1. 请求查询数据
2. 先查询缓存
3. 命中 → 直接返回
4. 未命中 → 查询数据库 → 写入缓存 → 返回

写流程 :见 。

四、依据部署方式来进行分类的缓存类别中的4.1, 存在着一种为本地缓存(Local Cache)的类型。

数据存储在应用进程内或本机内存中。

缺点 :2. 分布式缓存( Cache)

数据存储在独立的缓存服务集群中,多应用实例共享。

缺点 :4.2 按层级分类(多级缓存)

实际生产中常采用 多级缓存架构:

复制代码
请求 → L1 本地缓存(Caffeine) → L2 分布式缓存(Redis) → 数据库

4.3, 沿着 CDN或者 HTTP缓存的类别进行划分, 五, 那些具有常见性的用于缓存的读写策略, 5.1, Cache Aside(旁路缓存), 这是最为常用的。

应用代码同时维护缓存与数据库,缓存由调用方主动管理。

读流程:

先查缓存,命中则返回未命中查数据库,并将结果写入缓存

写流程:

先更新数据库,再删除缓存(推荐)

// 伪代码

Data read(Long id) {

Data data = cache.get(id);

if (data == null) {

data = db.query(id);

if (data != null) {

cache.put(id, data);

data;

void (Data data) {

db.(data);

缓存, (数据获取其标识), (此动作是删除, 而非更新)。

要点:

5.2 Read/Write (读穿/写穿)

应用只与缓存交互,缓存组件负责同步回源到数据库。

具有这样的特性, 它对于应用而言是透明的, 然而它的实现过程却是复杂的, 正常情况下它需要缓存中间件的扶持, 像是 Guava 这种。

5.3 Write / Write Back(异步回写)

写入时只更新缓存,由后台异步批量刷新到数据库。

适用场景:计数器、日志、监控数据等容忍数据丢失的场景。

5.4 三种策略对比策略一致性性能复杂度适用场景

Cache Aside

较强

通用场景,最常用

Read/Write

缓存组件支持的场景

Write

极高

写密集、容忍丢数据

六、缓存淘汰策略

在缓存容量抵达上限这个时候, 要依据淘汰策略去删除一部分数据。常见的策略有:

6.1 常见淘汰策略策略说明适用场景

FIFO(First In First Out)

先入先出,淘汰最早进入的

简单场景,少用

LRU(Least Used)

淘汰最久未使用

通用,最常用,符合时间局部性

LFU(Least Used)

淘汰使用频率最低

热点数据明显的场景

LRU-K

综合最近 K 次访问

优化 LRU 的"缓存污染"问题

ARC( Cache)

自适应 LRU 与 LFU

复杂度高,特定场景

(随机淘汰)

随机删除

实现简单,性能稳定

TTL(Time To Live)

按过期时间淘汰

数据有时效性

6.2 Redis 的 8 种淘汰策略策略范围算法

不淘汰

内存满时写入直接报错

-lru

所有 Key

LRU

-lfu

所有 Key

LFU

所有 Key

随机

-lru

设置过期时间的 Key

LRU

-lfu

设置过期时间的 Key

LFU

设置过期时间的 Key

随机

-ttl

设置过期时间的 Key

TTL 最短优先

Redis 4.0 以及更高版本引入了 LFU, 它适用于那种热点数据较为明显的场景。

6.3 LRU 简易实现思路

LRU, 一般是借助双向链表来达成实现, 以此保障了O(11)的get以及put。

七、缓存三大经典问题7.1 缓存穿透(Cache )

这样来定义, 请求去查询一个, 根本就不存在的数据, 缓存里面没有, 数据库里面也没有, 每一次请求都会打到数据库那里。

问题如下: 存在恶意攻击者, 其会运用大量并不存在的 Key 发起请求, 这种行为存在让数据库承受重大压力倒塌的可能性。

解决方案:

缓存空值或者默认值, 哪怕数据库当中没查到, 也会把null进行缓存, 并且设置较短的过期时间。

复制代码
cache.put(key, NULL, 60s);

有种名为布隆过滤器(Bloom)的东西, 要进行这样的操作, 即在缓存的前面添加一层布隆过滤器, 让请求先行通过这个过滤器, 以此来判断 Key 有没有存在的可能性。

接口限流与参数校验:对非法请求(如 ID < 0)直接拦截

7.2 缓存击穿(Cache )

定义是, 热点Key处于过期的那个瞬间, 有大量并发请求一同到达, 并且全部穿透到数据库。

危害:瞬时高并发请求压垮数据库。

解决方案:

一种称作, 互斥锁的机制, Mutex Lock, 当出现未命中的情况时, 仅会允许一个线程回到源头来查询数据库, 而其他的线程则都要处于等待的状态。

复制代码
// 伪代码
if ((data = cache.get(key)) == null) {
    if (tryLock(key)) {           // 获取分布式锁
        try {
            if ((data = cache.get(key)) == null) {
                data = db.query(key);
                cache.put(key, data);
            }
        } finally {
            unlock(key);
        }
    } else {
        sleep(50);               // 短暂等待后重试
        return read(key);
    }
}

热点 Key 永不过期:不设置过期时间,通过后台异步更新

逻辑过期之时, 于 Value 当中存储逻辑过期的时间, 当此时间到达期限之后, 由后台进行异步刷新。

7.3 缓存雪崩(Cache )

有一种情况被进行了界定, 那就是, 许多的 Key 一同失去效果, 甚至于这个缓存服务整个地出现故障而停止运行, 最终致使数量巨大的请求直接穿透到数据库里。

危害:数据库瞬时负载激增,可能引发连锁故障。

解决方案:

过期时间打散 :在基础过期时间上加随机值,避免同时失效

复制代码
expire = baseExpire + random(0, 300s)

缓存高可用:Redis 集群、哨兵模式,避免单点故障

多级缓存:本地缓存 + 分布式缓存兜底

限流降级:数据库层加限流,缓存不可用时返回降级数据

缓存预热:系统启动时提前加载热点数据

7.4 三大问题对比问题触发原因核心特征主流方案

穿透

查询不存在的数据

数据本身不存在

空值缓存 + 布隆过滤器

击穿

热点 Key 过期

单个 Key 失效

互斥锁 + 永不过期

雪崩

大量 Key 同时失效

大面积失效

过期时间打散 + 高可用

八、缓存与数据库一致性8.1 一致性问题来源

缓存, 是独立的存储, 数据库, 也是独立的存储, 二者无法于一个事务之内进行原子更新, 所以必然会存在短暂的不一致窗口。

8.2 常见方案对比方案 1:先更新数据库,再更新缓存

下面这种情况会出现问题, 在并发写的场景当中, 线程A和线程B会交错着去执行, 这样一来就可能致使缓存跟数据库之间出现不一致的状况:

复制代码
线程A 更新 DB = 1
线程B 更新 DB = 2
线程B 更新缓存 = 2
线程A 更新缓存 = 1   ← 缓存为 1,DB 为 2,不一致!

方案二: 首先进行数据库的更新操作, 之后再开展删除缓存的行为, 此为Cache Aside方式, 属于推荐的做法。

写操作删除缓存,下次读会重新加载,避免并发更新问题。

方案 3:先删除缓存,再更新数据库

问题:删缓存后、更新数据库前,有线程读取并回填旧数据:

复制代码
线程A 删除缓存
线程B 读缓存未命中 → 读 DB 旧值 → 写入缓存(旧值)
线程A 更新数据库    ← 缓存为旧值,不一致!

改进为: 延迟双删, 先进行缓存删除操作, 接着更新数据库, 随后让系统休眠一段时长, 最后再次开展缓存删除动作。

复制代码
1. 删除缓存
2. 更新数据库
3. sleep(N ms)        // 等待读请求完成回填
4. 再次删除缓存

8.3 终极方案:消息队列 + 重试

碰到删除缓存失败致使的不一致情况, 要凭借消息队列确保最终一致性来加以解决。

复制代码
更新DB → 删除缓存 → 失败? → 投递 MQ → 消费者重试删除

或订阅数据库 (如 Canal)异步删除缓存:

复制代码
DB → binlog → Canal → 解析变更 → 删除对应缓存

8.4 一致性方案选型建议场景推荐方案

一般业务,容忍短暂不一致

Cache Aside(先更新 DB,再删除缓存)

强一致要求

不缓存 / 短 TTL + 加锁 / 同步

删缓存易失败

MQ 重试 / 订阅

核心坚持的原则是, 首先进行数据库操作, 随后再开展缓存操作, 采取删除的方式而非更新, 并且要确保删除能够成功, 若不成功则进行重试。

九、常见缓存技术对比9.1 Redis vs 特性

数据结构

/Hash/List/Set/ZSet/ 等

仅 KV()

持久化

支持 RDB/AOF

不支持

集群

原生 、哨兵

客户端分片

线程模型

单线程(6.0 多线程 IO)

多线程

内存效率

较高

更高(结构简单)

适用场景

复杂业务、需要持久化

纯 KV、大容量简单缓存

9.2 本地缓存对比特性

性能

最优(W-)

良好

一般

淘汰算法

W-(接近最优命中率)

LRU

LRU/LFU

堆外存储

支持

不支持

支持

推荐度

( Boot 默认)

已停更

选型给出的建议是, 对于本地缓存, 首先考虑选用的是, (在 Boot 2.x 及以上版本为默认情况的那种), 而对于分布式缓存, 首先选择的是 Redis。

9.3, 其他缓存层面, 十, 关于缓存高可用以及集群方面, 10.1, Redis的高可用方案, 其一, 主从复制, 也就是Slave模式, 其二, 哨兵模式, 其三, 就是存在的集群模式, 10.2, 涉及到数据分片策略, 10.3, 关乎由缓存故障时所具备的容错能力, 十一, 缓存的最佳实践之中, 11.1, Key的设计, 11.2, Value的设计, 11.3, 缓存粒度, 11.4, 有着监控指标, 以及针对该指标的说明, 还有告警阈值这些情况的建议。

命中率

hit / (hit + miss)

< 80% 需关注

内存使用率

used / max

> 80% 关注

QPS

每秒请求数

接近上限告警

慢查询

执行时间长的命令

> 10ms 告警

大 Key

单 Key 体积

> 10KB 关注

热 Key

访问频次极高

单 Key > 1万 QPS

11.5, 常见禁忌十二, 典型应用场景, 12.1, 商品详情页, 12.2, 秒杀, 斜杠, 抢购, 12.3, 排行榜, 12.4, 会话存储(括号形式那种), 12.5, 计数与限流, 12.6, 热点数据, 十三, 常见面试题, Q1, 为什么进行写操作的时候要把缓存删掉而不是对缓存进行更新呢?

答:

规避并发写致使数据出现不一致状况(更新顺序无法把控), 节省资源, 存在如此情况: 部分缓存或许从来都未曾被读取过, 更新属于浪费行为, 秉持懒加载观念: 在用的时候才展开加载, 数据会更具"新鲜度", Q2: 怎样确保缓存与数据库达成最终一致性呢?

答: 采取 Cache Aside(一开始更新数据库, 随后删除缓存所涉及缓存), 加上消息队列重试 的办法, 或者 订阅(借助 Canal)以在异步状态下删除缓存, 进而确保删除这一操作最终能够成功实现。

Q3:缓存穿透、击穿、雪崩的区别与解决方案?

见 。

Q4: Redis这般快是为何? 其基于内存, 是纯内存方面的操作, 采用单线程模型, 能避免上下文切换以及锁竞争, 运用IO多路复用(epoll), 还有高效的数据结构(SDS、跳表、压缩列表等), 在6.0时引入多线程来处理网络IO? Q5: 怎样去排查线上缓存命中率突然间下降的情况? 查查有没有大的Key被抛弃, 瞅瞅过期策略变了没, 看看有没有异常的流量, 像是大量冷数据的请求, 瞧瞧缓存容量够不够, 看看Key的设计合不合理, 比如说随机性太高, Q6: 分布式锁怎么用缓存来实现?

凭借 Redis 里边 SET 这种操作, 针对 key, 赋予 value, 使用 NX、PX 这类方式予以应用, 要留意。

参考资料

相关推荐
莫得感情 o5 小时前
Redis 10 · 集群:16384槽、扩缩容与请求路由
redis·缓存
泡海椒5 小时前
JQuick-java (JQuick-ASM)性能优化原理:字节码生成与缓存机制深度解析
java·缓存·性能优化
莫得感情 o5 小时前
Redis 11 · 缓存问题:穿透、击穿与雪崩
redis·缓存
kiss strong6 小时前
redis服务器登录(内网环境,无法使用客户端)
数据库·redis·缓存
Javatutouhouduan17 小时前
Java如何速通性能优化难题?
java·java面试·jvm调优·后端开发·java程序员·java八股文·java性能优化
来让爷抱一个19 小时前
2026 语义缓存实战:把命中契约写进SPEC,MonkeyCode 云端跑通
人工智能·机器学习·缓存
hweiyu0019 小时前
Redis命令:UNLINK
redis·缓存
志尊宝1 天前
Vue3 零基础每日笔记(009):watch 侦听器——数据一变就“做事“
vue.js·笔记·缓存
FfHUCisI1 天前
Go 内存分配器概览:从 TCMalloc 到三级缓存架构
缓存·架构·golang