SQLite—-Page Cache之事务处理(1)

写在前面:从本章开始,将对SQLite的每个模块进行讨论。讨论的顺序按照我阅读SQLite的顺序来进行,由于项目的需要,以及时间关系,不能给出一个完整的计划,但是我会先讨论我认为比较重要的内容。本节讨论SQLite的事务处理技术,事务处理是DBMS中最关键的技术,对SQLite也一样,它涉及到并发控制,以及故障恢复,由于内容较多,分为两节。好了,下面进入正题。

本节通过一个具体的例子来分析SQLite原子提交的实现(基于Version 3.3.6的代码)。 CREATE TABLE episodes( id integer primary key,name text, cid int) ; 插入一条记录:insert into episodes(name,cid) values(“cat”,1) ; 它经过编译器处理后生成的虚拟机代码如下:

  1. sqlite> explain insert into episodes(name,cid) values("cat",1);
  2. 0|Trace|0|0|0|explain insert into episodes(name,cid) values("cat",1);|00|
  3. 1|Goto|0|12|0||00|
  4. 2|SetNumColumns|0|3|0||00|
  5. 3|OpenWrite|0|2|0||00|
  6. 4|NewRowid|0|2|0||00|
  7. 5|Null|0|3|0||00|
  8. 6|String8|0|4|0|cat|00|
  9. 7|Integer|1|5|0||00|
  10. 8|MakeRecord|3|3|6|dad|00|
  11. 9|Insert|0|6|2|episodes|0b|
  12. 10|Close|0|0|0||00|
  13. 11|Halt|0|0|0||00|
  14. 12|Transaction|0|1|0||00|
  15. 13|VerifyCookie|0|1|0||00|
  16. 14|Transaction|1|1|0||00|
  17. 15|VerifyCookie|1|0|0||00|
  18. 16|TableLock|0|2|1|episodes|00|
  19. 17|Goto|0|2|0||00|

1、初始状态(Initial State)

当一个数据库连接第一次打开时,状态如图所示。图中最右边(“Disk”标注)表示保存在存储设备中的内容。每个方框代表一个扇区。蓝色的块表示这个扇区保存了原始数据。图中中间区域是操作系统的磁盘缓冲区。开始的时候,这些缓存是还没有被使用,因此这些方框是空白的。图中左边区域显示SQLite用户进程的内存。因为这个数据库连接刚刚打开,所以还没有任何数据记录被读入,所以这些内存也是空的。 document/2015-09-15/55f7c36622c27

2、获取读锁(Acquiring A Read Lock)

在SQLite写数据库之前,它必须先从数据库中读取相关信息。比如,在插入新的数据时,SQLite会先从sqlite_master表中读取数据库模式(相当于数据字典),以便编译器对INSERT语句进行分析,确定数据插入的位置。 在进行读操作之前,必须先获取数据库的共享锁(shared lock),共享锁允许两个或更多的连接在同一时刻读取数据库。但是共享锁不允许其它连接对数据库进行写操作。 shared lock存在于操作系统磁盘缓存,而不是磁盘本身。文件锁的本质只是操作系统的内核数据结构,当操作系统崩溃或掉电时,这些内核数据也会随之消失。 document/2015-09-15/55f7c380a0816

3、读取数据

一旦得到shared lock,就可以进行读操作。如图所示,数据先由OS从磁盘读取到OS缓存,然后再由OS移到用户进程空间。一般来说,数据库文件分为很多页,而一次读操作只读取一小部分页面。如图,从8个页面读取3个页面。 document/2015-09-15/55f7c3a07a20f

4、获取Reserved Lock

在对数据进行修改操作之前,先要获取数据库文件的Reserved Lock,Reserved Lock和shared lock的相似之处在于,它们都允许其它进程对数据库文件进行读操作。Reserved Lock和Shared Lock可以共存,但是只能是一个Reserved Lock和多个Shared Lock——多个Reserved Lock不能共存。所以,在同一时刻,只能进行一个写操作。 Reserved Lock意味着当前进程(连接)想修改数据库文件,但是还没开始修改操作,所以其它的进程可以读数据库,但不能写数据库。 document/2015-09-15/55f7c3b0c9365

5、创建恢复日志(Creating A Rollback Journal File)

在对数据库进行写操作之前,SQLite先要创建一个单独的日志文件,然后把要修改的页面的原始数据写入日志。回滚日志包含一个日志头(图中的绿色)——记录数据库文件的原始大小。所以即使数据库文件大小改变了,我们仍知道数据库的原始大小。 从OS的角度来看,当一个文件创建时,大多数OS(Windows,Linux,Mac OS X)不会向磁盘写入数据,新创建的文件此时位于磁盘缓存中,之后才会真正写入磁盘。如图,日志文件位于OS磁盘缓存中,而不是位于磁盘。 document/2015-09-15/55f7c3c163a3a

  1. //事务指令的实现
  2. //p1为数据库文件的索引号---0为main database;1为temporary tables使用的文件
  3. //p2 不为0,一个写事务开始
  4. case OP_Transaction: {
  5. //数据库的索引号
  6. int i = pOp->p1;
  7. //指向数据库对应的btree
  8. Btree *pBt;
  9. assert( i>=0 && i<db->nDb );
  10. assert( (p->btreeMask & (1<<i))!=0 );
  11. //设置btree指针
  12. pBt = db->aDb[i].pBt;
  13. if( pBt ){
  14. //从这里btree开始事务,主要给文件加锁,并设置btree事务状态
  15. rc = sqlite3BtreeBeginTrans(pBt, pOp->p2);
  16. if( rc==SQLITE_BUSY ){
  17. p->pc = pc;
  18. p->rc = rc = SQLITE_BUSY;
  19. goto vdbe_return;
  20. }
  21. if( rc!=SQLITE_OK && rc!=SQLITE_READONLY /* && rc!=SQLITE_BUSY */ ){
  22. goto abort_due_to_error;
  23. }
  24. }
  25. break;
  26. }
  27. //开始一个事务,如果第二个参数不为0,则一个写事务开始,否则是一个读事务
  28. //如果wrflag>=2,一个exclusive事务开始,此时别的连接不能访问数据库
  29. int sqlite3BtreeBeginTrans(Btree *p, int wrflag){
  30. BtShared *pBt = p->pBt;
  31. int rc = SQLITE_OK;
  32. btreeIntegrity(p);
  33. /* If the btree is already in a write-transaction, or it
  34. ** is already in a read-transaction and a read-transaction
  35. ** is requested, this is a no-op.
  36. */
  37. //如果b-tree处于一个写事务;或者处于一个读事务,一个读事务又请求,则返回SQLITE_OK
  38. if( p->inTrans==TRANS_WRITE || (p->inTrans==TRANS_READ && !wrflag) ){
  39. return SQLITE_OK;
  40. }
  41. /* Write transactions are not possible on a read-only database */
  42. //写事务不能访问只读数据库
  43. if( pBt->readOnly && wrflag ){
  44. return SQLITE_READONLY;
  45. }
  46. /* If another database handle has already opened a write transaction
  47. ** on this shared-btree structure and a second write transaction is
  48. ** requested, return SQLITE_BUSY.
  49. */
  50. //如果数据库已存在一个写事务,则该写事务请求时返回SQLITE_BUSY
  51. if( pBt->inTransaction==TRANS_WRITE && wrflag ){
  52. return SQLITE_BUSY;
  53. }
  54. do {
  55. //如果数据库对应btree的第一个页面还没读进内存
  56. //则把该页面读进内存,数据库也相应的加read lock
  57. if( pBt->pPage1==0 ){
  58. //加read lock,并读页面到内存
  59. rc = lockBtree(pBt);
  60. }
  61. if( rc==SQLITE_OK && wrflag ){
  62. //对数据库文件加RESERVED_LOCK锁
  63. rc = sqlite3pager_begin(pBt->pPage1->aData, wrflag>1);
  64. if( rc==SQLITE_OK ){
  65. rc = newDatabase(pBt);
  66. }
  67. }
  68. if( rc==SQLITE_OK ){
  69. if( wrflag ) pBt->inStmt = 0;
  70. }else{
  71. unlockBtreeIfUnused(pBt);
  72. }
  73. }while( rc==SQLITE_BUSY && pBt->inTransaction==TRANS_NONE &&
  74. sqlite3InvokeBusyHandler(pBt->pBusyHandler) );
  75. if( rc==SQLITE_OK ){
  76. if( p->inTrans==TRANS_NONE ){
  77. //btree的事务数加1
  78. pBt->nTransaction++;
  79. }
  80. //设置btree事务状态
  81. p->inTrans = (wrflag?TRANS_WRITE:TRANS_READ);
  82. if( p->inTrans>pBt->inTransaction ){
  83. pBt->inTransaction = p->inTrans;
  84. }
  85. }
  86. btreeIntegrity(p);
  87. return rc;
  88. }
  89. /*
  90. **获取数据库的写锁,发生以下情况时去除写锁:
  91. ** * sqlite3pager_commit() is called.
  92. ** * sqlite3pager_rollback() is called.
  93. ** * sqlite3pager_close() is called.
  94. ** * sqlite3pager_unref() is called to on every outstanding page.
  95. ** pData指向数据库的打开的页面,此时并不修改,仅仅只是获取
  96. ** 相应的pager,检查它是否处于read-lock状态。
  97. **如果打开的不是临时文件,则打开日志文件.
  98. **如果数据库已经处于写状态,则do nothing
  99. */
  100. int sqlite3pager_begin(void *pData, int exFlag){
  101. PgHdr *pPg = DATA_TO_PGHDR(pData);
  102. Pager *pPager = pPg->pPager;
  103. int rc = SQLITE_OK;
  104. assert( pPg->nRef>0 );
  105. assert( pPager->state!=PAGER_UNLOCK );
  106. //pager已经处于share状态
  107. if( pPager->state==PAGER_SHARED ){
  108. assert( pPager->aInJournal==0 );
  109. if( MEMDB ){
  110. pPager->state = PAGER_EXCLUSIVE;
  111. pPager->origDbSize = pPager->dbSize;
  112. }else{
  113. //对文件加 RESERVED_LOCK
  114. rc = sqlite3OsLock(pPager->fd, RESERVED_LOCK);
  115. if( rc==SQLITE_OK ){
  116. //设置pager的状态
  117. pPager->state = PAGER_RESERVED;
  118. if( exFlag ){
  119. rc = pager_wait_on_lock(pPager, EXCLUSIVE_LOCK);
  120. }
  121. }
  122. if( rc!=SQLITE_OK ){
  123. return rc;
  124. }
  125. pPager->dirtyCache = 0;
  126. TRACE2("TRANSACTION %d\n", PAGERID(pPager));
  127. //使用日志,不是临时文件,则打开日志文件
  128. if( pPager->useJournal && !pPager->tempFile ){
  129. //为pager打开日志文件,pager应该处于RESERVED或EXCLUSIVE状态
  130. //会向日志文件写入header
  131. rc = pager_open_journal(pPager);
  132. }
  133. }
  134. }
  135. return rc;
  136. }
  137. //创建日志文件,pager应该处于RESERVED或EXCLUSIVE状态
  138. static int pager_open_journal(Pager *pPager){
  139. int rc;
  140. assert( !MEMDB );
  141. assert( pPager->state>=PAGER_RESERVED );
  142. assert( pPager->journalOpen==0 );
  143. assert( pPager->useJournal );
  144. assert( pPager->aInJournal==0 );
  145. sqlite3pager_pagecount(pPager);
  146. //日志文件页面位图
  147. pPager->aInJournal = sqliteMalloc( pPager->dbSize/8 + 1 );
  148. if( pPager->aInJournal==0 ){
  149. rc = SQLITE_NOMEM;
  150. goto failed_to_open_journal;
  151. }
  152. //打开日志文件
  153. rc = sqlite3OsOpenExclusive(pPager->zJournal, &pPager->jfd,
  154. pPager->tempFile);
  155. //日志文件的位置指针
  156. pPager->journalOff = 0;
  157. pPager->setMaster = 0;
  158. pPager->journalHdr = 0;
  159. if( rc!=SQLITE_OK ){
  160. goto failed_to_open_journal;
  161. }
  162. /*一般来说,os此时创建的文件位于磁盘缓存,并没有实际
  163. **存在于磁盘,下面三个操作就是为了把结果写入磁盘,而对于
  164. **windows系统来说,并没有提供相应API,所以实际上没有意义.
  165. */
  166. //fullSync操作对windows没有意义
  167. sqlite3OsSetFullSync(pPager->jfd, pPager->full_fsync);
  168. sqlite3OsSetFullSync(pPager->fd, pPager->full_fsync);
  169. /* Attempt to open a file descriptor for the directory that contains a file.
  170. **This file descriptor can be used to fsync() the directory
  171. **in order to make sure the creation of a new file is actually written to disk.
  172. */
  173. sqlite3OsOpenDirectory(pPager->jfd, pPager->zDirectory);
  174. pPager->journalOpen = 1;
  175. pPager->journalStarted = 0;
  176. pPager->needSync = 0;
  177. pPager->alwaysRollback = 0;
  178. pPager->nRec = 0;
  179. if( pPager->errCode ){
  180. rc = pPager->errCode;
  181. goto failed_to_open_journal;
  182. }
  183. pPager->origDbSize = pPager->dbSize;
  184. //写入日志文件的header---24个字节
  185. rc = writeJournalHdr(pPager);
  186. if( pPager->stmtAutoopen && rc==SQLITE_OK ){
  187. rc = sqlite3pager_stmt_begin(pPager);
  188. }
  189. if( rc!=SQLITE_OK && rc!=SQLITE_NOMEM ){
  190. rc = pager_unwritelock(pPager);
  191. if( rc==SQLITE_OK ){
  192. rc = SQLITE_FULL;
  193. }
  194. }
  195. return rc;
  196. failed_to_open_journal:
  197. sqliteFree(pPager->aInJournal);
  198. pPager->aInJournal = 0;
  199. if( rc==SQLITE_NOMEM ){
  200. /* If this was a malloc() failure, then we will not be closing the pager
  201. ** file. So delete any journal file we may have just created. Otherwise,
  202. ** the system will get confused, we have a read-lock on the file and a
  203. ** mysterious journal has appeared in the filesystem.
  204. */
  205. sqlite3OsDelete(pPager->zJournal);
  206. }else{
  207. sqlite3OsUnlock(pPager->fd, NO_LOCK);
  208. pPager->state = PAGER_UNLOCK;
  209. }
  210. return rc;
  211. }
  212. /*写入日志文件头
  213. **journal header的格式如下:
  214. ** - 8 bytes: 标志日志文件的魔数
  215. ** - 4 bytes: 日志文件中记录数
  216. ** - 4 bytes: Random number used for page hash.
  217. ** - 4 bytes: 原来数据库的大小(kb)
  218. ** - 4 bytes: 扇区大小512byte
  219. */
  220. static int writeJournalHdr(Pager *pPager){
  221. //日志文件头
  222. char zHeader[sizeof(aJournalMagic)+16];
  223. int rc = seekJournalHdr(pPager);
  224. if( rc ) return rc;
  225. pPager->journalHdr = pPager->journalOff;
  226. if( pPager->stmtHdrOff==0 ){
  227. pPager->stmtHdrOff = pPager->journalHdr;
  228. }
  229. //设置文件指针指向header之后
  230. pPager->journalOff += JOURNAL_HDR_SZ(pPager);
  231. /* FIX ME:
  232. **
  233. ** Possibly for a pager not in no-sync mode, the journal magic should not
  234. ** be written until nRec is filled in as part of next syncJournal().
  235. **
  236. ** Actually maybe the whole journal header should be delayed until that
  237. ** point. Think about this.
  238. */
  239. memcpy(zHeader, aJournalMagic, sizeof(aJournalMagic));
  240. /* The nRec Field. 0xFFFFFFFF for no-sync journals. */
  241. put32bits(&zHeader[sizeof(aJournalMagic)], pPager->noSync ? 0xffffffff : 0);
  242. /* The random check-hash initialiser */
  243. sqlite3Randomness(sizeof(pPager->cksumInit), &pPager->cksumInit);
  244. put32bits(&zHeader[sizeof(aJournalMagic)+4], pPager->cksumInit);
  245. /* The initial database size */
  246. put32bits(&zHeader[sizeof(aJournalMagic)+8], pPager->dbSize);
  247. /* The assumed sector size for this process */
  248. put32bits(&zHeader[sizeof(aJournalMagic)+12], pPager->sectorSize);
  249. //写入文件头
  250. rc = sqlite3OsWrite(pPager->jfd, zHeader, sizeof(zHeader));
  251. /* The journal header has been written successfully. Seek the journal
  252. ** file descriptor to the end of the journal header sector.
  253. */
  254. if( rc==SQLITE_OK ){
  255. rc = sqlite3OsSeek(pPager->jfd, pPager->journalOff-1);
  256. if( rc==SQLITE_OK ){
  257. rc = sqlite3OsWrite(pPager->jfd, "\000", 1);
  258. }
  259. }
  260. return rc;
  261. }

其实现过程如下图所示: document/2015-09-15/55f7c3eba1fd4