为什么需要缓冲池?

磁盘 IO 是数据库最主要的性能瓶颈。一次磁盘随机读取约为 0.1-1ms,而内存访问仅需 ~100ns。缓冲池将热点页面缓存在内存中,减少磁盘访问。

缓冲池架构

┌────────────────────────────────────────────┐
│              缓冲池 (Buffer Pool)           │
│                                            │
│  ┌──────┐ ┌──────┐ ┌──────┐     ┌──────┐  │
│  │Frame 0│ │Frame 1│ │Frame 2│ ... │Frame N│ │
│  │ Page  │ │ Page  │ │ Free  │     │ Page  │ │
│  │  #102 │ │  #55  │ │       │     │  #204 │ │
│  │usage=3│ │usage=1│ │       │     │usage=2│ │
│  └──────┘ └──────┘ └──────┘     └──────┘  │
│                                            │
│  ┌──────────────────────────────┐          │
│  │        Clock 替换算法         │          │
│  │    当前指针 → Frame 2        │          │
│  └──────────────────────────────┘          │
└────────────────────────────────────────────┘

页面生命周期

[磁盘] ──read──→ [Buffer Pool 空闲帧]

                     ├── Pin (引用计数+1)


               [活跃页面]

                     ├── Unpin (引用计数-1)


               [可替换页面]

              ┌──────┴──────┐
              │              │
         (被替换)      (再次 Pin)
              │              │
              ▼              ▼
         [磁盘]        [活跃页面]

替换算法

Clock 算法

详见 从源码看 PostgreSQL Clock-sweep 算法

核心思想:将缓冲池视为环形缓冲区,维护一个时钟指针。每次需要替换时:

  1. 推进指针
  2. 如果当前页 usage_count > 0:递减计数,继续扫描
  3. 如果 usage_count == 0:选定为 victim,写出脏页后替换

LRU-K

改进版 LRU,记录最后 K 次访问的时间戳,预测下一次访问时间。比纯 LRU 更能避免顺序扫描污染缓存。

并发控制

缓冲池的并发控制需要平衡性能与正确性:

机制 粒度 用途
Buffer 锁 (Latch) 单帧 保护页面内容读写
分区锁 多个帧 减少全局锁争用
无锁设计 基于 CAS 高性能场景

PostgreSQL 的缓冲池采用每帧 LWLock + 原子状态位的混合方案。

预取 (Prefetching)

对于顺序扫描、索引扫描等可预测的访问模式,提前加载可能需要的数据页:

顺序扫描预取:
Scan Page 0 → 后台加载 Page 1-3
Scan Page 1 → 命中缓存 ✓
Scan Page 2 → 命中缓存 ✓

总结

  • 缓冲池是数据库性能的核心支柱
  • Clock 算法是 LRU 在并发环境下的实用替代
  • 多级锁设计(分区锁 + 帧锁)是关键并发优化
  • 预取可显著提升顺序扫描性能

参考