为什么需要缓冲池?
磁盘 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 算法。
核心思想:将缓冲池视为环形缓冲区,维护一个时钟指针。每次需要替换时:
- 推进指针
- 如果当前页
usage_count > 0:递减计数,继续扫描 - 如果
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 在并发环境下的实用替代
- 多级锁设计(分区锁 + 帧锁)是关键并发优化
- 预取可显著提升顺序扫描性能
参考
- PostgreSQL 源码: bufmgr.c
- MySQL InnoDB: Buffer Pool
- Alex Petrov, Database Internals, Chapter 2