在前面2篇文章讲了有关WAL日志相关的一些基础知识:

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

SQLite3源码学习(30)WAL-Index文件中的hash表

接下来分析一下在WAL日志模式下,整个事务的处理机制和流程

1.原子提交

事务管理最核心的特性就是满足原子提交特性,之前的回滚日志模式实现了这个特性,而WAL日志模式也实现了原子提交的特性。

在WAL日志模式下有3个文件,分别是:

1.数据库文件,文件名任意,例如"example.db"

2.WAL日志文件,在数据库文件名后加-wal,例如"example.db-wal"

3.WAL-index文件,在数据库文件名后加-shm,例如"example.db-shm"

WAL日志和回滚日志最大的区别是,在WAL模式下,修改过的数据并不直接写入到数据库,而是先写入到WAL日志。过一段时间后,通过检查点操作将WAL日志中修改的新页替换数据库文件的老页。

WAL日志模式下,对数据库的操作主要有4种:

1.读数据

2.写数据

3.检查点操作,把WAL日志的新页同步到数据库

4. WAL-index文件恢复操作

所谓原子提交特性,就是在写数据写到一半时出现系统崩溃或断电后,事务对数据库的修改只能处于初始状态或完成状态,而不能处于中间状态。

和回滚日志一样,在开始一个写事务之前,首先要有一个读事务将要修改的页读到内存中。读数据时,最新修改的还没同步到数据库的页从WAL日志读取,其他的页在数据库中读取,WAL-index文件是一个共享内存文件。

在把要修改的页读取到内存中后就可以对其修改,修改前需要对WAL-index文件加上写锁,修改完毕后将修改的页追加到WAL日志的末尾(即第mxFrame帧之后),在提交事务时,在最后一帧写入数据库长度,把WAL新添加的帧索引和页号记录到WAL-index文件中,最后更新WAL-index头部的mxFrame字段。

经过上一个步骤之后,事实上写事务已经完成了。虽然这些新修改的页没有同步到数据库中,但是读取的时候会通过WAL-index文件查询有哪些新修改的页在WAL文件中还没同步到数据库,如果在WAL文件中则在WAL文件中读取,否则从数据库中读取。

SQLite会定期把WAL日志中的页回填到数据库中,默认是WAL到了1000帧的时候执行检查点操作,把从nBackfill到mxFrame的页写回到数据库,如果写到一半出现异常并不会影响事务继续正常进行,因为读事务读取这些页面是在WAL日志中读取。在WAL日志和数据库同步完毕后,如果现在没有读事务,WAL-index头部字段的mxFrame复位为0,下一次向WAL日志追加数据时从头开始。

回写数据库出现异常并不影响事务的正常进行,写WAL日志异常页不会对事务的原子性有什么影响,事务只有在提交时才在WAL-index文件中更新mxFrame字段,如果在此前出现事务失败,刚写入WAL末尾的数据将会被忽略掉。如果在写WAL-index的时候中断,下一次开始读事务时会检测到头部异常,需要根据WAL日志的对WAL-index文件进行恢复,WAL-index文件出错会影响接下来读写的正确性。

2.WAL的优缺点

优点:

1.并发优势

在WAL模式中,写数据只是向WAL末尾添加数据,而读事务开始前,会做一个read-mark标记,只读read-mark之前的数据,所以写事务和读事务完全并发互不干扰。而回滚日志模式,在写事务把修改提交到数据库时会获取独占锁,阻止其他读事务的开始,一定程度影响了读写的并发。

2.写速度优势

在回滚日志中,写数据到数据库前需要先把原始数据写入到日志中,并对日志刷盘,再写记录到日志头,再刷盘,最后才把数据写入到数据库,这里出现了多次磁盘I/O操作,而WAL模式需一次向WAL日志写入数据即可,而且也能保持事务的原子性。而且写WAL日志都是按顺序写入的,相对于离散写入的也更快。

缺点:

1.需要共享内存

在WAL模式下,需要额外提供一个WAL-index文件,同时需要操作系统支持对该文件的共享内存访问,这就限制了所有进程访问数据库必须要在同一台机器上。

2.不支持多文件

WAL模式下没有回滚机制,所以一个事务处理多个文件时,并不能保证整体的原子性。而回滚日志模式,可以把多个数据库的日志关联到master日志里,事务恢复时可以进行整体回滚。

3.读性能会略有下降

因为每次读数据库之前都会通过WAL-index文件查找要读的页是否在日志中,会产生一些额外的损耗。

4.WAL文件可能会很大

在读事务一直持续进行时,一直没有机会把WAL日志里的内容更新到数据库,会使WAL文件变得很大。

3.读事务的实现

在开始读数据之前,需要通过sqlite3WalBeginReadTransaction()开启一个读事务,并检查此时有没有写事务对数据库进行改动,如果有改动的话,清除页缓存。

下面来一步步分析实现,首先要获取WAL-index文件头

rc =walIndexReadHdr(pWal, pChanged);

在这里需要先判断WAL-index有没有变更,先来看一些WAL-index的头部格式:

Bytes

Description

0..47

First copy of the WAL Index Information

48..95

Second copy of the WAL Index Information

96..135

Checkpoint Information and Locks

可以看到WAL Index头部为48字节,后面48~95偏移位置还有一份拷贝。为什么同一个头部要记录2次呢?

把前面48字节记为h1,接下来的拷贝部分记为h2,读是先读h1再读h2,而写是先写h2再写h1,如果读到的h1和h2不同,就说明在写入WAL-index头部出现中断或正在写入,此时如果无法获取写锁,那需要等待将文件头写完再开始,如果可以获取写锁,说明是上一次出现损坏,需要对文件头修复。

static int walIndexTryHdr(Wal *pWal, int *pChanged){u32 aCksum[2];                  /* Checksum on the header content */WalIndexHdr h1, h2;             /* Two copies of the header content */WalIndexHdr volatile *aHdr;     /* Header in shared memory *///walShmBarrier(pWal);保证读取h1和h2是严格按照先后次序aHdr = walIndexHdr(pWal);memcpy(&h1, (void *)&aHdr[0], sizeof(h1));walShmBarrier(pWal);memcpy(&h2, (void *)&aHdr[1], sizeof(h2));//文件头损坏,未初始化,校验值不对都需要重新恢复if( memcmp(&h1, &h2, sizeof(h1))!=0 ){return 1;   /* Dirty read */}  if( h1.isInit==0 ){return 1;   /* Malformed header - probably all zeros */}walChecksumBytes(1, (u8*)&h1, sizeof(h1)-sizeof(h1.aCksum), 0, aCksum);if( aCksum[0]!=h1.aCksum[0] || aCksum[1]!=h1.aCksum[1] ){return 1;   /* Checksum does not match */}……/* The header was successfully read. Return zero. */return 0;
}

恢复WAL-index文件由walIndexRecover()函数实现

static int walIndexRecover(Wal *pWal){//获取全部类型的独占锁,此时不能进行任何其他操作iLock = WAL_ALL_BUT_WRITE + pWal->ckptLock;nLock = SQLITE_SHM_NLOCK - iLock;rc = walLockExclusive(pWal, iLock, nLock);if( rc ){return rc;}//校验WAL日志头部,校验不通过不恢复WAL-index的页号索引,只初始化WAL-index头部……//校验通过时读取WAL日志的所有帧,将其页号和帧号写入到WAL-index文件索引。//这里需要注意的是校验WAL日志每一帧的头部时是一个循环检验的过程,即上一帧的校验值输出需要作为下一帧的校验值输入for(iOffset=WAL_HDRSIZE; (iOffset+szFrame)<=nSize; iOffset+=szFrame){u32 pgno;                   /* Database page number for frame */u32 nTruncate;              /* dbsize field from frame header *//* Read and decode the next log frame. */iFrame++;//读取WAL日志的帧rc = sqlite3OsRead(pWal->pWalFd, aFrame, szFrame, iOffset);if( rc!=SQLITE_OK ) break;//校验读取的帧的头部isValid = walDecodeFrame(pWal, &pgno, &nTruncate, aData, aFrame);if( !isValid ) break;//把页号和帧号添加到索引rc = walIndexAppend(pWal, iFrame, pgno);if( rc!=SQLITE_OK ) break;……}//完毕后释放锁walUnlockExclusive(pWal, iLock, nLock);return rc;
}

获取WAL-index头部信息后,还要获取读锁,如果不需要从WAL日志中读取时获取0号读锁

if( !useWal && pInfo->nBackfill==pWal->hdr.mxFrame){//此时WAL日志和数据库已经完全同步rc = walLockShared(pWal, WAL_READ_LOCK(0));……
}

如果日志和数据没有完全同步,那么需要从1~4号读锁中获取一把,每一种锁都对应一个pInfo->aReadMark[i],这个读标记记录了拥有该锁的读事务在WAL中所能读取的最大帧。

如果有一个锁空闲,将该锁的ReadMark设为mxFrame,并获取该锁。如果没有锁空闲,那么找到ReadMark最大的锁并获取。

最终读取页面时,只查看pInfo->nBackfill+1~pInfo->aReadMark[i]的帧是否在WAL日志中,pInfo->nBackfill之前的帧在数据库中读取。在检查点操作时,不能将pInfo->aReadMark[i]之后的帧同步到数据库,否则会影响读事务的正确性。

这部分加锁的代码比较繁琐,就不再贴出。

4.写事务的实现

写事务的实现基本全在sqlite3WalFrames()函数里,首先要获取一把独占的写锁。在开始写事务之前必定开始了一个读事务,读取数据库的第一页。下面通过注释来说明代码的关键地方,很多细节的地方略去

int sqlite3WalFrames(Wal *pWal,                      /* Wal handle to write to */int szPage,                     /* Database page-size in bytes */PgHdr *pList,                   /* List of dirty pages to write */Pgno nTruncate,                 /* Database size after this commit */int isCommit,                   /* True if this is a commit */int sync_flags                  /* Flags to pass to OsSync() (or 0) */
){//检查WAL日志和数据库是否完全同步,//如果已经完全同步,获取0号读锁//将mxFrame的值设为0,即从头开始写WAL日志if( SQLITE_OK!=(rc = walRestartLog(pWal)) ){return rc;}iFrame = pWal->hdr.mxFrame;//如果这是第一帧,写入WAL日志头if( iFrame==0 ){…….}//为了便于理解把这块代码从头移到这里// iFirst初始为0,如果WAL-index头被改变//则为当前事务WAL添加的第一帧pLive = (WalIndexHdr*)walIndexHdr(pWal);if( memcmp(&pWal->hdr, (void *)pLive, sizeof(WalIndexHdr))!=0 ){iFirst = pLive->mxFrame+1;}//遍历所有的脏页,写入数据for(p=pList; p; p=p->pDirty){int nDbSize;   /* 0 normally.  Positive == commit flag *///在当前的写事务内,可能会多次调用写数据函数//如果这一帧在之前写过,则只写入帧数据//不写入帧头if( iFirst && (p->pDirty || isCommit==0) ){u32 iWrite = 0;VVA_ONLY(rc =) sqlite3WalFindFrame(pWal, p->pgno, &iWrite);assert( rc==SQLITE_OK || iWrite==0 );if( iWrite>=iFirst ){//这里非常关键,记下所有重写的帧中最小的一个// iReCksum为开始校验的帧,帧头是一个连续的循环校验if( pWal->iReCksum==0 || iWrite<pWal->iReCksum ){pWal->iReCksum = iWrite;}//覆盖已经写入的数据帧,暂时不修改帧头,之后统一修改……p->flags &= ~PGHDR_WAL_APPEND;continue;}}//如果该帧没写过,帧号+1iFrame++;assert( iOffset==walFrameOffset(iFrame, szPage) );nDbSize = (isCommit && p->pDirty==0) ? nTruncate : 0;//这里会写入帧头rc = walWriteOneFrame(&w, p, nDbSize, iOffset);if( rc ) return rc;pLast = p;iOffset += szFrame;p->flags |= PGHDR_WAL_APPEND;}/* Recalculate checksums within the wal file if required. *///事务提交时,需要从pWal->iReCksum开始重新校验if( isCommit && pWal->iReCksum ){rc = walRewriteChecksums(pWal, iFrame);if( rc ) return rc;}//如果最后一帧需要在帧头写入数据库大小代表事务提交了//此后如果需要提交事务,要做的事情为://1.将WAL日志刷入磁盘//2.将所有新增的帧的页号和帧号写入WAL-index文件//3.更新WAL-index文件头……
}

4.检查点的实现

检查点就是把WAL日志中最新的帧同步到数据库,默认为1000帧之后同步。在同步之后可以选择是否将日志文件的长度截断为0。

检查点需要更新的帧从从nBackfill开始到pInfo->aReadMark[i]结束,这里代码通过一个迭代器,把WAL-index的每一块记录的帧都按照页号排序,按照页号从小到大更新到数据库,如果页号相同,选择后面的帧,因为后面的帧比前面要新。

static int walCheckpoint(Wal *pWal,                      /* Wal connection */sqlite3 *db,                    /* Check for interrupts on this handle */int eMode,                      /* One of PASSIVE, FULL or RESTART */int (*xBusy)(void*),            /* Function to call when busy */void *pBusyArg,                 /* Context argument for xBusyHandler */int sync_flags,                 /* Flags for OsSync() (or 0) */u8 *zBuf                        /* Temporary buffer to use */
){//只有nBackfill比最大有效帧小时才更新数据库if( pInfo->nBackfill<pWal->hdr.mxFrame ){/* Allocate the iterator *///迭代器把每一块的帧按照页号排序//如果J<K,那么aPgno [aList[J]] < aPgno [aList[K]]rc = walIteratorInit(pWal, &pIter);if( rc!=SQLITE_OK ){return rc;}//获取所有读锁中,最大的aReadMark的值mxSafeFrame……if( pInfo->nBackfill<mxSafeFrame&& (rc = walBusyLock(pWal, xBusy, pBusyArg, WAL_READ_LOCK(0),1))==SQLITE_OK){//在更新数据库时需要持有0号锁的独占锁//0号锁的读事务只在数据库中读取数据/* Iterate through the contents of the WAL, copying data to the db file */while( rc==SQLITE_OK && 0==walIteratorNext(pIter, &iDbpage, &iFrame) ){//遍历迭代器的每一个元素,找到符号要求的页将其更新到数据库……}……walUnlockExclusive(pWal, WAL_READ_LOCK(0), 1);}}
}

5.迭代器

下面来简要说明一下迭代器,初始化中有一个归并排序,比较难理解,这里稍微讲一下,之前讲的归并排序是关于链表的,而这里是数组元素的排序:

// 排序目标是,如果J<K,那么
//aContent [aList[J]] < aContent [aList[K]]
static void walMergesort(const u32 *aContent,            /* Pages in wal */ht_slot *aBuffer,               /* Buffer of at least *pnList items to use */ht_slot *aList,                 /* IN/OUT: List to sort */int *pnList                     /* IN/OUT: Number of elements in aList[] */
){//遍历迭代器的数组元素,将每个元素都划分到子数组里for(iList=0; iList<nList; iList++){nMerge = 1;aMerge = &aList[iList];// aSub[i]. aList中存的是子数组的首地址// aSub[i].nList中存的是子元素的个数//aSub[0]存1个,aSub[1]存2个,aSub[2]存2^2个元素//依次类推//假如当前iList是9(0b1001),那么只有在aSub[0]和aSub[3]//中存有子数组for(iSub=0; iList & (1<<iSub); iSub++){struct Sublist *p;assert( iSub<ArraySize(aSub) );p = &aSub[iSub];// p->aList为子数组的第一个元素//归并后,p->aList的内容经过了重新去重和排序//结束后p->aList本身的地址赋值给了aMerge, // nMerge为归并后的元素个数walMerge(aContent, p->aList, p->nList, &aMerge, &nMerge, aBuffer);}// aMerge是上一个子数组的首地址//虽然归并后的内容经过了重新排序,但是地址没变aSub[iSub].aList = aMerge;aSub[iSub].nList = nMerge;}//经过简单分析,不难得出aMerge是第一个子数组的首地址//aMerge和接下来的aSub[iSub]继续归并,归并后数组的//首地址仍然输出给aMerge,p->aList更新的是内容//排序后它的地址已经不重要了for(iSub++; iSub<ArraySize(aSub); iSub++){if( nList & (1<<iSub) ){struct Sublist *p;p = &aSub[iSub];walMerge(aContent, p->aList, p->nList, &aMerge, &nMerge, aBuffer);}}//输出迭代器的大小*pnList = nMerge;
}

遍历迭代器时需要遍历所有的块,每一块初始化是都已经根据页号排好序了,找出所有块中最小的元素

static int walIteratorNext(WalIterator *p,               /* Iterator */u32 *piPage,                  /* OUT: The page number of the next page */u32 *piFrame                  /* OUT: Wal frame index of next page */
){u32 iMin;                     /* Result pgno must be greater than iMin */u32 iRet = 0xFFFFFFFF;        /* 0xffffffff is never a valid page number */int i;                        /* For looping through segments */iMin = p->iPrior;assert( iMin<0xffffffff );//这里遍历所有块for(i=p->nSegment-1; i>=0; i--){struct WalSegment *pSegment = &p->aSegment[i];while( pSegment->iNext<pSegment->nEntry ){u32 iPg = pSegment->aPgno[pSegment->aIndex[pSegment->iNext]];//在当前块内找到大于上一次迭代的页号//找到之后先别急着增加pSegment->iNext//可能iPg并不是所有块内最小的页,需要遍历//完所有的块才知道if( iPg>iMin ){if( iPg<iRet ){iRet = iPg;*piFrame = pSegment->iZero + pSegment->aIndex[pSegment->iNext];}break;}pSegment->iNext++;}}*piPage = p->iPrior = iRet;//遍历迭代器所有元素后返回1return (iRet==0xFFFFFFFF);
}

6.参考资料

《SQLite Database System Design andImplementation》p.249~p.252

Write-AheadLogging

WAL-mode File Format

SQLite分析之WAL机制

Sqlite学习笔记(四)&&SQLite-WAL原理

Sqlite学习笔记(三)&&WAL性能测试

SQLite中的WAL机制详细介绍

SQLite3源码学习(32) WAL日志详细分析相关推荐

  1. spring源码学习之整合Mybatis原理分析

    本文主要解析spring是如何与mybatis进行整合,整合的过程中需要哪些组件的支持.以前面提到过的配置例子<spring源码学习之aop事物标签解析> 整合的过程中需要使用以下这个依赖 ...

  2. mybatis源码学习篇之——执行流程分析

    前言 在正式学习mybatis框架源码之前,需要先弄懂几个问题?myabtis框架是什么?为什么需要mybatis框架?使用mybatis框架带来的好处是什么? 回答这几个问题之前,我们先来看一下,之 ...

  3. spark源码学习(十)--- blockManager分析

    blockManager主要原理: blockmanager位于org.apache.spark.storage中,包含四个重要的组件:DiskStore,MemoryStore,Blocktrans ...

  4. SQLite3源码学习(18) 互斥锁

    互斥锁是为了保证在多线程时一些不可重入函数执行的串行化,有些函数如malloc等会操作一些共享数据,如果被重入了就会导致共享资源被破坏,从而出现逻辑错误,所以如果有多个线程对共享资源的访问就要加互斥锁 ...

  5. Linux源码手机,Linux操作系统源代码详细分析

    对于内核的进一步讨论将超出本章的既定范围,因此在这个问题上我们到此为止.然而本书中也包括了其他必需的内核代码.在读完第4章和第5章之后,也许你会希望再次仔细研读一下这部分内容.有关这个问题的两个文件是 ...

  6. Java多线程之JUC包:Semaphore源码学习笔记

    若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/go2sea/p/5625536.html Semaphore是JUC ...

  7. postgresql源码学习(27)—— 事务日志⑦-日志落盘上层函数 XLogFlush

    一. 预备知识 1. XLOG什么时候需要落盘 事务commit之前 log buffer被覆盖之前 后台进程定期落盘 2. 两个核心结构体 这两个结构体定义代码在xlog.c,它们在日志落盘过程中非 ...

  8. postgresql源码学习(51)—— 提交日志CLOG 原理 用途 管理函数

    一. CLOG是什么 CLOG(commit log)记录事务的最终状态. 物理上,是$PGDATA/pg_xact目录下的一些文件 逻辑上,是一个数组,下标为事务id,值为事务最终状态 1. 事务最 ...

  9. JDK源码学习笔记——Integer

    一.类定义 public final class Integer extends Number implements Comparable<Integer> 二.属性 private fi ...

最新文章

  1. RBAC权限设计实例(转)
  2. shiro单点登录原理_SSO单点登录三种情况的实现方式详解
  3. 移动前端—H5实现图片先压缩再上传
  4. 港中文开源基于PyTorch的多任务人脸识别框架
  5. PAT乙级(1028 人口普查)
  6. android 8.0 三星,这些三星手机竟到2019年才能升级安卓8.0:等到头发都白了
  7. 技术分享| Sip与WebRTC互通-SRProxy开源库讲解
  8. (mac版本)IntelliJ IDEA 常用快捷键
  9. uni.navigateTo页面跳转时传对象参数
  10. 【剑指Offer】46. 把数字翻译成字符串
  11. Python 输出[m,n]之间既能被3整除又能被7整除的数
  12. md5加密原理!!!【转】
  13. 用Xlsx xlsx-style 导出excel表格,附带合并单元格,文字居中,文字颜色字体大小等样式 (复制即可实现)
  14. 计算机网络--自顶向下方法学习笔记
  15. 软件构造(九) 面向复用的软件构造技术
  16. 微信小程序开发教程(一)--注册小程序、下载开发工具及新建工程
  17. 音视频大合集最终篇;学废了
  18. MacOS设置终端代理
  19. 虚电路(交换虚电路和永久虚电路)
  20. C语言 单链表的增删改查

热门文章

  1. Python 实现串口调试助手
  2. 对于Markdown文件的一些编辑方法说明
  3. 疫情好转,宅在家几个月,历经几个月的投简历、视频面试,突然收到(余额宝)视频面试,四面成功拿下offer
  4. stm32f407小车控制板:电机函数
  5. oracle挑库发放次数,EBS OM发运状态 wsh_delivery_details.RELEASED_STATUS
  6. 数据堂公司董事长齐红威应邀参加安徽省政府组织的企业家恳谈会
  7. 工程之星位置服务器,工程之星4.0——转换参数、坐标转换等操作步骤
  8. 华为模拟器eNSP基础配置命令
  9. 请描述计算机硬件故障检测工具的使用,电脑硬件故障检测工具(SyvirPC) v3.00免费版...
  10. 通过大规模机器学习自动调优数据库参数