通过上一篇文章的分析,我们知道了pager模块在整个sqlite中所处的位置。它是sqlite的核心模块,充当了多种重要角色。作为一个事务管理器,它通过并发控制和故障恢复实现事务的ACID特性,负责事务的原子提交和回滚;作为一个页管理器,它处理从文件中读写数据页,并执行文件空间管理工作;作为日志管理器,它负责写日志记录到日志文件;作为锁管理器,它确保事务在访问数据页之前,一定先对数据文件上锁,实现并发控制。本质上来说,pager模块实现了存储的持久性和事务的原子性。从图1中我们可以看到pager模块主要由4个子模块组成:事务管理模块,锁管理模块,日志模块和缓存模块。而事务模块的实现依赖于其它3个子模块。因此pager模块最核心的功能实质是由缓存模块、日志管理器和锁管理器完成。Tree模块是pager模块的上游,Tree模块在访问数据文件前,需要创建一个pager对象,通过pager对象来操作文件。pager模块利用pager对象来跟踪文件锁相关的信息,日志状态,
数据库状态等。对于同一个文件,一个进程可能有多个pager对象;这些对象之间都是相互独立的。对于共享缓存模式,每个数据文件只有一个pager对象,所有连接共享这个pager对象。
?
缓存模块
?
这里谈到的缓存管理,实际上就是page cache模块。应用程序访问
数据库文件时,pager模块会以块为单位进行缓存,每一个连接都有自己独有的pager模块,因此每个连接都有自己独有的缓存。
?
1.缓存锁状态
?
文件的页面缓存初始化时,pager模块处于NO_LOCK状态。BTREE模块第一次调用sqlite3PagerGet从数据文件中读取页面时,pager状态转换为SHARED_LOCK状态。当Tree模块调用sqlite3PagerUnref释放页面时,pager状态重新回到NO_LOCK状态。当ree模块第一次调用sqlite3PagerWrite访问页面时,pager状态变为RESERVED_LOCK状态。要注意的是,调用sqlite3PagerWrite访问的页面,一定是之前被读过的页面,pager状态是从SHARED_LOCK转换到RESERVED_LOCK状态。在向数据文件页写入之前,pager模块转换到EXECLUSIVE_LOCK状态,事务提交sqlite3BtreeCommitPhaseTwo或回滚sqlite3PagerRollback时,pager模块回到NO_LOCK状态。
?
2.缓存组织
?
如图2所示,缓存其实是由一个hash表和多个链表组成。Pager模块访问页面时,首先以页号为key,在hash表中查找,迅速确定所需的页面是否在缓存。建立hash表时,若出现多个页号映射在同一个桶,则通过链表链接起来。除了hash表,缓存组织还包括一个LRU链表和DIRTY链表,分别用于缓存替换和缓存刷脏。
3.缓存策略
?
sqlite并不像有些数据库有预取机制,可能是为了简单,也可能是因为sqlite主要用于端设备,本身缓存就比较少。因此,只有在上层模块需要指定的页时,才从文件中读取到缓存中。由于缓存是一定的,并且一般情况下小于数据库文件容量,所以一定会存在缓存替换的问题。sqlite采用LRU算法,基本上主流的关系型数据库都采用这种算法。LRU(least recent used),通过将过去一段时间页面的访问来预测未来页面的访问。意思就是,如果一个页面现在被访问,就可以认为这个页面很有可能会被再次访问;如果一个页面在过去一段时间内很久都没有被访问,则可以认为该页在未来一段时间内也不会被访问。那么当缓存池满时,则选择最近最少被访问的页面替换。如果选择被替换的页是脏页,在替换出缓存之前,需要将先将被替换的页写入数据文件(这时候脏页对应的old-page需要先刷入日志文件)。我们知道缓存中的脏页刷盘后才算真正写入文件,那么缓存中的脏页何时刷盘?sqlite不提供刷脏页接口,因此用户不能主动触发刷page(写datafile)操作,这个操作由pager模块在特定情况下触发。主要有两种情况:缓存的page数量已经超过了page_size;另外一种情况是,事务在提交过程中。
?
4.核心流程
?
读取一个page的过程(假设页号为P)
?
(1).在page cache中查找
?
通过页号,在hash表中搜索,定位到指定的桶,然后通过PgHdr1.pNext逐个比较是否是需要的页。如果找到,则将PgHdr.nRef加1,并将页面返回给上层调用模块。
?
(2).如果在page cache中没有找到,则获取一个空闲的slot,或者直接新建一个slot,只要不超过slot的阀值PCache1.nMax即可。
?
(3).如果没有可用的slot,则选择一个可以重用的slot(slot对应的页面需要释放,通过LRU算法)
?
(4).如果选择重用的slot对应的page是脏页,则将该页写入文件(对于wal,刷脏页前,先将脏页写入日志文件)
?
(5).加载page
?
如果页号P对应的偏移小于文件的大小,从文件读入page到slot,设置PgHdr.nRef为1,返回;如果页号P对应的偏移大于当前文件大小,则将slot中内容初始化为0,同样将PgHdr.nRef设置为1。
?
更新page的过程
?
这里假设page已经读取到内存中。Tree模块往page写入数据之前,需要调用sqlite3PagerWrite函数,使得一个page变为可写的状态,否则pager模块不知道这个page需要被修改。pager模块在数据文件上加一个reserved锁,并创建一个日志文件。如果加锁失败,则返回SQLITE_BUSY错误。它将page的原始信息拷贝到日志文件,如果日志文件中之前已经存在该page,则不进行拷贝动作,而只是将page标记为dirty。当page被写入文件后,dirty标记会被清除。
?
日志管理器
?
1.写日志策略
?
目前数据库日志主要用两种方式:第一种是WAL(Write Ahead Logging),另外一种是影子分页技术(Shadow paging)。sqlite分别实现了这两种日志方式来保证事务的ACID特性,可以通过参数journal_mode来控制日志模式,默认情况下采用影子分页技术。影子分页模式下,每个page只在日志文件中存一份,无论这个页被修改过多少次。日志文件中,只记录事务开始前page的原始信息,进行恢复时,只需要利用日志文件中的page进行覆盖即可。对于新生成的页,日志中不会记录,而是在日志头记录事务开始时数据文件page的数目,进行恢复时,只需要截断数据文件即可,不需要新页的数据,况且新页本来就没有任何数据。
?
2. Shadow paging
?
(1).将old-page写入日志文件,并fsync
?
(2).修改日志头,更新日志记录数,并fsync,这个值初始为0
?
(3).在数据文件上