SQLite3源码学习(29) 用户事务和Savepoint

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/pfysw/article/details/80491463

相关文章:

SQLite3源码学习(26) Pager模块之日志管理

SQLite3源码学习(27)Bitmap算法

SQLite3源码学习(28) Pager模块之事务管理

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;
  }
}

展开阅读全文

没有更多推荐了,返回首页