数据库缓存架构深度解析:命中率、一致性、多层缓存管理

喜欢把枯燥的技术文档变成"手把手教程",不讲空话,只讲怎么连、怎么写、怎么优化。


数据库慢,很多人第一反应是"加缓存"。

但"缓存"不是一个东西,是一类东西。数据库内部有 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 辅助数据库性能调优、多租户架构这些话题,跟着我一篇篇学,数据库这块就没问题了。

有问题评论区见。


喜欢把枯燥的技术文档变成"手把手教程"。关注我,数据库这块我们一起搞定。

相关推荐
Ikalus19881 小时前
检查自己写着「这不会让任务失败」:一条 Critical 风险就这么过了 178 天
安全·架构
55873生态系统手记1 小时前
三足鼎立,文明永续:55873 全域文明生态系统的架构师分工、十三套子大典与 Web4 技术底座拆解
架构
姜鱼问生2 小时前
备份和服务器在同一台机器上,等于没备份
算法·架构
搜狐技术产品小编20232 小时前
MVI + Compose 架构实战:从状态混乱到单向数据流
架构
CJi0NG2 小时前
【自用】MySQL-多表查询
数据库·mysql
姜鱼问生2 小时前
别再 chmod 777 了,用 ACL 给它开一扇窗
后端·算法·架构
雨落在了我的手上2 小时前
MySQL数据库基础(12):事务
数据库·mysql
D姐聊门店2 小时前
门店收银系统断网兜底怎么设计:本地账本、离线续传与日终对账的工程实现
数据库·saas
用户8662978480082 小时前
抓包看 KingbaseES 内部协议?官方给了个现成工具
数据库