SQLite3源码学习(31) WAL日志的锁机制

1.锁的原理

       先来回顾一下回滚日志的文件锁,之前的锁是针对数据库文件加锁的,有4种类型,分别是shared、reserverd、pending和exclusive。在WAL日志模式下不再使用原来的锁,只有在WAL日志模式和回滚日志模式切换的时候才使用shared锁和exclusive锁,其他时候使用WAL模式独有的锁,这种锁是针对WAL-index文件加锁的。

       文件锁的加锁位置是在数据库文件的0x40000000(1GB)地址后的512字节加锁,这个区域称之为锁页,用来实现各种类型的锁。而WAL锁的加锁位置在WAL-index文件头部的第120字节起的8个字节,每个字节代表一种类型的锁。

       所有8种锁的类型如下:

Name

Offset

xShmLock

File

WAL_WRITE_LOCK

0

120

WAL_CKPT_LOCK

1

121

WAL_RECOVER_LOCK

2

122

WAL_READ_LOCK(0)

3

123

WAL_READ_LOCK(1)

4

124

WAL_READ_LOCK(2)

5

125

WAL_READ_LOCK(3)

6

126

WAL_READ_LOCK(4)

7

127

       这8种类型的锁,都有2种属性,分别是共享锁和独占锁。共享锁可以和共享锁同时使用,而独占锁和其他任何锁同时使用时都会产生排斥。

       不同于回滚日志文件锁的升降级机制,WAL锁只有加锁和释放2种操作,根据锁的属性是共享锁还是独占锁,有4个和锁相关的接口,分别是:

static int walLockShared(Wal *pWal, int lockIdx)
static void walUnlockShared(Wal *pWal, int lockIdx)
static int walLockExclusive(Wal *pWal, int lockIdx, int n)
static void walUnlockExclusive(Wal *pWal, int lockIdx, int n)

       其中lockIdx为传入的锁的类型,n为锁占的字节,共享锁只占1个字节。锁的类型是0~7的宏定义:

#define SQLITE_SHM_NLOCK        8
#define WAL_WRITE_LOCK         0
#define WAL_ALL_BUT_WRITE      1
#define WAL_CKPT_LOCK          1
#define WAL_RECOVER_LOCK       2
#define WAL_READ_LOCK(I)       (3+(I))
#define WAL_NREADER            (SQLITE_SHM_NLOCK-3)

       那为什么还会有n这个参数呢?因为有时想加的锁不只一种类型,需要多种类型的锁一起加,来看看下面的例子

walUnlockExclusive(pWal, WAL_READ_LOCK(1),WAL_NREADER-1);

       这里锁占的字节是WAL_NREADER-1=4字节,代表有4种锁,第一种锁的类型是WAL_READ_LOCK(1)即4,这条语句就把WAL_READ_LOCK(1)~ WAL_READ_LOCK(4)这4种类型的锁一起加了。

       再来举一个例子:

 iLock = WAL_ALL_BUT_WRITE + pWal->ckptLock;
  nLock = SQLITE_SHM_NLOCK - iLock;
  rc = walLockExclusive(pWal, iLock, nLock);

       其中pWal->ckptLock是0或1,如果是0,那么iLock=1,nLock=7,此时锁占的字节为7字节,第一个字节的锁是WAL_CKPT_LOCK,即把除WAL_WRITE_LOCK之外的锁都加上,同理如果pWal->ckptLock是1则把除WAL_WRITE_LOCK和WAL_CKPT_LOCK之外的锁都加上。

       如果有一个WAL_READ_LOCK(1)的锁已经存在,那么这个锁会因为排斥而获取失败。如果获取成功后,此时再想获取一个WAL_READ_LOCK(1)类型的共享锁,将产生排斥。

       最终所有的接口由sqlite3OsShmLock()实现,其函数原型为

int sqlite3OsShmLock(sqlite3_file *id, intoffset, int n, int flags)

       其中offset为传入的锁类型,n为锁占的字节,flag指明是共享锁还是独占锁,下面这篇文章的结尾介绍了仅在多线程情况下对sqlite3OsShmLock的一个简单实现:

                               SQLite3源码学习(17)test_vfs的共享内存机制

       接下来我们来看一下在windows下的vfs是如何实现的,代码如下:

static int winShmLock(
  sqlite3_file *fd,          /* Database file holding the shared memory */
  int ofst,                  /* First lock to acquire or release */
  int n,                     /* Number of locks to acquire or release */
  int flags                  /* What to do with the lock */
){
  winFile *pDbFd = (winFile*)fd;        /* Connection holding shared memory */
  winShm *p = pDbFd->pShm;              /* The shared memory being locked */
  winShm *pX;                           /* For looping over all siblings */
  winShmNode *pShmNode = p->pShmNode;
  int rc = SQLITE_OK;                   /* Result code */
  u16 mask;                             /* Mask of locks to take or release */
  //锁的第一个类型+锁占的字节不能超过8
  assert( ofst>=0 && ofst+n<=SQLITE_SHM_NLOCK );
  //锁最少占1个字节
  assert( n>=1 );
  //此处flag对应加锁的4个接口
  assert( flags==(SQLITE_SHM_LOCK | SQLITE_SHM_SHARED)
       || flags==(SQLITE_SHM_LOCK | SQLITE_SHM_EXCLUSIVE)
       || flags==(SQLITE_SHM_UNLOCK | SQLITE_SHM_SHARED)
       || flags==(SQLITE_SHM_UNLOCK | SQLITE_SHM_EXCLUSIVE) );
  //共享锁只占1个字节
  assert( n==1 || (flags & SQLITE_SHM_EXCLUSIVE)!=0 );
  //等价于((1<1 || mask==(1<mutex);
  //释放锁
  if( flags & SQLITE_SHM_UNLOCK ){
    u16 allMask = 0; /* Mask of locks held by siblings */

    /* See if any siblings hold this same lock */
    //搜索所有使用共享内存文件的连接
    for(pX=pShmNode->pFirst; pX; pX=pX->pNext){
      if( pX==p ) continue;
      assert( (pX->exclMask & (p->exclMask|p->sharedMask))==0 );
      //记下是否还有其他连接对该位置加锁
      allMask |= pX->sharedMask;
    }

    /* Unlock the system-level locks */
    if( (mask & allMask)==0 ){
      //如果当前只有本连接持有锁,则将其释放
      // WIN_SHM_BASE是120表示加锁的起始地址
      //以下会调用windows api释放锁
      rc = winShmSystemLock(pShmNode, WINSHM_UNLCK, ofst+WIN_SHM_BASE, n);
    }else{
      //还有其他进程持有锁,不作处理
      rc = SQLITE_OK;
    }

    /* Undo the local locks */
    //清除掩码,表示当前进程不再持有该锁
    if( rc==SQLITE_OK ){
      p->exclMask &= ~mask;
      p->sharedMask &= ~mask;
    }
  }else if( flags & SQLITE_SHM_SHARED ){
    //如果加的共享锁
    u16 allShared = 0;  /* Union of locks held by connections other than "p" */

    /* Find out which shared locks are already held by sibling connections.
    ** If any sibling already holds an exclusive lock, go ahead and return
    ** SQLITE_BUSY.
    */
    //加锁时不允许任何其他进程持有对本锁的独占锁
    for(pX=pShmNode->pFirst; pX; pX=pX->pNext){
      if( (pX->exclMask & mask)!=0 ){
        rc = SQLITE_BUSY;
        break;
      }
      allShared |= pX->sharedMask;
    }

    /* Get shared locks at the system level, if necessary */
    if( rc==SQLITE_OK ){
      if( (allShared & mask)==0 ){
        //调用win下的加读锁接口,即共享锁
        rc = winShmSystemLock(pShmNode, WINSHM_RDLCK, ofst+WIN_SHM_BASE, n);
      }else{
        rc = SQLITE_OK;
      }
    }

    /* Get the local shared locks */
    //设置掩码,表示当前进程拥有该共享锁
    if( rc==SQLITE_OK ){
      p->sharedMask |= mask;
    }
  }else{
    /* Make sure no sibling connections hold locks that will block this
    ** lock.  If any do, return SQLITE_BUSY right away.
    */
    //对于mask所包含类型的锁,不允许任何其他进程持有共享锁或独占锁
    for(pX=pShmNode->pFirst; pX; pX=pX->pNext){
      if( (pX->exclMask & mask)!=0 || (pX->sharedMask & mask)!=0 ){
        rc = SQLITE_BUSY;
        break;
      }
    }

    /* Get the exclusive locks at the system level.  Then if successful
    ** also mark the local connection as being locked.
    */
    if( rc==SQLITE_OK ){
      //调用win下的加写锁接口,即独占锁
      rc = winShmSystemLock(pShmNode, WINSHM_WRLCK, ofst+WIN_SHM_BASE, n);
      if( rc==SQLITE_OK ){
        assert( (p->sharedMask & mask)==0 );
        //设置掩码表示当前进程拥有独占锁
        p->exclMask |= mask;
      }
    }
  }
  sqlite3_mutex_leave(pShmNode->mutex);
  return rc;
}

2.锁的应用

       前文讲到了在WAL日志模式中也会用到回滚日志的共享锁和独占锁,WAL模式下,所有连接始终都持有回滚日志的共享锁,在退出WAL日志模式时,会先获取回滚日志的独占锁,如果获取失败,说明还有其他连接在使用数据库,那么就不能退出。只有当前一个连接在使用数据库时才能退出,退出时会把WAL日志文件和WAL-index文件删除。

       下面来分析每一种WAL锁的应用:

       ●  WAL_WRITE_LOCK

       这个锁为独占锁,每一个写事务开始时都需要该锁,同一个时间只能有一个写事务持有该锁,写事务和写事务不能并发进行。

       在事务异常中断,WAL-index文件头部遭破坏后恢复这段时间也会持有该锁,因为写事务提交时也会修改WAL-index文件。

       ●  WAL_CKPT_LOCK

       在将WAL日志更新到数据库文件时会使用该锁,同一时间只能有一个连接进行checkpoint操作,所以该锁为独占锁。在wal_checkpoint模式为PASSIVE时,WAL同步数据的内容不超过每个线程的read-mark记录的帧,后面的帧不会同步,读事务只读WAL日志中在read-mark之前的帧,后面的帧在数据库中读取,所以同步数据库时并不会影响到读事务。写事务是向WAL文件追加内容,也不影响checkpoint操作。

       checkpoint操作必须持有WAL_READ_LOCK(0)独占锁,因为持有WAL_READ_LOCK(0)共享锁的读事务只在数据库中读取数据。

       如果wal_checkpoint模式为其他模式时,需要截断WAL日志,或者把写事务的起始地址恢复到WAL文件开头,此时需要持有WAL_WRITE_LOCK,防止其他连接开始写事务。同时也要持有WAL_READ_LOCK(i)锁,此时读事务已经不能在WAL日志中读取数据。

       ●  WAL_RECOVER_LOCK

       在恢复WAL-index时会加该锁,该锁是独占锁。同时在恢复期间,所有的锁都会加上,此时不能进行任何读写操作。但是如果恢复前发现有其他进程在进行checkpoint操作来同步数据库时,并不会等待其完成,而是直接同步进行。

       在进入walIndexRecover()函数之前,已经加了WAL_WRITE_LOCK锁。进入函数后将全部的锁加上:

 //如果是多线程下,pWal->ckptLock==1说明WAL_CKPT_LOCK锁已经
//加上,不用再加锁,checkpoint和recover同时进行,
//如果是多进程,只能等待其他进程把所有的锁都释放了
  assert( pWal->ckptLock==1 || pWal->ckptLock==0 );
  //1=0+1
  assert( WAL_ALL_BUT_WRITE==WAL_WRITE_LOCK+1 );
  //1=1
  assert( WAL_CKPT_LOCK==WAL_ALL_BUT_WRITE );
  //WAL_WRITE_LOCK锁已经加上
  assert( pWal->writeLock );
  // iLock=1或2
  iLock = WAL_ALL_BUT_WRITE + pWal->ckptLock;
  // nLock=8-1或8-2
  nLock = SQLITE_SHM_NLOCK - iLock;
  rc = walLockExclusive(pWal, iLock, nLock);

       为什么恢复时不能有读事务,因为读事务需要依赖WAL-index来确定要读的页是否在WAL日志中,此时进行读取数据页可能读到坏页,此时也不能有写事务,写事务提交时会修改WAL-index页的内容影响恢复。

       另外有一定不明白的是,检查点操作和恢复操作肯定不能同时进行,否则可能把未提交的页同步到数据库中,那为什么当其他线程持有WAL_CKPT_LOCK锁时,还要继续执行恢复操作?个人的理解是如果检查点操作在同步数据库时处在关键操作的地方,必然会占有读锁或写锁的独占锁,此时恢复操作肯定不能加锁成功,而如果恢复操作加锁成功,那么检查点操作并不是在关键地方,在恢复操作完成之前,下一次检查点操作可以依然正常获取WAL_CKPT_LOCK锁,但是其他写锁和读锁都获取失败,所以不可能对数据库进行同步。

       另外别的进程可以通过获取WAL_RECOVER_LOCK的共享锁是否成功,来判断是否有进程在进行恢复操作。

       ●  WAL_READ_LOCK(0)

       读事务申请的共享锁,和WAL_WRITE_LOCK不冲突,读写可以完全并发进行,互不影响。但是不能和数据库同步操作和WAL-index文件恢复并发进行。

       0表示只从数据库读取页

       ●  WAL_WRITE_LOCK(i)  i=1~4

       表示先在WAL日志中读取数据,最大帧不超过ReadMark[i]的值,如果不在WAL日志中,则从数据库中读取数据。

       如果要修改ReadMark[i]的值,则需要获取WAL_WRITE_LOCK(i)的独占锁,以确保此时没有相关的读事务。

       为什么需要ReadMark[i],只要读的帧不超过mxFrame不就行了吗,因为mxFrame在写事务结束时是会改变的,检查点操作时不知道读事务的最大帧,所以需要记录在ReadMark[i]里面。

 参考资料:

       http://www.sqlite.org/walformat.html

你可能感兴趣的:(SQLite)