相关文章:
1.用户事务
在上一篇文章中主要介绍了读事务和写事务的基本流程,每执行一条SQL语句SQLite都会默认创建一个事务,执行完后会提交事务。如果有大批量的insert和update语句执行,那会不断地创建写事务和提交,这是一个非常昂贵的操作,因为这会不断地打开和删除日志,不断把修改刷新到磁盘,这其中有大量的磁盘I/O操作,这会严重影响到数据库的执行效率。
SQLite提供了用户事务来解决上述问题,即把一系列SQL语句封装在一个事务里,当执行完一条语句后先不提交事务,只有在最后结束时才提交。通过begin命令创建一个用户事务,执行完毕后通过commit命令提交。
如下程序是用户事务的一个例子:
CREATE TABLE t1(a PRIMARY KEY, b);
BEGIN;
INSERT INTO t1 VALUES(1, 'one');
INSERT INTO t1 VALUES(2, 'two');
UPDATE t1 SET a = a + 10
INSERT INTO t1 VALUES(3,null);
COMMIT;
上面程序创建了一张名为t1的表,第一个字段a是一个主键,这个事务里有4条SQL语句,如果UPDATE操作违反了主键唯一性的约束,那么将产生一个冲突,此时UPDATE可能执行了多条记录的变更,SQLite提供了5种可能的冲突解决方案,这5种方案定义了错误的容忍范围,从最宽松的replace到最严格的rollback:
● replace:将违反约束的记录移除,之后的记录继续修改,SQL语句继续执行
● ignore:违反约束的记录保持不变,剩余的记录继续修改,SQL语句也将继续执行
● fail:SQLite将终止命令,但是不恢复约束违反之前已经修改的记录,如果UPDATE在第100行违反约束,之前99行已经修改的记录不会回滚,但对100行之外的改变不会发生,因为命令已经终止了。
● abort:SQLite将终止命令,并对之前并对UPDATE所做的修改进行回滚,但是事务中的其他命令会继续执行。
● rollback:终止当前命令和整个事务,当前命令所做的修改和事务中之前命令的改变都会被回滚。
在SQLite中abort是对冲突的默认处理方案,其他方案需要用户指定,比如使用rollback方案时,可以在update和insert命令后加上or rollback
UPDATE OR ROLLBACK t1 SET a = a + 10
2.Savepoint的作用
使用abort方式处理冲突时,需要对执行命令后已经更改的记录进行回滚,这需要使用savepoint来实现。在上面的例子中,执行UPDATE命令时会创建一个隐式的savepoint来记录事务的状态,如果命令执行时出现违反约束的冲突,事务将根据这个savepoint来回退。
一个隐式的savepoint只对应一条SQLite语句,如果这条语句执行成功,那么会释放savepoint,否则执行失败后将会回退。
用户还可以通过命令SAVEPOINT来显示创建一个savepoint,ROLLBACK来回退到这个savepoint,RELEASE来释放savepoint
BEGIN;
SAVEPOINT one;
INSERT INTO t1 VALUES(1, 'one');
SAVEPOINT two;
INSERT INTO t1 VALUES(2, 'two');
UPDATE t1 SET a = a + 10;
SAVEPOINT three;
INSERT INTO t1 VALUES(3,null);
ROLLBACK TO two;
COMMIT;
在上面的例子中创建了3个保存点,实际上还有一个隐式的保存点,所以总共有4个,在事务的最后通过ROLLBACK命令回退到了第2个保存点的状态,也就是说这个事务最终只执行了INSERTINTO t1 VALUES(1, 'one');这个修改。
SQLite不支持事务的嵌套,所以一个事务只对应一个主日志,该日志记录了事务刚开始时的数据库状态,事务失败时会通过日志还原到事务的初始状态。但是SQLite通过savepoint实现了类似于事务嵌套的功能,每个savepoint相当于一个子事务。
第一个保存点在事务的开始位置,所以源代码里不会再特地打开一个保存点用来记录状态,回退时直接根据主日志回退到事务的开始。后面的保存点不在事务的开始,所以SQLite会在pSavepoint->iOffset记录当前主日志的偏移地址pPager->journalOff,回退时pPager->journalOff之前的内容不会回滚。
如果在保存点的位置之后执行SQL语句时,数据页已经在主日志中存在,此时这一页在保存点之前已经做过修改,那么需要把这一页记录到子日志里以便将来能够恢复。
关于保存点的详细使用,可以参考pager1.test的pager1-3.7.1~ pager1-3.7.6的测试用例
3.Savepoint的实现
以下贴出的代码都省略了大量的细节,只展示最关键的代码。在创建保存点时首先要调用pagerOpenSavepoint来创建保存点对象:
// nSavepoint指定当前的要打开的保存点对象的个数
static SQLITE_NOINLINE int pagerOpenSavepoint(Pager *pPager, int nSavepoint){
//之前保存点的个数
int nCurrent = pPager->nSavepoint;
//创建nSavepoint个保存点的空间
aNew = (PagerSavepoint *)sqlite3Realloc(
pPager->aSavepoint, sizeof(PagerSavepoint)*nSavepoint
);
//新建的保存点初始化为0
memset(&aNew[nCurrent], 0, (nSavepoint-nCurrent) * sizeof(PagerSavepoint));
//把事务当前的状态记录到新的保存点里
for(ii=nCurrent; ii<nSavepoint; ii++){
//记录当前文件大小
aNew[ii].nOrig = pPager->dbSize;
if( isOpen(pPager->jfd) && pPager->journalOff>0 ){
//记录当前日志的偏移地址
aNew[ii].iOffset = pPager->journalOff;
}else{
//如果日志还没打开,将其设为日志除日志头的第一个记录的地址
aNew[ii].iOffset = JOURNAL_HDR_SZ(pPager);
}
//记录当前子日志的记录数
aNew[ii].iSubRec = pPager->nSubRec;
//创建该保存点的bitmap,用来指明某一页是否在该保存点的子日志里
aNew[ii].pInSavepoint = sqlite3BitvecCreate(pPager->dbSize);
//增加当前保存点的个数
pPager->nSavepoint = ii+1;
}
}
每打开一个日志也会创建一个记录日志页码的bitmap,写完一页会在bitmap的相应位置置位,如果某一页已经在日志中存在就不再添加到日志里。
static int pager_open_journal(Pager *pPager){
pPager->pInJournal = sqlite3BitvecCreate(pPager->dbSize);
}
//此函数将pPg设为可写,并备份到日志中去
static int pager_write(PgHdr *pPg){
//如果该页在日志中部存在,将其添加到日志中
if(sqlite3BitvecTestNotNull(pPager->pInJournal, pPg->pgno)==0){
rc = pagerAddPageToRollbackJournal(pPg);
}
}
static SQLITE_NOINLINE int pagerAddPageToRollbackJournal(PgHdr *pPg){
//写完后,将该页添加到bitmap里
rc = sqlite3BitvecSet(pPager->pInJournal, pPg->pgno);
}
在主日志里添加新页时也会把新页添加到保存点的bitmap里,这说明该页是保存点回退时所对应的页。
rc |= addToSavepointBitvecs(pPager, pPg->pgno);
static int addToSavepointBitvecs(Pager *pPager, Pgno pgno){
for(ii=0; ii<pPager->nSavepoint; ii++){
PagerSavepoint *p = &pPager->aSavepoint[ii];
// p->nOrig是保存点的数据库长度
if( pgno<=p->nOrig ){
rc |= sqlite3BitvecSet(p->pInSavepoint, pgno);
}
}
return rc;
}
如果修改的页已经添加到了主日志,但是这一页并不在保存点里,那么将其添加到子日志里面:
static int subjournalPageIfRequired(PgHdr *pPg){
if( subjRequiresPage(pPg) ){
return subjournalPage(pPg);
}else{
return SQLITE_OK;
}
}
//只要有一个保存点未记录该页,则返回1
static int subjRequiresPage(PgHdr *pPg){
Pager *pPager = pPg->pPager;
PagerSavepoint *p;
Pgno pgno = pPg->pgno;
int i;
for(i=0; i<pPager->nSavepoint; i++){
p = &pPager->aSavepoint[i];
if( p->nOrig>=pgno && 0==sqlite3BitvecTestNotNull(p->pInSavepoint, pgno) ){
return 1;
}
}
return 0;
}
static int subjournalPage(PgHdr *pPg){
//打开一个子日志
rc = openSubJournal(pPager);
if( rc==SQLITE_OK ){
void *pData = pPg->pData;
//写入第nSubRec个记录
i64 offset = (i64)pPager->nSubRec*(4+pPager->pageSize);
char *pData2;
pData2 = pData;
rc = write32bits(pPager->sjfd, offset, pPg->pgno);
if( rc==SQLITE_OK ){
rc = sqlite3OsWrite(pPager->sjfd, pData2, pPager->pageSize, offset+4);
}
}
//记录数加1,并把这一页添加到保存点的bitmap里
if( rc==SQLITE_OK ){
pPager->nSubRec++;
rc = addToSavepointBitvecs(pPager, pPg->pgno);
}
}
保存点的状态记录完毕后,执行完事务后,通过sqlite3PagerSavepoint来释放保存点或进行回退。
int sqlite3PagerSavepoint(Pager *pPager, int op, int iSavepoint){
assert( op==SAVEPOINT_RELEASE || op==SAVEPOINT_ROLLBACK );
//如果第iSavepoint个保存点需要回退,那么暂时先不释放
nNew = iSavepoint + (( op==SAVEPOINT_RELEASE ) ? 0 : 1);
//保存点是按照顺序创建的,所以如果第2个保存点被释放,那么第3、4个保存点都会被释放
for(ii=nNew; ii<pPager->nSavepoint; ii++){
sqlite3BitvecDestroy(pPager->aSavepoint[ii].pInSavepoint);
}
pPager->nSavepoint = nNew;
if( op==SAVEPOINT_RELEASE ){
if( nNew==0 && isOpen(pPager->sjfd) ){
//这里先不释放,在提交事务后,所有保存点都会释放包括bitmap对象和子日志
/* Only truncate if it is an in-memory sub-journal. */
if( sqlite3JournalIsInMemory(pPager->sjfd) ){
rc = sqlite3OsTruncate(pPager->sjfd, 0);
assert( rc==SQLITE_OK );
}
pPager->nSubRec = 0;
}
else if( isOpen(pPager->jfd) ){
//开始回退到该保存点
//如果nNew为0说明没有保存点创建
//这在保存点在事务开始时的位置会发生
PagerSavepoint *pSavepoint = (nNew==0)?0:&pPager->aSavepoint[nNew-1];
rc = pagerPlaybackSavepoint(pPager, pSavepoint);
}
}
}
在回退时,先回退主日志里记录的内容,再回退子日志里的内容,如果回退的页在主日志和子日志里都存在,则只回退主日志的页。在一个保存点之后,某一页可能经过多次修改,回退到保存点之后第一次修改前的状态。
static int pagerPlaybackSavepoint(Pager *pPager, PagerSavepoint *pSavepoint){
/* Set the database size back to the value it was before the savepoint
** being reverted was opened.
*/
//把数据库长度设为保存点记下的长度
pPager->dbSize = pSavepoint ? pSavepoint->nOrig : pPager->dbOrigSize;
//记录当前状态日志的偏移地址
szJ = pPager->journalOff;
/* Allocate a bitvec to use to store the set of pages rolled back */
if( pSavepoint ){
// pDone是一个bitmap,标记某一页是否play back
//如果这一页已经通过主日志play back,那么在子日志
//里就不再play back
pDone = sqlite3BitvecCreate(pSavepoint->nOrig);
if( !pDone ){
return SQLITE_NOMEM_BKPT;
}
}
/* Begin by rolling back records from the main journal starting at
** PagerSavepoint.iOffset and continuing to the next journal header.
** There might be records in the main journal that have a page number
** greater than the current database size (pPager->dbSize) but those
** will be skipped automatically. Pages are added to pDone as they
** are played back.
*/
// pSavepoint->iHdrOffset要么是0,要么是Segment的最后一个字节
//如果是0,说明还没跨越到下一个Segment,取pPager->journalOff
//为回滚的末尾,pSavepoint->iOffset记录了保存点中的日志偏移
//如果没有跨Segment,那么回滚从pSavepoint->iOffset开始,到
// szJ结束
if( pSavepoint ){
iHdrOff = pSavepoint->iHdrOffset ? pSavepoint->iHdrOffset : szJ;
pPager->journalOff = pSavepoint->iOffset;
while( rc==SQLITE_OK && pPager->journalOff<iHdrOff ){
rc = pager_playback_one_page(pPager, &pPager->journalOff, pDone, 1, 1);
}
assert( rc!=SQLITE_DONE );
}else{
//如果没创建保存点,从Segment的起始位置开始
pPager->journalOff = 0;
}
//继续下一个 Segment的回滚,直到结束
while( rc==SQLITE_OK && pPager->journalOff<szJ ){
u32 ii; /* Loop counter */
u32 nJRec = 0; /* Number of Journal Records */
u32 dummy;
//读取记录数nJRec,可能为0,如果日志头还没刷盘
//执行此函数后,pPager->journalOff增加一个日志头的偏移
//pPager->journalOff += JOURNAL_HDR_SZ(pPager);
rc = readJournalHdr(pPager, 0, szJ, &nJRec, &dummy);
assert( rc!=SQLITE_DONE );
//记录数为0,而pPager->journalOff又是第一个记录的地址
//此时通过文件大小来计算记录数
if( nJRec==0
&& pPager->journalHdr+JOURNAL_HDR_SZ(pPager)==pPager->journalOff
){
nJRec = (u32)((szJ - pPager->journalOff)/JOURNAL_PG_SZ(pPager));
}
for(ii=0; rc==SQLITE_OK && ii<nJRec && pPager->journalOff<szJ; ii++){
rc = pager_playback_one_page(pPager, &pPager->journalOff, pDone, 1, 1);
}
assert( rc!=SQLITE_DONE );
}
//回滚完了主日志,再去回滚子日志里的内容
if( pSavepoint ){
u32 ii; /* Loop counter */
i64 offset = (i64)pSavepoint->iSubRec*(4+pPager->pageSize);
for(ii=pSavepoint->iSubRec; rc==SQLITE_OK && ii<pPager->nSubRec; ii++){
assert( offset==(i64)ii*(4+pPager->pageSize) );
rc = pager_playback_one_page(pPager, &offset, pDone, 0, 1);
}
assert( rc!=SQLITE_DONE );
}
}
每一页的回滚在pager_playback_one_page中进行,如果第4个参数为1则是主日志回滚,为0则是子日志回滚目的3个参数记录了哪些页已经被回滚,第5个参数为0是hot日志回滚,1表示savepoint回滚。
回滚时要注意2个地方,回滚的页可能大于保存点的原始数据库长度pSavepoint->nOrig,这是可能的,因为在保存点后可能添加新的页,所以这种情况不要回滚,如果要回滚的页在pDone存在,这说明该页已经通过主日志回滚,在子日志遇到相同的页码不要回滚,因为这已经是后来修改的页
static int pager_playback_one_page(
Pager *pPager, /* The pager being played back */
i64 *pOffset, /* Offset of record to playback */
Bitvec *pDone, /* Bitvec of pages already played back */
int isMainJrnl, /* 1 -> main journal. 0 -> sub-journal. */
int isSavepnt /* True for a savepoint rollback */
){
if( pgno>(Pgno)pPager->dbSize || sqlite3BitvecTest(pDone, pgno) ){
return SQLITE_OK;
}
/* If this page has already been played back before during the current
** rollback, then don't bother to play it back again.
*/
//回滚之后将这一页在bitmap相应位置置1
if( pDone && (rc = sqlite3BitvecSet(pDone, pgno))!=SQLITE_OK ){
return rc;
}
}