TL;DR
PostgreSQL 使用 Clock-sweep(时钟扫描)算法管理 shared_buffers 中的页面替换,而非经典的 LRU。这篇文章带你逐行走读 src/backend/storage/buffer/bufmgr.c 中的核心实现。
为什么不用 LRU?
LRU 需要维护一个全局有序链表,在并发环境下锁争用严重。PostgreSQL 采用 Clock-sweep:一个环形缓冲区 + 一个时钟指针,每次扫描时递减 usage_count,找到第一个 usage_count == 0 的页面即被替换。
核心数据结构
// src/include/storage/buf_internals.h
typedef struct BufferDesc {
BufferTag tag; // 页面标识
pg_atomic_uint32 state; // 状态位
int buf_id; // 缓冲区索引
int freeNext; // 空闲列表指针
LWLock content_lock; // 内容锁
} BufferDesc;
usage_count 被编码在 state 原子变量中,每个页面最大引用计数为 5(BM_MAX_USAGE_COUNT)。
Clock-sweep 执行流程
// src/backend/storage/buffer/bufmgr.c
static BufferDesc *
StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state) {
for (;;) {
// 1. 推进时钟指针
StrategyControl->nextVictimBuffer++;
if (StrategyControl->nextVictimBuffer >= NBuffers)
StrategyControl->nextVictimBuffer = 0;
buf = GetBufferDescriptor(StrategyControl->nextVictimBuffer);
// 2. 递减 usage_count
local_buf_state = LockBufHdr(buf);
usage_count = BUF_STATE_GET_USAGECOUNT(local_buf_state);
if (usage_count > 0) {
local_buf_state -= BUF_USAGECOUNT_ONE; // usage_count--
// 继续扫描
} else {
// usage_count == 0 → 选定为 victim
*buf_state = local_buf_state;
return buf;
}
}
}
算法关键点
- 时钟指针绕圈:每圈扫描都会递减
usage_count,经过BM_MAX_USAGE_COUNT圈后usage_count必然归零 - NUMA 友好:没有全局有序结构,锁粒度在单个 buffer 级别
- pin 计数独立:被 pin 住的页面不会被替换,
usage_count递增在 pin 时发生
访问模式下的行为
场景:顺序扫描
↓(每页只访问一次)
usage_count: 1 → clock 经过 → 0 → 下一圈被淘汰 ✓
场景:热点页面(索引根节点)
↓(频繁访问)
usage_count: 1 → clock 经过 → 0 → 再被 pin → 1
→ 一直保持在缓存中 ✓
这与 LRU-K 的思想类似:只访问一次的数据很快被淘汰,频繁访问的数据持续保留。
总结
PostgreSQL 的 Clock-sweep 是 LRU 在并发环境下的工程优化,核心思想是“用环形扫描代替排序,用多圈递减代替精确排序”。此算法也广泛用于操作系统的页面替换中。