先来回顾一下回滚日志的文件锁,之前的锁是针对数据库文件加锁的,有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;
}
前文讲到了在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