SQLite源码分析6 分页和缓存#
原理#
磁盘之上是page层。
数据库文件以页为单位被分割。每页都有编号。
频繁操作页比如频繁去读某些页,就会造成频繁操作磁盘,如果把数据放入内存,速度会以数量级提高。
但是很多情况不可能把整个数据库文件都放入内存。
那么需要做个有限大小的缓存。每次读页,先去查缓存,如果缓存里没有,才去读磁盘。这样减少了频繁的磁盘io。每个页对应有一个页缓存(page cahce)。
cache状态可为clean,表示缓存中的数据与磁盘一致。即干净的,不需要处理。 状态可为dirty,即缓存数据被改过,是脏的,需要同步到磁盘。page层之上是b树。b树都是用pager的接口,不直接操作磁盘。
这次分析分页和缓存相关流程。
定义#
页结构体。包含单个页的信息。每个页对应一个实体。可以有大量实体。
struct PgHdr {
...
void *pData; // 页数据
void *pExtra; // 其他数据
PCache *pCache; // 该页的所属的缓存总管
PgHdr *pDirty; // 脏页链表
Pager *pPager; // 所属的分页器
Pgno pgno; // 页号
PgHdr *pDirtyNext; // 下一个脏页
PgHdr *pDirtyPrev; // 前一个脏页
u16 flags; // 各种标志位。PGHDR_CLEAN、PGHDR_DIRTY、PGHDR_NEED_SYNC等等。
}
cache总管。只有一个实体。在sqlite3PagerOpen中生成。
struct PCache {
...
PgHdr *pDirty, *pDirtyTail; // 脏页列表。pDirty指向头,pDirtyTail指向尾。
int szPage; // 页大小
int nRefSum; // 引用计数
sqlite3_pcache *pCache; // 这里又做了一层,实现底层的cache逻辑,比如按页号搜索的算法、分配cache等。代码在pcache1.c。
}
Pager相当于一个分页总管。存各种配置和分页的总体信息。只有一个实体,在sqlite3PagerOpen中生成。
struct Pager {
...
int (*xGet)(Pager*,Pgno,DbPage**,int); // 获取页实际数据的函数。
sqlite3_file *fd; // 数据库文件fd
sqlite3_file *jfd; // 主日志fd
sqlite3_file *sjfd; // 副日志fd
PCache *pPCache; // cache总管
PagerSavepoint *aSavepoint; // 记录点
int nSavepoint; // 记录点数量
char *zFilename; // 数据库文件名
char *zJournal; // 日志文件名
int pageSize; //每页的字节数
}
常用流程#
打开数据库#
openDatabase
sqlite3_initialize
sqlite3PcacheInitialize
sqlite3PCacheSetDefault
把xFetch配置为pcache1Fetch
sqlite3BtreeOpen
sqlite3PagerOpen
打开指定的数据库文件.生成一个Pager实体,存到BtShared.pPager.
sqlite3PcacheOpen(pagerStress)
setGetterMethod
pPager->xGet = getPageNormal; // 配置xGet
sqlite3PagerReadFileheader
sqlite3OsRead
会通过os适配层最终对应到比如说unix的read
xGet通常配置为getPageNormal。具体实现了获取页数据。
大致是按页号查找对应的缓存,找到就直接返回。找不到时可能按LRU回收,可能新分配一个页cache,可能返回空。 具体看pcache1FetchNoMutex的注释,非常详细。
getPageNormal(Pager*,Pgno,DbPage**,int) // 读取页号为Pgno的页数据
sqlite3PcacheFetch // 先尝试从缓存里读
xFetch(pcache1Fetch)
pcache1FetchNoMutex
用hash找到筒,再遍历筒找页号是否存在.如果存在直接返回.否则走
pcache1FetchStage2
回收或新建cache
sqlite3PcacheFetchFinish
按他的设计cache有两层
底层中另有cache和page的抽象定义PCache1和PgHdr1,去承载原始的PCache和PgHdr.
逻辑是针对PCache1和PgHdr1来写.
流程结束后会转换一下,得到上层的PgHdr实体.
readDbPage // 如果是新的cache,那么从磁盘读数据并关联到这个cache。
sqlite3OsRead
读取页数据#
sqlite3PagerGet(Pager *pPager, Pgno pgno, DbPage **ppPage, int clrFlag)
用xGet读取指定页号的页数据到PgHdr // 广泛使用
写页数据#
sqlite3PagerWrite(DbPage*) // 把一个页标记为脏页、可写等。
pager_write
sqlite3PcacheMakeDirty
pcacheManageDirtyList(p, PCACHE_DIRTYLIST_ADD);
维护好链表
pagerAddPageToRollbackJournal
把页数据写入Pager的jfd.即把修改之前的页数据存入日志.
sqlite3PagerWrite只用来标记和存日志,一般会紧接着用put4byte、memcpy、memset之类函数处理实际数据。
b树开始一个事务#
sqlite3BtreeBeginTrans
sqlite3PagerBegin(Pager*, int exFlag, int)
pager开始一个事务
大体上是对pager的df也就是数据库文件争个锁
pagerLockDb
sqlite3OsLock
xLock
unixLock
unixFileLock
osSetPosixAdvisoryLock
osFcntl
fcntl // linux的fcntl
页同步#
sqlite3PagerSync(Pager *pPager, const char *zSuper)
sqlite3OsSync
xSync
unixSync
full_fsync
fsync
fsync在操作系统层面确认数据写到了硬件
提交#
看之前的opcode,最后一般是OP_HALT,就会进行收尾,用sqlite3PagerSync把数据固化到磁盘。
sqlite3VdbeHalt
vdbeCommit
sqlite3BtreeCommit
sqlite3BtreeCommitPhaseOne
sqlite3PagerCommitPhaseOne
sqlite3PcacheDirtyList // 获取所有脏页的cache。当即按页号排序。
syncJournal // 确保日志同步到了磁盘
sqlite3OsSync
pager_write_pagelist // 所有脏页写到磁盘
sqlite3OsWrite
xWrite
unixWrite
seekAndWrite
seekAndWriteFd
osWrite
write // 最终用linux的write写文件。
sqlite3PcacheCleanAll // 所有脏页标记为clean
sqlite3PagerSync // 页同步
sqlite3BtreeCommitPhaseTwo
sqlite3PagerCommitPhaseTwo
pager_end_transaction // 事务结束。做各种清理。
releaseAllSavepoints // 清掉所有记录点
按需把日志清掉.因为事务的数据更新实际已经完成.
页回滚#
从回滚日志一页一页地写回当前数据,实现回滚功能。
sqlite3BtreeRollback
sqlite3PagerRollback
pager_playback
readSuperJournal
loop
readJournalHdr
loop
pager_playback_one_page
sql的savepoint语句#
可rollback到某个记录点。或者commit到某个记录点。
sqlite3BtreeSavepoint
sqlite3PagerSavepoint
如果是commit
按需truncate日志文件
如果是rollback
pagerPlaybackSavepoint
loop
pager_playback_one_page