在冯诺依曼架构中,数据存储在磁盘上,但必须加载到内存中才能执行操作。因此,如果数据库管理系统希望处理大量数据,它就必须能够高效地在磁盘与内存之间移动数据。这个任务由缓冲池管理器(Buffer Pool Manager)完成。从高层来看,主要实现以下功能:

  1. 物理页(Physical Pages)在内存的缓冲区与持久存储之间来回移动;
  2. 作为一个缓存(Cache)使用,将常用页面保留在内存中以便更快访问,而将不常用或冷却的页面逐出到磁盘上。

从时间、空间两个角度看待:

  1. 空间上:应该将pages写入磁盘的哪个位置。目标是使得常被访问的页面,存储在相近的位置(空间局部性)
  2. 时间上:何时将pages从磁盘读入内存;何时将内存中的pages写回磁盘。

缓冲池管理

缓冲池(Buffer Pool):位于内存中的页面缓存区,用于连接内存与磁盘之间的数据传输。它本质上是数据库内部分配的一块较大的内存区域,用来临时存储页面(Pages)。

组织方式:Buffer Pool是内存中page的缓存,执行引擎请求page(引擎知道对应的数据在哪个page中),先去缓冲池中寻找:如果缓冲池中有该page,则读取;否则缓冲池将执行page的置换算法将请求的page加载到缓冲池中。这实际上跟操作系统中的cache有很大的相似之处。

我们将缓冲池管理器视为一种回写型缓存(Write-Back Cache),即对页面的更改不会立即写回磁盘,而是先缓存在内存中。这与直写型缓存(Write-Through Cache)相反,后者会在每次修改后立即将变更写入磁盘。

Buffer Pool将DBMS主动申请(由OS分配)的内存分为多个frame,每个frame实际上就是磁盘上的一个page的slot,执行时将会把磁盘上的page载入缓存池中的frame上

Page Directory(页目录):将page id映射到数据库文件中的具体位置。(所有对页面目录的更改都必须写入磁盘,以确保数据库在重启后能正确找到页面)

缓冲池Metadata

Page Table(页表):一个位于内存中的哈希表,用于追踪当前已加载到内存中的pages。它将page ID映射到缓冲池中的frame位置。(由于缓冲池中pages的排列顺序不一定与磁盘上的顺序一致,因此这个额外的间接映射层使得系统能够正确定位页面在内存中的位置。)我们通过页表和page id查看请求的page是否在缓存池中。另外,页表还为每个page维护了以下元信息:

  • 脏标志(Dirty Flag):每当一个线程修改page内容时,就会设置这个标志。这表示该page在被驱逐出缓冲池之前,必须先写回磁盘
  • 固定/引用计数器(Pin/Reference Counter):记录当前正在访问该页面的线程数(无论是读取还是修改)。每个线程在访问page之前,必须先将该计数器加一。如果某个page的“pin count”大于 0,说明它正在被使用,那么缓冲池管理器不允许将该page驱逐出内存。(固定某个page,不会阻止其他事务同时访问它,即允许并发访问)

如果缓冲池已满,且不存在未被固定的页面用于替换,则系统抛出OOM错误。

页目录 & 页表

页目录(Page Directory):将页面ID映射到数据库文件中实际位置的映射关系。

  • 所有对页目录的更改都必须写入磁盘,因为页目录需要告诉执行引擎数据在哪个page的slot,因此必须持久化,以确保数据库在重启后仍然能找到页面的位置。

页面表(Page Table):将页面ID映射到缓冲池中页面副本所在frame的位置的映射关系。

  • 这是一个仅存在于内存中的哈希表,不需要持久化存储在磁盘上(丢失了重建一个就好)

简而言之:页目录是磁盘上的真实位置索引,持久化保存;页表是内存中的缓冲池映射表,临时使用。

OS & DBMS

Locks vs Latches

Locks更高级的逻辑原语

  • 用于保护数据库中的逻辑内容,如元组(tuple)、表(table)、整个数据库等,防止其他事务并发访问或修改;
  • 锁的持有时间是整个事务的持续时间
  • 锁的存在对用户是可见的,例如运行查询时,数据库系统可以告诉你当前持有哪些锁;
  • 因为事务可能会失败或被回滚,所以需要支持回滚更改

Latches底层的保护原语(可使用自旋锁)

  • 是一种底层的保护机制,用于保护数据库内部数据结构的关键区域,如哈希表、内存区域等,防止多个线程同时修改造成破坏。
  • 用于保护数据库内部数据结构中的关键代码段,防止其他线程并发修改;
  • 生命周期只持续到该操作完成,不需要长时间持有;
  • 因为只保护内部状态,不涉及逻辑数据变更,不需要支持回滚

为什么不使用OS?

为什么不直接用 OS的页缓存(比如 mmap)来管理DBMS中的pages呢?

虽然 mmap 等系统调用能将文件直接映射到内存中,让 OS 负责页面的加载/驱逐,但对于追求高性能与强事务一致性的数据库来说,存在以下严重问题:

  1. 事务安全性差:OS可能随时将脏页刷新到磁盘,打破事务的原子性和一致性;
  2. I/O延迟不可控:如果采用OS,那么DBMS无从得知哪些pages已经存入内存;一旦发生缺页,线程可能会因页面错误而被阻塞;
  3. 错误处理复杂:如果访问非法页面,OS可能抛出SIGBUS信号,导致数据库必须额外处理底层异常;
  4. 性能问题:操作系统内部数据结构可能产生竞争;页表更新引发的 TLB shootdown 会降低多核系统性能。

为什么DBMS更倾向于自行管理内存? 1. 更好地控制脏页刷新顺序:保证事务日志优先、数据页随后写入; 2. 实现更智能的预取策略:基于访问模式预测数据; 3. 更合理地调度线程/进程:控制并发与性能瓶颈。