MyException - 我的异常网
当前位置:我的异常网» SQL » SQLite学习笔记(9):pager模块

SQLite学习笔记(9):pager模块

www.MyException.Cn  网友分享于:2015-12-01  浏览:0次
SQLite学习笔记(九):pager模块

概述

      通过上一篇文章的分析,我们知道了pager模块在整个sqlite中所处的位置。它是sqlite的核心模块,充当了多种重要角色。作为一个事务管理器,它通过并发控制和故障恢复实现事务的ACID特性,负责事务的原子提交和回滚;作为一个页管理器,它处理从文件中读写数据页,并执行文件空间管理工作;作为日志管理器,它负责写日志记录到日志文件;作为锁管理器,它确保事务在访问数据页之前,一定先对数据文件上锁,实现并发控制。本质上来说,pager模块实现了存储的持久性和事务的原子性。从图1中我们可以看到pager模块主要由4个子模块组成:事务管理模块,锁管理模块,日志模块和缓存模块。而事务模块的实现依赖于其它3个子模块。因此pager模块最核心的功能实质是由缓存模块、日志管理器和锁管理器完成。Tree模块是pager模块的上游,Tree模块在访问数据文件前,需要创建一个pager对象,通过pager对象来操作文件。pager模块利用pager对象来跟踪文件锁相关的信息,日志状态,数据库状态等。对于同一个文件,一个进程可能有多个pager对象;这些对象之间都是相互独立的。对于共享缓存模式,每个数据文件只有一个pager对象,所有连接共享这个pager对象。

 

 

                             图1

缓存模块

      这里谈到的缓存管理,实际上就是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链表,分别用于缓存替换和缓存刷脏。

                                           图2

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).在数据文件上获取EXECLUSIVE lock,如果此时还有读事务,则报SQLITE_BUSY错误
(4).将dirty-page写入日志文件,并将这些cache标记为clean,表示可以重用如果是因为cache满了,导致需要写datafile操作,由于用户没有发起事务提交,因此pager模块也不会提交事务。
pager模块重复上述的1,2,3,4点,直到事务提交。为了避免事务提交前,其他事务读到脏数据,因此在进行刷脏页时,需要在文件上EXECLUSIVE lock,那么直到事务提交前,这个EXECLUSIVE lock都不会释放,导致这种情况下,所有其它读、写事务都被堵塞。因此,大事务会降低整体的并发性能。

锁管理器

   sqlite的并发控制靠封锁实现,依据两阶段锁协议,保证事务的ACID。Sqlite的并发控制依赖于文件锁,通过锁文件的特定的区域,实现互斥。Sqlite主要包含4种锁,SHARED_LOCK(共享锁),(RESERVED_LOCK)保留锁,(PENDING_LOCK)未决锁和(EXCLUSIVE_LOCK)排它锁,其中共享锁与排它锁在文件的同一片区域。RESERVED_LOCK主要用于写写互斥,PENDING_LOCK则主要用于读写互斥,并有延缓互斥的作用。关于锁并发详细可以看sqlite封锁机制

 

                                      图3

关键接口

sqlite3PagerCommitPhaseOne    //提交事务第一阶段:文件修改计数器增1,将日志文件刷入磁盘,将事务修改的脏页刷入磁盘。
sqlite3PagerCommitPhaseTwo    //提交事务的第二阶段:删除日志文件,释放锁
sqlite3PagerRollback      //回滚事务:回滚事务在数据文件的修改,将排它锁降级为共享锁,所有缓存页面还原到修改之前的状态,删除日志文件。
pcache1ResizeHash【扩展hash】   //最大缓存2000个page,LRU链表,插入队头,从队尾删除
setSharedCacheTableLock    //table-lock接口
pagerLockDb    //文件锁接口
walLockShared wal    //文件共享读锁接口
walLockExclusive wal    //文件排它锁接口
sqlite3PagerAcquire    //获取某一页
readDbPage    //读取page
pcache1FetchNoMutex   //查找cache
pcache1FetchStage2   //添加cache
pcache1RemoveFromHash  //移除cache

参考文档

SQlite Database System Design and Implementation

 

文章评论

鲜为人知的编程真相
鲜为人知的编程真相
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
程序员必看的十大电影
程序员必看的十大电影
为什么程序员都是夜猫子
为什么程序员都是夜猫子
旅行,写作,编程
旅行,写作,编程
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
程序员的鄙视链
程序员的鄙视链
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
我是如何打败拖延症的
我是如何打败拖延症的
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
 程序员的样子
程序员的样子
那些争议最大的编程观点
那些争议最大的编程观点
总结2014中国互联网十大段子
总结2014中国互联网十大段子
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
那些性感的让人尖叫的程序员
那些性感的让人尖叫的程序员
如何成为一名黑客
如何成为一名黑客
程序员和编码员之间的区别
程序员和编码员之间的区别
漫画:程序员的工作
漫画:程序员的工作
10个调试和排错的小建议
10个调试和排错的小建议
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
我的丈夫是个程序员
我的丈夫是个程序员
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
2013年美国开发者薪资调查报告
2013年美国开发者薪资调查报告
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
编程语言是女人
编程语言是女人
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
程序员都该阅读的书
程序员都该阅读的书
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
Java程序员必看电影
Java程序员必看电影
Google伦敦新总部 犹如星级庄园
Google伦敦新总部 犹如星级庄园
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
老程序员的下场
老程序员的下场
代码女神横空出世
代码女神横空出世
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
中美印日四国程序员比较
中美印日四国程序员比较
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
程序员应该关注的一些事儿
程序员应该关注的一些事儿
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有