lectrue6 缓冲池

DBMS负责管理其内存,并负责数据在磁盘与内存之间的来回移动。由于在绝大多数情况下,数据无法直接在磁盘上进行操作,因此任何数据库都必须能够高效地将磁盘中以文件形式表示的数据移动到内存中以便使用。图 1 展示了这一交互过程。从执行引擎的角度来看,最理想的情况是让它感觉所有数据似乎都在内存中,执行引擎不应该需要担心数据是如何被获取进内存的。

解决这一问题的另一种思路是从空间控制和时间控制这两个维度来考虑:

  • 空间控制:指页面在磁盘上的物理写入位置。空间控制的目标是尽可能将经常一起使用的页面在磁盘上物理地靠在一起存储。

  • 时间控制:指何时将页面读入内存以及何时将其写回磁盘。时间控制旨在尽量减少因必须从磁盘读取数据而导致的停顿(Stalls)次数。例如project1中的LRU-K和脏页写回机制就是在进行时间控制。


locks 与 latches

锁:锁是一种高层级的逻辑原语,用于保护数据库的逻辑内容(例如元组、表、数据库)免受其他事务的干扰。锁保护的是用户的业务数据,如果一个事务在更新元组的中途失败了,系统必须能够撤销对该元组的修改,回到初始状态,因此锁必须与事务的原子性挂钩。

  • 持有时间:事务通常会在其整个存续期间持有锁。

  • 可见性:数据库系统可以向用户展示在查询运行期间持有了哪些锁。

  • 回滚支持:锁需要支持回滚 (Rollback) 更改的操作。即保证事务的全无或全有。

闩锁:闩锁是一种底层级的保护原语,DBMS使用它来保护其内部数据结构(例如哈希表、内存区域)中的临界区。

  • 持有时间:闩锁仅在操作执行期间持有。

  • 回滚支持:闩锁不需要支持回滚更改。


缓冲池:缓冲池是用于缓存从磁盘读取的页面的内存缓冲区。它本质上是数据库内部分配的一块大内存区域,用于存储从磁盘获取的页面。

缓冲池的内存区域被组织为一个固定大小页面的数组。数组中的每个条目被称为一个页框 。当 DBMS 请求一个页面时,它会从磁盘被复制到缓冲池的一个页框中。当请求某个页面时,数据库系统会首先搜索缓冲池。只有在找不到该页面的情况下,系统才会从磁盘获取该页面的副本。脏页会被缓冲处理,而不会立即写回磁盘。

注:缓冲池管理只是任意的一些页,它完全无法理解表的概念。

缓冲池元数据:为了高效且正确地使用缓冲池,必须维护特定的元数据。

首先,页表是一个内存中的哈希表,用于追踪当前已在内存中的页面。它将页ID page_id映射到缓冲池中的页框位置 frame_id。由于缓冲池中页面的顺序不一定反映磁盘上的顺序,这个额外的间接层允许系统识别页面在池中的位置。

注意:不要将页表和页目录混淆,页目录是页ID到数据库文件页面位置的映射,页目录的所有更改都必须记录在磁盘上,以便DBMS在重启时能够找到页面。

页表还为每个页面维护额外的元数据:脏页标志和引用计数器。

脏页标志:每当线程修改页面时,都会设置该标志。这向存储管理器指示该页面必须写回磁盘。

引用计数器:追踪当前正在访问该页面(读取或修改)的线程数量。线程在访问页面之前必须增加该计数器。如果一个页面的固定计数(Pin Count)大于零,则存储管理器不允许从内存中驱逐该页面。固定操作并不会阻止其他事务并发访问该页面。

内存分配策略:数据库中的内存根据两种策略分配给缓冲池:

  • 全局策略:关注 DBMS 为使正在执行的整个工作负载获益而应做出的决策。它会考虑所有活跃事务,以找到分配内存的最优决策。

  • 局部策略:做出的决策旨在使单个查询或事务运行得更快,即使这可能对整体工作负载不利。局部策略将页框分配给特定事务,而不考虑并发事务的行为。

大多数系统结合使用全局和局部视图。


缓冲池优化

多缓冲池:DBMS可以维护多个用于不同目的的缓冲池(例如:每个数据库一个缓冲池,或每种页面类型一个缓冲池)。这样,每个缓冲池都可以采用为其内部存储的数据量身定制的局部策略。这种方法有助于减少闩锁争用 (Latch Contention) 并提高数据局部性。

将目标页面映射到特定缓冲池主要有两种方法:对象ID和哈希。

  • 对象 ID:在元组中额外添加一个属性记录所属缓冲池。

  • 哈希:另一种方法是哈希,DBMS 对页面 ID 进行哈希计算,从而选择要访问哪一个缓冲池。

预取:DBMS还可以通过根据查询计划预取页面来进行优化。当第一组页面正在被处理时,第二组页面可以被预先提取到缓冲池中,这种方法通常在DBMS需要顺序访问大量页面时使用。此外,缓冲池管理器也可以预取树形索引数据结构中的叶子页面。

扫描共享/同步扫描:查询游标可以重用从存储或算子计算中检索到的数据。这允许多个查询挂靠到扫描同一张表的单个游标上。如果一个新的查询发起扫描,而系统中已经有一个查询正在扫描这张表,那么 DBMS 会将第二个查询的游标挂靠到现有的游标上。DBMS 会记录第二个查询加入第一个查询时的位置,以便当扫描到达数据结构末尾时,它可以回过头来扫描开始时错过的部分,从而完成整个扫描。

缓冲池绕过:顺序扫描算子不会将获取的页面存储在缓冲池中,以避免造成开销。相反,大扫描会使用局部内存来查询,读完就扔,坚决不进公共缓冲池。如果算子需要读取磁盘上连续的大量页面序列,这种方法效果很好。缓冲池绕过也可以用于临时数据(如排序、连接操作产生的中间数据)。


缓冲池替换策略:当DBMS需要腾出一个页框(frame)以 为新页面腾出空间时,它必须决定从缓冲池中驱逐 (Evict) 哪一个页面。替换策略是 DBMS 实现的一种算法,用于在需要空间时决策将哪些页面从缓冲池中移除。 替换策略的实现目标包括:提高正确性、准确性、速度,并降低元数据开销。

LRU:最近最少使用,LRU 替换策略维护每个页面最后一次被访问的时间戳。DBMS 选择驱逐那个时间戳最早(最老)的页面。这个时间戳可以存储在一个单独的数据结构中(如队列),以便进行排序并通过减少驱逐时的排序时间来提高效率。

时钟算法:CLOCK 策略是 LRU 的一种近似实现,它不需要为每个页面维护单独的时间戳。在 CLOCK 策略中,每个页面被分配一个引用位 (Reference Bit)。当页面被访问时,该位被设置为 1。

为了形象化这一过程,可以将页面组织成一个带有"时钟指针"的环形缓冲区。

  • 指针扫过时,检查页面的引用位是否为 1。

  • 如果是 1,将其置为 0,指针继续移动。相当于为1的页面可以被给予两次机会,如果在一个时钟周期内它又被访问,那么又多一次机会。

  • 如果是 0,则驱逐该页面。

通过这种方式,时钟指针记住了上一次驱逐操作结束时的位置。

LRU和CLOCK策略的问题:容易收到顺寻泛滥的影响,这种现象是指缓冲池的内容由于一次全表顺序扫描而被污染或破坏。

  • 由于顺序扫描会快速读取大量页面,缓冲池很快被填满。

  • 来自其他查询的页面(可能是热门页面)会被驱逐,因为它们的时间戳相对较旧。

  • 在这种场景下,最近的时间戳并不能准确反映我们实际上想要驱逐哪些页面(因为扫描读进来的页可能只读一次就不再用了,但它们却显得很"新鲜")。

有三种更好的解决方案:

  • LRU-K:追踪最近 K 次引用的历史记录作为时间戳,并计算后续访问之间的时间间隔。利用这段历史来预测页面下一次被访问的时间。

  • 每查询局部化:DBMS 基于每个事务/查询来选择驱逐哪些页面。这最大限度地减少了单个查询对整个缓冲池的污染。

  • 优先级提示:允许事务根据查询执行期间每个页面的上下文,告诉缓冲池该页面是否重要。

脏页:处理带有脏页位的页面时直接丢弃非脏页面,只写回脏页。

这两种方法展现了快速驱逐和写回未来不在读取的脏页之间的权衡。避免不必要写回操作的一种方法是后台写入。通过后台写入,DBMS 可以定期扫描页表并将脏页写入磁盘。当一个脏页被安全写入后,DBMS 可以选择驱逐该页面,或者仅仅是清除其脏页标志(将其变为 Clean)。


其他内存池:除了存储元组(Tuples)和索引(Indexes)之外,DBMS 还需要内存来支持其他功能的运行。根据具体的实现方式,这些内存池并不总是像缓冲池那样有磁盘文件作为后端支撑(即它们可能是纯内存的临时结构)。

  • 排序与连接缓冲区 (Sorting + Join Buffers):用于存储查询执行过程中的中间数据。例如,在进行外部归并排序或哈希连接(Hash Join)时,需要内存空间来构建哈希表或存放排序块。

  • 查询缓存 (Query Caches):用于存储 SQL 查询的结果。如果相同的查询再次执行且底层数据未变,DBMS 可以直接返回缓存中的结果,而无需重新解析和执行查询。

  • 维护缓冲区 (Maintenance Buffers):用于执行数据库管理任务,如创建索引(Index Build)、统计信息收集(Analyze)或真空清理操作(Vacuum/GC)。

  • 日志缓冲区 (Log Buffers):用于在将预写日志(WAL)记录刷新到持久化存储(磁盘)之前,先在内存中暂存这些记录。

  • 字典缓存 (Dictionary Caches):用于存储关于数据库对象的元数据(Metadata),例如表定义、列名、数据类型以及权限信息。


操作系统页缓存:大多数磁盘操作都是通过os的API完成的,除非被显式告知执行其他操作,否则操作系统会维护其自有的文件系统缓存,也被称为页缓存。普通I/O时,内核将页缓存上的文件内容复制到用户空间的某一块内存上,双重缓冲的代价是难以接受的。大多数DBMS会使用直接I/O来绕过操作系统的这一层缓存,此时数据直接从磁盘控制器通过DMA(直接内存访问)搬运到用户空间的缓冲池中,内核缓存里完全没有这份数据。这样做是为了避免在内存中存储冗余的数据页副本,并防止需要同时处理两套不同的缓存淘汰策略(即 DBMS 自身的策略与操作系统的策略)。

Postgres是一个使用操作系统页缓存的典型数据库系统案例。


磁盘I/O调度

DBMS会维护一个或多个内部队列,用于跟踪来自整个系统的页面读写请求。任务的优先级通常根据以下几个因素来决定:

  • 顺序 I/O 与 随机 I/O: 调度器会考虑请求的空间局部性。

  • 关键路径任务与后台任务: 例如,用户查询正在等待的数据页属于关键路径,而定期的检查点写入则属于后台任务。

  • 数据类型(表 vs. 索引 vs. 日志 vs. 临时数据): 不同类型的文件对系统的性能和恢复至关重要性不同(例如,预写日志 WAL 通常具有极高的写入优先级)。

  • 事务信息: 涉及锁竞争或高优先级事务的 I/O 请求可能会被优先处理。

  • 基于用户的服务等级协议: 根据用户的级别或重要性分配不同的调度权重。

相关推荐
小唐同学爱学习1 小时前
缓存与数据库一致性问题
java·数据库·spring boot·缓存
Traced back2 小时前
Windows窗体应用 + SQL Server 自动清理功能方案:按数量与按日期双模式
数据库·windows·c#·.net
观远数据2 小时前
在线数据分析网站有哪些?7款自助平台选型指南
大数据·数据库·数据分析
不想写bug呀2 小时前
Redis集群介绍
数据库·redis·缓存
派大鑫wink2 小时前
【Day47】MyBatis 进阶:动态 SQL、关联查询(一对一 / 一对多)
数据库·sql·mybatis
biter00882 小时前
Ubuntu 上搜狗输入法突然“消失 / 只能英文”的排查与修复教程
linux·数据库·ubuntu
何以不说话2 小时前
MyCat实现 MySQL 读写分离
数据库·mysql
齐 飞2 小时前
SQL server使用MybatisPlus查询SQL加上WITH (NOLOCK)
数据库·mysql·sqlserver
_F_y2 小时前
MySQL表的增删查改
android·数据库·mysql