SQLite入门与分析(四)---Page Cache之事务处理(1)

前端之家收集整理的这篇文章主要介绍了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) ;
它经过编译器处理后生成的虚拟机代码如下:
sqlite> explain insert into episodes(name,1);
0|Trace|0|0|0|explain insert into episodes(name,1);|00|
1|Goto|0|12|0||00|
2|SetNumColumns|0|3|0||00|
3|OpenWrite|0|2|0||00|
4|NewRowid|0|2|0||00|
5|Null|0|3|0||00|
6|String8|0|4|0|cat|00|
7|Integer|1|5|0||00|
8|MakeRecord|3|3|6|dad|00|
9|Insert|0|6|2|episodes|0b|
10|Close|0|0|0||00|
11|Halt|0|0|0||00|
12|Transaction|0|1|0||00|
13|VerifyCookie|0|1|0||00|
14|Transaction|1|1|0||00|
15|VerifyCookie|1|0|0||00|

16|TableLock|0|2|1|episodes|00|

17|Goto|0|2|0||00|

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

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


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

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

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

上面 5步的代码的实现:


//事务指令的实现
p1为数据库文件的索引号---0为maindatabase;1为temporarytables使用的文件
p2不为0,一个写事务开始
caseOP_Transaction:{
数据库的索引号
inti=pOp->p1;
指向数据库对应的btree
Btree*pBt;

assert(i
>=0&&i<db->nDb);
assert((p
->btreeMask&(1<<i))!=0);
设置btree指针
pBt=db->aDb[i].pBt;

if(pBt){
从这里btree开始事务,主要给文件加锁,并设置btree事务状态
rc=sqlite3BtreeBeginTrans(pBt,pOp->p2);

if(rc==sqlITE_BUSY){
p
->pc=pc;
p
->rc=rc=sqlITE_BUSY;
gotovdbe_return;
}
if(rc!=sqlITE_OK&&rc!=sqlITE_READONLY/*&&rc!=sqlITE_BUSY*/){
gotoabort_due_to_error;
}
}
break;
}

开始一个事务,如果第二个参数不为0,则一个写事务开始,否则是一个读事务
如果wrflag>=2,一个exclusive事务开始,此时别的连接不能访问数据库intsqlite3BtreeBeginTrans(Btree*p,intwrflag){
BtShared
*pBt=p->pBt;
intrc=sqlITE_OK;

btreeIntegrity(p);

Ifthebtreeisalreadyinawrite-transaction,orit
**isalreadyinaread-transactionandaread-transaction
**isrequested,thisisano-op.
*/
如果b-tree处于一个写事务;或者处于一个读事务,一个读事务又请求,则返回sqlITE_OKif(p->inTrans==TRANS_WRITE||(p->inTrans==TRANS_READ&&!wrflag)){
returnsqlITE_OK;
}

Writetransactionsarenotpossibleonaread-onlydatabase写事务不能访问只读数据库if(pBt->readOnly&&wrflag){
returnsqlITE_READONLY;
}

Ifanotherdatabasehandlehasalreadyopenedawritetransaction
**onthisshared-btreestructureandasecondwritetransactionis
**requested,returnsqlITE_BUSY.
如果数据库已存在一个写事务,则该写事务请求时返回sqlITE_BUSYif(pBt->inTransaction==TRANS_WRITE&&wrflag){
returnsqlITE_BUSY;
}

do{
如果数据库对应btree的第一个页面还没读进内存
则把该页面读进内存,数据库也相应的加readlockif(pBt->pPage1==0){
加readlock,并读页面到内存
rc=lockBtree(pBt);
}

if(rc==sqlITE_OK&&wrflag){
数据库文件加RESERVED_LOCK锁
rc=sqlite3pager_begin(pBt->pPage1->aData,wrflag>1);
if(rc==sqlITE_OK){
rc
=newDatabase(pBt);
}
}

if(rc==sqlITE_OK){
if(wrflag)pBt->inStmt=0;
}
else{
unlockBtreeIfUnused(pBt);
}
}
while(rc==sqlITE_BUSY&&pBt->inTransaction==TRANS_NONE&&
sqlite3InvokeBusyHandler(pBt
->pBusyHandler));

if(p->inTrans==TRANS_NONE){
btree的事务数加1
pBt->nTransaction++;
}
设置btree事务状态
p->inTrans=(wrflag?TRANS_WRITE:TRANS_READ);
if(p->inTrans>pBt->inTransaction){
pBt
->inTransaction=p->inTrans;
}
}

btreeIntegrity(p);
returnrc;
}


**获取数据库的写锁,发生以下情况时去除写锁:
***sqlite3pager_commit()iscalled.
***sqlite3pager_rollback()iscalled.
***sqlite3pager_close()iscalled.
***sqlite3pager_unref()iscalledtooneveryoutstandingpage.
**pData指向数据库的打开的页面,此时并不修改,仅仅只是获取
**相应的pager,检查它是否处于read-lock状态。
**如果打开的不是临时文件,则打开日志文件.
**如果数据库已经处于写状态,则donothing
intsqlite3pager_begin(void*pData,255)">intexFlag){
PgHdr
*pPg=DATA_TO_PGHDR(pData);
Pager
*pPager=pPg->pPager;
intrc=sqlITE_OK;
assert(pPg
->nRef>0);
assert(pPager
->state!=PAGER_UNLOCK);
pager已经处于share状态if(pPager->state==PAGER_SHARED){
assert(pPager
->aInJournal==if(MEMDB){
pPager
->state=PAGER_EXCLUSIVE;
pPager
->origDbSize=pPager->dbSize;
}
else{
文件加RESERVED_LOCK
rc=sqlite3OsLock(pPager->fd,RESERVED_LOCK);
设置pager的状态
pPager->state=PAGER_RESERVED;
if(exFlag){
rc
=pager_wait_on_lock(pPager,EXCLUSIVE_LOCK);
}
}
if(rc!=sqlITE_OK){
returnrc;
}
pPager
->dirtyCache=0;
TRACE2(
"TRANSACTION%d\n",PAGERID(pPager));
使用日志,不是临时文件,则打开日志文件if(pPager->useJournal&&!pPager->tempFile){
为pager打开日志文件,pager应该处于RESERVED或EXCLUSIVE状态
会向日志文件写入header
rc=pager_open_journal(pPager);
}
}
}
returnrc;
}


创建日志文件,pager应该处于RESERVED或EXCLUSIVE状态staticintpager_open_journal(Pager*pPager){
intrc;
assert(
!MEMDB);
assert(pPager
->state>=PAGER_RESERVED);
assert(pPager
->journalOpen==0);
assert(pPager
->useJournal);
assert(pPager
->aInJournal==0);
sqlite3pager_pagecount(pPager);
日志文件页面位图
pPager->aInJournal=sqliteMalloc(pPager->dbSize/8+if(pPager->aInJournal==0){
rc
=sqlITE_NOMEM;
gotoFailed_to_open_journal;
}
打开日志文件
rc=sqlite3OsOpenExclusive(pPager->zJournal,&pPager->jfd,
pPager
->tempFile);
日志文件的位置指针
pPager->journalOff=0;
pPager
->setMaster=0;
pPager
->journalHdr=0;
一般来说,os此时创建的文件位于磁盘缓存,并没有实际
**存在于磁盘,下面三个操作就是为了把结果写入磁盘,而对于
**windows系统来说,并没有提供相应API,所以实际上没有意义.
fullSync操作对windows没有意义
sqlite3OsSetFullSync(pPager->jfd,pPager->full_fsync);
sqlite3OsSetFullSync(pPager
->fd,pPager->full_fsync);
Attempttoopenafiledescriptorforthedirectorythatcontainsafile.
**Thisfiledescriptorcanbeusedtofsync()thedirectory
**inordertomakesurethecreationofanewfileisactuallywrittentodisk.
*/
sqlite3OsOpenDirectory(pPager
->jfd,pPager->zDirectory);
pPager
->journalOpen=1;
pPager
->journalStarted=0;
pPager
->needSync=0;
pPager
->alwaysRollback=0;
pPager
->nRec=if(pPager->errCode){
rc
=pPager->errCode;
gotoFailed_to_open_journal;
}
pPager
->origDbSize=pPager->dbSize;
写入日志文件的header---24个字节
rc=writeJournalHdr(pPager);

if(pPager->stmtAutoopen&&rc==sqlITE_OK){
rc
=sqlite3pager_stmt_begin(pPager);
}
if(rc!=sqlITE_OK&&rc!=sqlITE_NOMEM){
rc
=pager_unwritelock(pPager);
if(rc==sqlITE_OK){
rc
=sqlITE_FULL;
}
}
returnrc;

Failed_to_open_journal:
sqliteFree(pPager
->aInJournal);
pPager
->aInJournal=if(rc==sqlITE_NOMEM){
Ifthiswasamalloc()failure,thenwewillnotbeclosingthepager
**file.Sodeleteanyjournalfilewemayhavejustcreated.Otherwise,
**thesystemwillgetconfused,wehavearead-lockonthefileanda
**mysterIoUsjournalhasappearedinthefilesystem.
*/
sqlite3OsDelete(pPager
->zJournal);
}
else{
sqlite3OsUnlock(pPager
->fd,NO_LOCK);
pPager
->state=PAGER_UNLOCK;
}
写入日志文件
**journalheader的格式如下:
**-8bytes:标志日志文件的魔数
**-4bytes:日志文件中记录数
**-4bytes:Randomnumberusedforpagehash.
**-4bytes:原来数据库的大小(kb)
**-4bytes:扇区大小512byte
intwriteJournalHdr(Pager*pPager){
日志文件charzHeader[sizeof(aJournalMagic)+16];

intrc=seekJournalHdr(pPager);
if(rc)returnrc;

pPager
->journalHdr=pPager->journalOff;
if(pPager->stmtHdrOff==0){
pPager
->stmtHdrOff=pPager->journalHdr;
}
设置文件指针指向header之后
pPager->journalOff+=JOURNAL_HDR_SZ(pPager);

FIXME:
**
**Possiblyforapagernotinno-syncmode,thejournalmagicshouldnot
**bewrittenuntilnRecisfilledinaspartofnextsyncJournal().
**
**Actuallymaybethewholejournalheadershouldbedelayeduntilthat
**point.Thinkaboutthis.
*/
memcpy(zHeader,aJournalMagic,255)">sizeof
(aJournalMagic));
ThenRecField.0xFFFFFFFFforno-syncjournals.*/
put32bits(
&zHeader[sizeof(aJournalMagic)],pPager->noSync?0xffffffff:Therandomcheck-hashinitialiser*/
sqlite3Randomness(
sizeof(pPager->cksumInit),&pPager->cksumInit);
put32bits(
&zHeader[4],pPager->cksumInit);
Theinitialdatabasesize8],pPager->dbSize);
Theassumedsectorsizeforthisprocess12],pPager->sectorSize);
写入文件
rc=sqlite3OsWrite(pPager->jfd,zHeader,255)">sizeof(zHeader));

Thejournalheaderhasbeenwrittensuccessfully.Seekthejournal
**filedescriptortotheendofthejournalheadersector.
if(rc==sqlITE_OK){
rc
=sqlite3OsSeek(pPager->jfd,pPager->journalOff-if(rc==sqlITE_OK){
rc
=sqlite3OsWrite(pPager->jfd,\0001);
}
}
returnrc;
}

其实现过程如下图所示:

主要参考:http://www.sqlite.org/atomiccommit.html

原文链接:https://www.f2er.com/sqlite/200564.html

猜你在找的Sqlite相关文章