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

2023-05-16

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<<n)-1)<<ofst,n为占的字节数用,如n=4
  //则((1<<n)-1)=0b1111,ofst为第一个bit的偏移位置
  mask = (u16)((1U<<(ofst+n)) - (1U<<ofst));
  assert( n>1 || mask==(1<<ofst) );
  sqlite3_mutex_enter(pShmNode->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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

SQLite3源码学习(31) WAL日志的锁机制 的相关文章

  • Unity3d 5.3.5使用sqlite3

    国内讲的乱七八糟 xff0c 更有故作神秘提供Mono Data Sqlite dll System Data dll的 xff0c 就是不告诉你这两文件在哪里 我很无语 看国外的 xff0c 多靠谱 http answers unity3
  • mysql trigger 使用以及与 sqlite3 trigger 比较

    一 触发事件的表与触发更新的表使用同一个表 使用情景 xff0c 表里的某行数据发生update时自动更新修改时间 updated sqlite3 3 40 0 MariaDB 10 10 2 对应 MySQL 8 1 sqlite3 up
  • sqlite3

    一 版本 从 www sqlite org 网站可下载到最新的 sqlite 代码和编译版本 写此文章时 xff0c 最新代码是 3 3 17 版本 二 基本编译 把 sqlite3 c 和 sqlite3 h 添加到工程 xff0c 再新
  • sqlite3_bind

    sqlite3 bind text 中绑定的指针 xff0c 在sqlite3 step 时必须存在 xff0c 不能释放 xff0c 否则会是乱码 sqlite3 bind blob中绑定的指针 xff0c 在sqlite3 step 时
  • 2017-06-08 每日一记 sqlite3_bind_blob函数

    sqlite3函数 xff1a sqlite3 bind blob stat 1 pdata int length of data in bytes NULL 参数1 xff1a sqlte stmt 参数2 xff1a 的索引 xff0c
  • SQLite Database Introduction

    Use SQLite installed on Linux CentOS7 to introduce You can also use python to install SQLite3 for learning SQLite Prefac
  • 《深入剖析tomcat》读书笔记3--servlet容器

    主要是 深入剖析tomcat 的第五章和第十一章 个人觉得如下3点是关键 1 pipeline相关概念及其执行valve的顺序 2 standardwrapper的接受http请求时候的调用序列 3 standardwrapper基础阀加载
  • sqlite3查看数据库中有哪些表(代码)

    说实话 用代码实现sqlite3查看数据库中有哪些表我还真的没找到现成资源 网上提供的语句还真用不了 而且大多都是命令行语句 由于的做的MFC项目要用到这个功能 特意学习了下 下面分享我的成果 希望可以帮到你 环境 VS2005 inclu
  • QSqlite3创建数据库表格时出现的create error QSqlError(““, ““, ““)

    QSqlite3创建数据库表格时出现的create error QSqlError 这个问题卡了很长时间 连接数据库成功了 但是创建表格时一直失败 通过 qCritical lt lt file lt
  • 一个小时内学习SQLite数据库

    1 介绍 SQLite 是一个开源的嵌入式关系数据库 实现自包容 零配置 支持事务的SQL数据库引擎 其特点是高度便携 使用方便 结构紧凑 高效 可靠 与其他数据库管理系统不同 SQLite 的安装和运行非常简单 在大多数情况下 只要确保S
  • sqlite3安装错误 node-pre-gyp http 403

    sqlite3安装错误 如图可以发现 访问 https mapbox node binary s3 amazonaws com sqlite3 v5 0 1 napi v6 win32 x64 tar gz 报错 403 我们用浏览器进去
  • Sqlite3之C++开发常用API总结

    文章目录 Sqlite3之C 开发常用API总结 使用流程 sqlite3 open Sqlite3 prepare sqlite3 bind sqlite3 step sqlite3 column sqlite3 exec sqlite3
  • Sqlite3 导出/导入SQL语句

    前言 Sqlite3 提供了较轻便的数据库操作 速度非常快 也比较稳定 在嵌入式产品中用的非常广泛 但嵌入式产品往往由于不稳定性因素非常多 备份是必不可少的 直接拷贝 db 文件并不是太好的主意 所以引出本文所要讲的主题 Sqlite3 导
  • 【Spring Boot 源码学习】深入 FilteringSpringBootCondition

    Spring Boot 源码学习系列 深入 FilteringSpringBootCondition 引言 往期内容 主要内容 1 match 方法 2 ClassNameFilter 枚举类 3 filter 方法 总结 引言 前两篇博文
  • sqlite3的交叉编译

    比如说我们在qtcreator中编写程序的时候想用到sqlite3数据库 但是因为qtcreator中的编译器中的库中并没有sqlite3的库 所以肯定编译不了 所以若想在qtcreator中编译sqlite3的程序 首先的将sqlite3
  • Apollo配置中心Client源码学习(二)-- 配置同步

    上一篇文章 https blog csdn net crystonesc article details 106630412 我们从Apollo社区给出的DEMO开始逐步分析了Apollo客户端配置的创建过程 作为Apollo配置中心Cli
  • 【Spring Boot 源码学习】OnClassCondition 详解

    Spring Boot 源码学习系列 OnClassCondition 详解 引言 往期内容 主要内容 1 getOutcomes 方法 2 多处理器拆分处理 3 StandardOutcomesResolver 内部类 4 getMatc
  • 深入理解MyBatis一级缓存和二级缓存【超详细源码解析】

    视频地址 https www bilibili com video BV1nP411A7Gu MyBatis的缓存是一个常见的面试题 一级缓存的作用域为何是 sqlSession 二级缓存的作用域为何是 mapper 怎么理解 一 二级缓存
  • Peewee

    Part1前言 在 Python 的 ORM 框架中 比较主流的有 Sqlalchemy peewee pony 等等 但是其中 peewee 和 Django 的 Models 框架很像 如果了解 Django 的同学肯定对 peewee
  • SQLite 数据库中的并发

    SQLite数据库如何实现并发 根据文档 可以使用 WAL 预写日志记录 但是 我不知道如何实现它 在我的应用程序中 我想在主线程上从数据库读取数据 同时后台线程正在同一个表中写入 插入一些数据 同时执行读取和写入查询时 应用程序将停止响应

随机推荐