数据库:页缓存
大多数数据库系统都会同时使用两种存储介质:磁盘(读写速度较慢)和内存(读写速度较快)。为了减少对磁盘的访问,提高系统性能,数据库通常会将部分数据页缓存在内存中,这种机制称为页缓存或缓冲池(Buffer Pool)。
缓存在线程读写中的作用
在处理读请求时,若目标数据所在的页已缓存在内存中,数据库会直接返回该页中的数据;若数据页尚未加载至内存,则需先从磁盘读取该页并加载到内存中,随后再响应客户端请求。这样一来,后续对该页的访问就可以直接通过内存完成,大幅降低磁盘 I/O。
在处理写请求时,数据库同样依赖缓存机制。许多数据库使用 B+ 树结构来组织数据,写入操作(如插入、修改)可能引发页分裂或页合并,频繁执行这些操作会严重影响性能。为此,数据库会先在缓存中对数据页进行修改,而不是立刻写入磁盘。随后,系统会在合适的时机将这些脏页(被修改但尚未落盘的内存页)刷新(flush)到磁盘。这种策略不仅能将多次磁盘写操作合并为一次,还能显著减少磁盘写入频率,提升整体性能。
故障恢复机制
一旦数据页被修改,它就变成了脏页,此时内存中的数据与磁盘上的数据处于不一致状态。如果此时服务器发生故障(如断电或重启),未刷盘的脏页将会丢失。而作为一款支持事务的数据库系统,数据的持久性是必须保障的——已提交的数据绝不能因为故障而丢失。
为解决这一问题,数据库引入了预写日志机制(WAL,Write-Ahead Log),这也是大多数数据库保障崩溃恢复能力的核心手段。
当数据库收到写请求时,系统会先将操作以追加方式写入 WAL 日志文件,然后再修改内存中的数据页。由于 WAL 日志是顺序写入磁盘的,写入效率远高于随机写。此外,操作系统自身也具备文件缓存机制,使得写 WAL 的性能损耗可控。
如果你对性能要求较高,可以选择异步 flush,即写入 WAL 后不立即执行 fsync 刷盘操作(只要系统未崩溃,日志仍在文件缓存中)。但如果你对持久性要求更高,建议在每次记录日志后立即执行 flush 操作,以避免任何数据丢失的风险。
WAL 还会定期维护checkpoint(检查点),记录当前哪些数据页已经被刷新到磁盘。写操作越频繁,WAL 文件增长越快。当 WAL 超过设定阈值时,系统将强制触发刷脏页操作。随着刷新进度推进,checkpoint 也会不断更新。若系统发生崩溃,数据库就会从上次 checkpoint 开始,通过 WAL 回放日志,恢复到最新状态。
缓存置换策略
由于内存空间有限,不可能将所有数据页永久保存在缓存中。当需要加载新页但内存已满时,系统必须淘汰部分旧页或将脏页刷盘,以释放空间加载新数据。这一过程称为缓存置换(Cache Replacement)。
选择淘汰哪些页是一项关键决策。若误将高频访问的热页淘汰,后续又需要频繁加载它们,反而会增加磁盘负担,降低性能。理想状态是让高频访问的热页常驻内存,而将冷数据置换出去。
下面介绍几种常见的缓存置换算法:
FIFO(先进先出) 最简单的策略,按加载顺序组成队列,新页加入队尾,淘汰时从队头开始。但该策略不考虑访问频率,容易将热点页误踢,实际使用较少。
LRU(最近最少使用) 维护一个队列,每次访问某页都会将其移至队尾,最近未使用的页位于队头。相比 FIFO 更合理,但仍存在“缓存污染”问题:短时间内大量访问新数据页可能导致热页被淘汰。
LFU(最不经常使用) 为每个页维护一个访问计数器,优先淘汰访问频率最低的页。策略更精准,但需要额外的统计和更新开销。
一个优秀的缓存置换策略,能在有限内存中发挥出最大的效能,是数据库性能优化的重要组成部分。