赢了面子,输了里子,缓存就是个面子

在线体验各类最新模型,更有模型 免费Token 额度领取!
立即体验
简介: 使用缓存来加速应用程序的访问速度,是几乎所有高性能系统都会采用的方法。但缓存真的那么好吗?架构师在构建高性能系统时,是不是必须增加缓存组件?缓存是不是多多益善?《一代宗师》里本山大叔说过这样的一段话:“一门里,有人当面子,就得有人当里子。面子不能沾一点儿灰尘。流了血,里子得收着,收不住,漏到了面子上,就是毁派灭门的大事。”

使用缓存来加速应用程序的访问速度,是几乎所有高性能系统都会采用的方法。

但缓存真的那么好吗?架构师在构建高性能系统时,是不是必须增加缓存组件?缓存是不是多多益善?

《一代宗师》里本山大叔说过这样的一段话:

“一门里,有人当面子,就得有人当里子。面子不能沾一点儿灰尘。流了血,里子得收着,收不住,漏到了面子上,就是毁派灭门的大事。”

image.png

如果缓存是里面的角色,那么绝对是面子。

虽然缓存带来了性能的提升,但给系统带来的多少复杂性。缓存穿透,缓存击穿,缓存雪崩,缓存污染这些问题不绝于耳。这些问题不是操作系统兜着,就是程序员。所以缓存是24K纯金的面子。人人都好面子,还得有个能轻松应对的里子。

历史悠久

看现在软件世界里面,尤其互联网系统,处处都有缓存。但其实缓存的历史很久远。在硬件时代就已经出现了。

且看存储金字塔结构:

image.png

受限于存储介质的存取速率和成本,现代计算机的存储结构呈现为金字塔型。越往塔顶,存取效率越高、但成本也越高,所以容量也就越小。得益于程序访问的局部性原理,这种节省成本的做法也能取得不俗的运行效率。从存储器的层次结构以及计算机对数据的处理方式来看,上层一般作为下层的Cache层来使用(广义上的Cache)。比如寄存器缓存CPU Cache的数据,CPU Cache L1~L3层视具体实现彼此缓存或直接缓存内存的数据,而内存往往缓存来自本地磁盘的数据。

在Linux的IO体系里面也有它的身影:

image.png

如图中的PageCache,应用程序在写文件的时候,操作系统会先把数据写入到 PageCache 中,数据在成功写到 PageCache 之后,对于用户代码来说,写入就结束了。

然后,操作系统再异步地把数据更新到磁盘的文件中。应用程序在读文件的时候,操作系统也是先尝试从 PageCache 中寻找数据,如果找到就直接返回数据,找不到会触发一个缺页中断,然后操作系统把数据从文件读取到 PageCache 中,再返回给应用程序。

在数据写到 PageCache 中后,它并不是同时就写到磁盘上了,这中间是有一个延迟的。操作系统可以保证,即使是应用程序意外退出了,操作系统也会把这部分数据同步到磁盘上。但是,如果服务器突然掉电了,这部分数据就丢失了。

Cache的同步方式有两种,即Write Through(写穿)和Write back(写回)。从名字上就能看出这两种方式都是从写操作的不同处理方式引出的概念。

对应到Linux的PageCache上所谓Write Through就是指write操作将数据拷贝到Page Cache后立即和下层进行同步的写操作,完成下层的更新后才返回。

而Write back正好相反,指的是写完Page Cache就可以返回了。Page Cache到下层的更新操作是异步进行的。

Linux下Buffered IO默认使用的是Write back机制,即文件操作的写只写到Page Cache就返回,之后Page Cache到磁盘的更新操作是异步进行的。因此才会出现数据丢失问题。

可以看出读写缓存的这种设计,它天然就是不可靠的,是一种牺牲数据一致性换取性能的设计。

但在硬件时代,相对程序员的好处是,自己不用考虑,操作系统已经实现这些机制了。

为什么需要缓存

回到开篇的问题,缓存真的是必不可少的组件吗?缓存带来的系统复杂性与提升的系统性能投入产出比合理吗?

如果CPU强劲,IO没有开销,还需要缓存吗?反过来,引入缓存到底是为了提升性能,还只是为了补齐CPU与IO的短板。是主攻还是防守?是不是个面子工程。

所以综观一下,引入缓存,主要是两方面的理由:

1.缓解CPU压力:把运算结果存储起来,节省CPU算力2.缓解IO压力:把原本对网络、磁盘等慢介质的读写访问变为对内存等快介质的访问。

而这两方面,核心不是为了性能,性能的提升只是个副作用。

也就是如果能通过提升硬件,增强CPU和IO本身性能,是更好的解决方案。

譬如云,如果不是硬件受工艺与成本的限制,谁会花大成本搞云呢。加机器多省事。

在互联网场景下,大多数时候是读缓存,但也有像消息队列,读写量几乎是均衡的,如何应对缓存带来复杂性,是每一次系统架构时引入缓存组件时都需要考虑的问题。

数据一致性

使用缓存组件,我们需要重点关注一些技术指标,如“吞吐量”和“命中率”。而在使用过程中面对的问题,相对缓存穿透、击穿、雪崩,还是更关注一下缓存污染,也就是数据一致性问题。之前写过《百万QPS系统的缓存实践》[1],但里面还有一些场景没有详细阐述。尤其在当前标配的分布式架构下,CAP理论大棒肆无忌惮的挥舞。时时都要考虑数据一致性。

当然,虽然我们通过一些手段解决缓存的一致性问题,但也仅仅是缓解,并没法根治,它是个面子工程,不然也不会有像Paxos、Raft等一系列强一致性算法出现。

缓存数据一致性的问题,主要是两方面:

一是并发:并发操作引起的数据不一致

二是原子性:操作DB与操作缓存要么都成功,要么都失败

在《百万QPS系统的缓存实践》中已经指出,Cache-Aside pattern是通识,也就是先操作数据库,再删除缓存。

并发

在理论上,会有一种并发问题:

1.缓存中 X 不存在(数据库 X = 1)2.线程 A 读取数据库,得到旧值(X = 1)3.线程 B 更新数据库(X = 2)4.线程 B 删除缓存5.线程 A 将旧值写入缓存(X = 1)

image.png

这个场景需要cache miss、读请求 + 写请求并发、更新数据库 + 删除缓存的时间(步骤 3-4),要比读数据库 + 写缓存时间短(步骤 2 和 5)

但这种现象的概率太低了,数据库写操作时长远远大于读操作的。

原子性

原子性是指,与数据库事务原子性语义相同,数据库与缓存操作怎么捆绑在一起,要么都成功,要么都失败。

如果要强一致,那就需要像二阶段提交协议,还有近年出现的Paxos、Raft。但那就更复杂了,简单的方案可以有几种选择:

1.操作缓存失败,直接回滚数据库,返回失败:想想真如果这么干,首先系统可用性大大降低,其次代码是什么样,挑战极大。2.重试:用户无感,系统重试。可失败后马上重试大概率也是失败,重试多少次,重试过程中,一直占用资源。3.异步重试:类似于Write Back 模式。

但此处的Write Back不能限于内存,一旦重试过程中,服务重启,必然造成数据丢失。因此需要第三方服务来支撑。显然消息队列很适合这种场景

消息队列可靠保障:只要成功写入,不会丢失;并且能成功投递,直到消费成功。

有没有简单点的方案,毕竟引入消息队列有点重。

也有,就是『订阅数据库变更日志,再操作缓存』

通过监听binglog,当数据库变更时,去删除对应的缓存。这样应用程序无需再去主动操作缓存。

怎么订阅binglog呢?也有现成的开源组件,如canal。


到目前为止,还算完美解决了一致性问题。但在互联网行业中,数据库大多采用一主多从的部署方案。也就是在引入缓存前,能增加硬件方式解决时优先考虑增加硬件。

在系统中只要采用数据库主从架构,就有主从本身一致性问题,引入缓存又增加了复杂性。

image.png

1.线程 A 更新主库 X = 2(原值 X = 1)

2.线程 A 删除缓存

3.线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)

4.从库「同步」完成(主从库 X = 2)

5.线程 B 将「旧值」写入缓存(X = 1)

怎么解决呢?类似于解决主从本身问题,可以等待slave同步完数据,延迟再删除一次缓存。

image.png

只是这个延迟间隔,需要依据master slave同步的间隔。

总结

总结一下本文的重点:

1、引入缓存会带来复杂性,甚至负面作用。尽量通过升级硬件来避免不必要的风险。

2、缓存需要考虑“吞吐量”、“命中率”等属性,还需要应对穿透、击穿、雪崩、不致性等问题。

3、Cache-Aside模式可以几乎完美解决单体架构下并发带来的问题。

4、在主从数据库模式下,Cacahe-Aside模式需要延迟双删方案,解决一致性问题。

5、CAP时时在发威,性能与一致性需要权衡。

References

[1] 《百万QPS系统的缓存实践》: https://www.zhuxingsheng.com/blog/cache-practice-of-million-qps-system.html

[2] 聊聊Linux IO: https://www.0xffffff.org/2017/05/01/41-linux-io/


目录
相关文章
|
运维 JavaScript 应用服务中间件
怎么微信WeixinJSBridge.invoke支付成功居然不跳转?还把我页面给关了!这篇文章就告诉你What should I do!
怎么微信WeixinJSBridge.invoke支付成功居然不跳转?还把我页面给关了!这篇文章就告诉你What should I do!
2339 0
怎么微信WeixinJSBridge.invoke支付成功居然不跳转?还把我页面给关了!这篇文章就告诉你What should I do!
|
缓存
npm install 报 npm ERR! cb()never called!的错误
npm install 报 npm ERR! cb()never called!的错误
2128 0
|
3月前
|
人工智能 数据可视化 API
零基础零门槛!OpenClaw 阿里云无影云电脑一键部署、iMessage对接与千问Qwen3.6-Plus配置教程
2026年,OpenClaw(原Clawdbot)作为轻量化、高扩展的AI智能体框架,凭借极简部署、多平台兼容与强大的工具调用能力,成为个人与团队搭建专属AI助理的首选方案。对于零基础用户,**阿里云无影云电脑**提供了官方认证的OpenClaw专属镜像,预装Node.js 22、Git、Homebrew等全部运行依赖,无需复杂环境配置,即可实现“分钟级部署、7×24小时稳定运行”。同时,通过官方imessage-connector插件可无缝对接苹果iMessage平台,搭配**阿里云千问Qwen3.6-Plus**大模型的高性能API,可实现长文本理解、复杂推理、代码生成、多轮对话等核心能力
478 6
|
4月前
|
SQL 运维 监控
MySQL高可用生产落地全解:主从同步、MGR集群、读写分离从原理到实战
本文系统讲解MySQL高可用三大核心:主从复制(含GTID、增强半同步实战)、MGR原生集群(单主模式部署、自动选主、脑裂防护)及读写分离(应用层/ProxySQL方案)。涵盖RTO/RPO指标、故障根因分析、全场景最佳实践与容灾预案,助你构建稳定、高性能、可扩展的生产级高可用体系。
484 3
|
5月前
|
人工智能 资源调度 安全
从豆包手机到 AI OS
从系统工程角度看,“终端形态”正在成为智能体时代的关键节点这指向的是一个更深层的变化:AI 正在从 App 层下沉到操作系统与终端层,终端正在成为智能体的常驻节点。
364 1
|
文字识别 算法 小程序
【项目总结】快瞳医疗化验单的OCR识别
快瞳科技通过图像识别技术,成功解决了医疗化验单OCR识别难题。项目要求精准识别表格内容,尤其是化验数值和名称,准确率达85%以上。针对化验单来源多样、干扰因素多的问题,团队采用智能文档抽取模型、opencv技术(如霍夫变换)进行图片扶正与裁剪,优化识别精度。最终,项目不仅达到药企要求,还实现超越,为医疗行业智能化转型提供了高性价比解决方案,助力快瞳科技在医疗信息化领域树立良好口碑。
1028 43
|
11月前
|
安全 Go 开发者
Go语言实战案例:使用sync.Mutex实现资源加锁
在Go语言并发编程中,数据共享可能导致竞态条件,使用 `sync.Mutex` 可以有效避免这一问题。本文详细介绍了互斥锁的基本概念、加锁原理及实战应用,通过构建并发安全的计数器演示了加锁与未加锁的区别,并封装了一个线程安全的计数器结构。同时对比了Go中常见的同步机制,帮助开发者理解何时应使用 `Mutex` 及其注意事项。掌握 `Mutex` 是实现高效、安全并发编程的重要基础。
|
SQL 分布式计算 大数据
MaxCompute 聚簇优化推荐简介
在大数据计算中,Shuffle 是资源消耗最大的环节之一。MaxCompute 提供聚簇优化推荐功能,通过调整 Cluster 表结构,有效减少 Shuffle 量,显著提升作业性能并节省计算资源。实际案例显示,该功能可帮助用户每日节省数 PB 的 Shuffle 数据量及数千 CU 的计算成本。
383 0
|
Kubernetes 测试技术 微服务
Kruise Rollout 全链路灰度实践
OpenKruise 是一个基于 Kubernetes 的扩展套件,主要聚焦于云原生应用的自动化,比如部署、发布、运维以及可用性防护。本文介绍通过 OpenKruise 构建自动化运维的方式实现基于 Istio 的全链路灰度功能。
48716 252
|
监控 数据可视化 数据挖掘
直播电商复盘全解析:如何通过工具提升团队效率
直播电商作为新兴商业模式,正改变传统零售格局。其成功不仅依赖主播表现和产品吸引力,更需团队高效协作与分工优化。复盘是提升执行力的关键环节,通过总结经验、发现问题、优化流程,结合在线工具如板栗看板,可提升复盘效率。明确团队角色、建立沟通机制、制定优化方案,确保数据驱动决策,从而在竞争中保持领先。