2. 缓存基础回顾:PostgreSQL内置缓存(shared_buffers)与外部缓存(Redis/Memcached)的工作原理

在深入排查缓存一致性故障之前,必须清晰理解两种核心缓存体系的工作机制:PostgreSQL 进程内部的原生缓存(shared_buffers)与独立部署的外部缓存层(Redis / Memcached)。两者的架构定位、数据生命周期、一致性模型存在本质差异,这些差异正是故障产生的根源。

2.1 PostgreSQL 内置缓存:shared_buffers

shared_buffers 是 PostgreSQL 共享内存中的一块区域,用于缓存数据页(Data Pages)。所有后端进程(Backend Process)通过它读写磁盘上的表数据与索引。

核心工作原理

  • 页面级缓存:以 8KB 的固定页面为单位,缓存从堆表、索引、自由空间映射等读取的数据。
  • 时钟扫描(Clock Sweep)淘汰:当缓存满时,使用近似 LRU 算法——时钟扫描——选择淘汰页。每个缓存页有一个 usage_count,扫描时递减,减到 0 则被淘汰。
  • 脏页回写(BgWriter / Checkpointer):被修改但未写盘的页面称为脏页。BgWriter 进程周期性地将脏页刷入操作系统页缓存;Checkpointer 在检查点发生时强制将所有脏页落盘。
  • WAL 先行:任何数据页修改前,必须先写 WAL 日志(Write-Ahead Logging),保证崩溃恢复时不会丢失已提交事务的修改。
关键参数:shared_buffers 通常设置为物理内存的 15%~25%。过小会导致频繁的磁盘 I/O;过大则可能引发操作系统双缓冲(OS Page Cache + shared_buffers)竞争。

shared_buffers 内部结构示意

PostgreSQL shared_buffers 内部结构 PostgreSQL 共享内存 — shared_buffers 结构 缓存槽 1 表 OID: 16384 页号: 42 usage_count: 3 脏标记: 是 缓存槽 2 表 OID: 16385 页号: 7 usage_count: 1 脏标记: 否 缓存槽 N 表 OID: 16386 页号: 15 usage_count: 0 脏标记: 否 指针 时钟扫描指针循环扫描缓存槽,递减 usage_count;当 usage_count 降为 0 时,该槽可被新页面覆盖。

2.2 外部缓存:Redis 与 Memcached

外部缓存独立于 PostgreSQL 进程运行,通常部署在应用层与数据库之间,用于减轻数据库读压力、加速热点数据访问。

Redis 核心特征

  • 数据结构丰富:支持 String、Hash、List、Set、Sorted Set 等,适合复杂业务缓存。
  • 持久化可选:RDB 快照 + AOF 日志,但缓存场景通常关闭持久化以追求性能。
  • 淘汰策略:支持 LRU、LFU、TTL 过期、随机淘汰等,通过 maxmemory-policy 配置。
  • 单线程事件循环:原子性执行命令,天然避免并发竞争,但大 Key 会阻塞。

Memcached 核心特征

  • 纯 KV 缓存:仅支持简单的 key-value 存储,value 最大 1MB。
  • 多线程架构:利用多核 CPU,适合高并发读场景。
  • 无持久化:数据全部在内存中,重启即丢失。
  • LRU 淘汰:每个 Slab Class 内部独立 LRU,可能引发 slab 失衡问题。
对比维度 PostgreSQL shared_buffers Redis Memcached
缓存粒度 数据页(8KB) 对象(String/Hash 等) KV 对(最大 1MB)
数据来源 磁盘表/索引文件 应用写入或回填 应用写入或回填
一致性模型 事务内可见性 + WAL 保证 最终一致性(需应用控制) 最终一致性(需应用控制)
淘汰算法 时钟扫描(近似 LRU) LRU / LFU / TTL / 随机 Slab 内 LRU
持久化 WAL + 检查点 可选(RDB/AOF) 无
并发模型 多进程 + 共享内存锁 单线程事件循环 多线程(事件驱动)

2.3 缓存一致性问题的根源

当应用同时使用 PostgreSQL 与外部缓存时,数据可能在不同缓存层之间产生不一致。典型场景包括:

  • 更新丢失:应用更新数据库后,未及时失效或更新外部缓存,导致旧数据被读取。
  • 并发写冲突:多个实例同时写数据库与缓存,时序错乱导致缓存中残留旧值。
  • 缓存穿透/击穿/雪崩:缓存失效瞬间大量请求直接打到数据库,引发性能抖动。
  • 主从延迟:应用写入主库后立即从缓存读取,但缓存数据来自从库(延迟未同步)。
常见误区:不要依赖外部缓存的过期时间(TTL)来解决一致性问题。TTL 只能保证最终一致,无法避免窗口期内的脏读。在严格一致性要求的业务中(如库存、余额),应优先考虑“先更新数据库,再删除缓存”的模式(Cache-Aside 的失效策略)。

2.4 两种缓存体系的数据流对比

下图展示了读请求在两种缓存体系下的典型路径,以及可能产生不一致的关键环节。

读请求缓存路径对比 读请求缓存路径对比 PostgreSQL 内置缓存 应用读请求 shared_buffers 磁盘数据页 WAL 日志 外部缓存(Redis / Memcached) 应用读请求 Redis/Memcached 数据库回填 TTL 过期/淘汰 左侧:shared_buffers 命中则直接返回,未命中则从磁盘加载并缓存。右侧:外部缓存未命中时回填数据库,依赖 TTL 或主动失效保证一致性。

2.5 故障排查视角的关键要点

  1. shared_buffers 命中率:通过 pg_stat_bgwriter 视图的 blks_hit 与 blks_read 计算。命中率低于 90% 通常意味着 shared_buffers 过小或查询计划不佳。
  2. 外部缓存命中率:Redis 通过 INFO stats 的 keyspace_hits / keyspace_misses 计算;Memcached 通过 stats 的 get_hits / get_misses 计算。
  3. 脏页堆积:pg_stat_bgwriter 中 buffers_backend 过高说明后端进程被迫自行刷脏页,通常意味着 BgWriter 或 Checkpointer 调度不足。
  4. 缓存失效风暴:大量 Key 同时过期(Redis)或 Slab 重新分配(Memcached)会导致瞬间数据库负载飙升,需检查 TTL 分布与淘汰策略。
排查建议:在一致性故障场景中,优先对比 shared_buffers 中的脏页时间戳与外部缓存中对应 Key 的最后更新时间。如果外部缓存的数据比 shared_buffers 中的对应数据“更旧”,则说明缓存失效或更新链路存在延迟或遗漏。