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 内部结构示意
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 两种缓存体系的数据流对比
下图展示了读请求在两种缓存体系下的典型路径,以及可能产生不一致的关键环节。
2.5 故障排查视角的关键要点
- shared_buffers 命中率:通过
pg_stat_bgwriter视图的blks_hit与blks_read计算。命中率低于 90% 通常意味着 shared_buffers 过小或查询计划不佳。 - 外部缓存命中率:Redis 通过
INFO stats的keyspace_hits/keyspace_misses计算;Memcached 通过stats的get_hits/get_misses计算。 - 脏页堆积:
pg_stat_bgwriter中buffers_backend过高说明后端进程被迫自行刷脏页,通常意味着 BgWriter 或 Checkpointer 调度不足。 - 缓存失效风暴:大量 Key 同时过期(Redis)或 Slab 重新分配(Memcached)会导致瞬间数据库负载飙升,需检查 TTL 分布与淘汰策略。
排查建议:在一致性故障场景中,优先对比 shared_buffers 中的脏页时间戳与外部缓存中对应 Key 的最后更新时间。如果外部缓存的数据比 shared_buffers 中的对应数据“更旧”,则说明缓存失效或更新链路存在延迟或遗漏。