喜欢把枯燥的技术文档变成"手把手教程",不讲空话,只讲怎么连、怎么写、怎么优化。
数据库慢,很多人第一反应是"加缓存"。
但"缓存"不是一个东西,是一类东西。数据库内部有 Buffer Pool、查询缓存、物化视图;中间层有 Redis、Memcached;应用层有本地缓存、分布式缓存。名字都叫缓存,解决的问题完全不同。
缓存加得越多,架构越复杂,问题也越多:缓存和数据不一致、缓存穿透、缓存雪崩、缓存击穿。缓存不是万能药,加错了反而更慢。
今天把数据库缓存的完整架构讲清楚。每一层缓存解决什么问题、什么时候该加、什么时候不该加、多层缓存怎么避免不一致。
01 数据库内部的缓存层
这是最基础的一层,数据库自己管理的缓存。不需要额外部署,不需要改应用。
Buffer Pool(缓冲池)
Buffer Pool 是数据库把磁盘数据读到内存里的机制。查询数据时,先看 Buffer Pool 有没有,有就直接返回(缓存命中),没有再去磁盘读。
scss
-- 查看 Buffer Pool 命中率
SELECT sum(heap_blks_read) AS disk_reads,
sum(heap_blks_hit) AS cache_hits,
round(sum(heap_blks_hit) * 100.0 /
(sum(heap_blks_read) + sum(heap_blks_hit)), 2) AS hit_rate
FROM pg_statio_user_tables;
命中率低于 95% 说明 Buffer Pool 不够大,热点数据无法全部缓存。调大 shared_buffers 可以缓解,但前提是服务器内存够。
踩坑提醒:Buffer Pool 不是越大越好。超过物理内存的 40% 会挤压 OS 的页缓存空间,反而降低整体性能。一般设为总内存的 25-40%。
在实际项目中,Buffer Pool 的调优不是一次配置就完事的。金仓 KES 在内存管理方面做了不少增强------除了标准的 shared_buffers,还引入了 os_cache_size 参数,可以动态控制 OS 页缓存的使用比例。这在做大表全表扫描的场景下特别有用:把 os_cache_size 调大,让操作系统缓存更多数据,减少磁盘 IO。加上 KEMCC 监控平台可以实时展示 Buffer Pool 命中率、脏页比例、页面淘汰率,运维人员不用自己写脚本采集数据,直接在监控面板上看到缓存是否够用。
查询缓存
有些数据库(如 MySQL)有查询缓存功能------相同的查询直接返回缓存结果,不重新执行。但查询缓存有很多限制:
- 任何写入操作会让相关表的查询缓存全部失效
- 高并发写入场景下,查询缓存几乎无效
- 查询必须是完全相同的 SQL 字符串(参数不同也算不同查询)
PostgreSQL 早期版本有查询缓存,后来移除了------因为实际场景中命中率太低,维护成本高于收益。
物化视图(Materialized View)
物化视图是预计算的查询结果,存在磁盘上。查询物化视图比执行原始查询快得多。
sql
-- 创建物化视图
CREATE MATERIALIZED VIEW mv_daily_sales AS
SELECT date, product_id, SUM(amount) AS total_sales
FROM orders
GROUP BY date, product_id;
-- 刷新物化视图
REFRESH MATERIALIZED VIEW mv_daily_sales;
-- 并发刷新
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_sales;
物化视图适合读多写少、数据变化不频繁的场景。比如日报表、统计看板。刷新频率按业务需求定,可以每天一次、每小时一次,或者按事件触发。
踩坑提醒:物化视图的并发刷新需要表上有唯一索引,否则无法使用 CONCURRENTLY。创建物化视图时就要规划好唯一键。
并发刷新是物化视图能否在生产环境使用的关键能力------刷新期间不能阻塞查询,否则报表用户在刷新期间看到的就是空白页面。金仓 KES 支持物化视图的并发刷新(CONCURRENTLY),刷新期间不阻塞读查询。在政务报表类场景中,这个能力很重要:刷新物化视图的时候,用户还在看报表,不能因为刷新就中断查询。配合定时任务(如 pg_cron),可以按固定频率自动刷新,减少人工运维成本。
02 中间层缓存:Redis/Memcached
当数据库内部缓存不够用时,需要在数据库和应用之间加一层缓存。
什么时候需要中间层缓存
| 场景 | 数据库内部缓存够不够 | 需要中间层缓存吗 |
|---|---|---|
| 单表高频查询 | Buffer Pool 命中率高,够 | 不需要 |
| 复杂 JOIN 查询频繁 | 不够,每次都要重新计算 | 需要 |
| 热点数据(爆款商品、热点新闻) | 不够,并发太高 | 需要 |
| 读多写少的配置数据 | 够,但跨库共享不方便 | 看情况 |
缓存策略
Cache-Aside(旁路缓存) :应用先查缓存,没有再查数据库,然后写入缓存。
markdown
应用 → 查 Redis
↓ 未命中
查数据库 → 写回 Redis → 返回
最常见的模式。代码简单,但存在缓存和数据库不一致的窗口期。
Write-Through(写穿透) :写入时同时写缓存和数据库。
应用 → 写 Redis → 写数据库
数据一致性好,但写入性能受影响(要等两个写操作都完成)。
Write-Behind(写回) :写入只写缓存,缓存异步批量刷到数据库。
markdown
应用 → 写 Redis
↓ 异步
批量刷到数据库
写入性能最高,但数据丢失风险最大(缓存挂了,数据没刷到数据库)。
缓存一致性
中间层缓存最大的问题是和数据库不一致。
写入时更新缓存:
markdown
1. 更新数据库
2. 删除缓存(不是更新缓存)
3. 下次查询时重新加载
用"删除"而不是"更新"的原因是:更新缓存需要重新计算查询结果,成本高。删除缓存让下次查询重新加载,更简单。
设置合理的过期时间:不要设太短(频繁查数据库),也不要设太长(数据长时间不一致)。通常 5-30 分钟。
03 读写分离:读查询的分流
读写分离本质上也是一种缓存策略------把读查询分流到备库,减轻主库压力。
markdown
应用 → 代理层
→ 主库(写)
→ 备库1(读)
→ 备库2(读)
什么时候需要读写分离
- 主库的读查询占了 70% 以上的负载
- 写入压力还在单机可承受范围内
- 备库可以通过流复制保持数据同步
读写分离不是分库分表的前一步。它的适用场景是读压力不均,写入和查询竞争同一套资源。
复制延迟的影响
备库的数据是通过流复制从主库同步过来的,有延迟(通常毫秒到秒级)。这意味着备库读到的可能是旧数据。
对实时性要求高的查询(如余额查询、库存查询)不能走备库,必须走主库。对实时性要求不高的查询(如报表、历史记录查询)可以走备库。
金仓 KES 的流复制支持同步和异步两种模式。同步复制保证备库和主库数据一致,但写入性能受影响(必须等备库确认才能返回);异步复制性能好,但有丢数据风险(主库挂了,备库还没同步到最新数据)。在读写分离场景中,通常备库用异步复制,通过监控复制延迟来确保数据新鲜度。如果业务对一致性要求更高,也可以对关键备库使用同步复制,非关键备库用异步------金仓支持一主多备的混合模式,不同备库可以配置不同的同步策略。
代理层的选型也很关键。Pgpool-II 提供查询路由、连接池、负载均衡一体化方案,适合中小型架构;如果规模更大,也可以用 HAProxy + Keepalived 做四层负载均衡,配合应用层的读写路由逻辑。核心原则是:读路由要能识别查询类型(SELECT 走备库,INSERT/UPDATE/DELETE 走主库),对实时性要求高的查询强制走主库。
04 多层缓存架构:怎么避免不一致
当 Buffer Pool、物化视图、Redis、读写分离都上了之后,数据可能存在于多个地方。更新数据库时,怎么保证所有缓存都更新?
架构全景
markdown
应用层本地缓存
↓
Redis/Memcached 缓存
↓
数据库物化视图
↓
数据库 Buffer Pool
↓
磁盘数据
不一致的风险
| 层级 | 不一致风险 | 典型延迟 |
|---|---|---|
| 应用本地缓存 | 高(多个应用实例之间不同步) | 秒到分钟 |
| Redis | 中(删除策略 + 过期时间可控制) | 秒 |
| 物化视图 | 低(刷新周期可控制) | 分钟到小时 |
| 备库(读写分离) | 低(流复制延迟) | 毫秒到秒 |
管理建议
不要加不必要的层。 每一层缓存都增加了一致性风险。Buffer Pool 够用就不要加 Redis,Redis 够用就不要加物化视图。从最底层开始优化,扛不住了再加上一层。
缓存的刷新策略要统一。 哪一层用什么策略(删除、更新、过期),要有明确的规范。不能 Redis 用删除策略、物化视图用手动刷新、备库不监控延迟------这样出了问题根本不知道是哪一层的数据不一致。
监控每一层的命中率。 Buffer Pool 命中率、Redis 命中率、物化视图刷新频率------这些指标告诉你缓存有没有起作用。加了缓存但命中率很低,说明缓存策略有问题,要调整。金仓 KES 的 KEMCC 监控平台可以集中采集 Buffer Pool 命中率、流复制延迟、物化视图刷新状态等关键指标,配合告警规则(比如 Buffer Pool 命中率低于 90% 告警、流复制延迟超过 10 秒告警),运维人员能第一时间发现缓存层的问题,而不是等到用户投诉才排查。
对比:各层缓存的效果和代价
| 缓存层 | 部署成本 | 一致性风险 | 性能提升 | 适用场景 |
|---|---|---|---|---|
| Buffer Pool | 无(数据库自带) | 无 | 高(命中时) | 所有场景的基础 |
| 查询缓存 | 低 | 高 | 低(写入频繁时) | 读多写少的简单查询 |
| 物化视图 | 低 | 低 | 高(预计算结果) | 报表、统计、读多写少 |
| Redis/Memcached | 中 | 中 | 极高 | 热点数据、高并发读 |
| 读写分离 | 中 | 低 | 高(读分流) | 读压力大的场景 |
| 应用本地缓存 | 低 | 极高 | 极高 | 配置数据、字典数据 |
决策框架:按场景选择缓存策略
| 场景特征 | 推荐缓存层 | 说明 |
|---|---|---|
| Buffer Pool 命中率 > 95% | 不需要额外缓存 | 数据库内部缓存够用 |
| Buffer Pool 命中率 < 95% | 调大 shared_buffers | 先优化内部缓存 |
| 复杂 JOIN 查询频繁 | 物化视图 | 预计算避免重复 JOIN |
| 热点数据高并发读 | Redis + Cache-Aside | 分流数据库压力 |
| 报表类查询 | 物化视图 | 定时刷新,读性能极高 |
| 配置/字典数据 | 应用本地缓存 + 过期时间 | 数据变化少,本地缓存最快 |
| 读压力 > 写压力 3 倍 | 读写分离 | 读查询分流到备库 |
| 写入压力大 + 读压力大 | 读写分离 + Redis | 写走主库,读走备库 + 缓存 |
深度分析:为什么缓存越多越容易出问题
缓存的本质是"用空间换时间"------多存一份数据,少一次查询。但多存一份数据,就多了一份不一致的风险。
第一,缓存失效的时序问题。 先更新数据库还是先删除缓存?先删除缓存还是先更新数据库?两种顺序在并发场景下都会出现不一致的窗口期。
第二,缓存雪崩。 大量缓存在同一时间过期,所有查询都打到数据库,数据库瞬间被打挂。解法是给过期时间加随机偏移。
第三,缓存穿透。 查询一个不存在的数据,缓存没有,数据库也没有,每次都打到数据库。解法是布隆过滤器或者缓存空值。
第四,缓存击穿。 热点数据的缓存过期瞬间,大量并发查询打到数据库。解法是互斥锁或者热点数据永不过期。
缓存架构的设计难度不在"怎么加缓存",在"怎么管理缓存的生命周期"。加缓存容易,管理缓存难。每一层缓存都要有明确的失效策略、刷新周期、监控指标------没有这些,缓存越多,系统越不可控。
总结
数据库缓存不是一层,是一组。Buffer Pool 是基础,物化视图处理复杂查询,Redis 处理热点高并发,读写分离分流读压力。每一层解决不同的问题,叠加使用要管理好一致性。
加缓存的原则是:从最底层开始优化,扛不住了再加上一层。不要一上来就加 Redis,先看看 Buffer Pool 命中率、有没有建对索引、SQL 能不能优化。
缓存越多越容易出问题。一致性风险、雪崩、穿透、击穿------每个都是实打实的运维挑战。加了缓存就要管好缓存的生命周期,否则缓存不是加速剂,是定时炸弹。
后续我会继续分享AI 辅助数据库性能调优、多租户架构这些话题,跟着我一篇篇学,数据库这块就没问题了。
有问题评论区见。
喜欢把枯燥的技术文档变成"手把手教程"。关注我,数据库这块我们一起搞定。