SQLite3源码学习(32) WAL日志详细分析
在前面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日志详细分析相关推荐
- spring源码学习之整合Mybatis原理分析
本文主要解析spring是如何与mybatis进行整合,整合的过程中需要哪些组件的支持.以前面提到过的配置例子<spring源码学习之aop事物标签解析> 整合的过程中需要使用以下这个依赖 ...
- mybatis源码学习篇之——执行流程分析
前言 在正式学习mybatis框架源码之前,需要先弄懂几个问题?myabtis框架是什么?为什么需要mybatis框架?使用mybatis框架带来的好处是什么? 回答这几个问题之前,我们先来看一下,之 ...
- spark源码学习(十)--- blockManager分析
blockManager主要原理: blockmanager位于org.apache.spark.storage中,包含四个重要的组件:DiskStore,MemoryStore,Blocktrans ...
- SQLite3源码学习(18) 互斥锁
互斥锁是为了保证在多线程时一些不可重入函数执行的串行化,有些函数如malloc等会操作一些共享数据,如果被重入了就会导致共享资源被破坏,从而出现逻辑错误,所以如果有多个线程对共享资源的访问就要加互斥锁 ...
- Linux源码手机,Linux操作系统源代码详细分析
对于内核的进一步讨论将超出本章的既定范围,因此在这个问题上我们到此为止.然而本书中也包括了其他必需的内核代码.在读完第4章和第5章之后,也许你会希望再次仔细研读一下这部分内容.有关这个问题的两个文件是 ...
- Java多线程之JUC包:Semaphore源码学习笔记
若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/go2sea/p/5625536.html Semaphore是JUC ...
- postgresql源码学习(27)—— 事务日志⑦-日志落盘上层函数 XLogFlush
一. 预备知识 1. XLOG什么时候需要落盘 事务commit之前 log buffer被覆盖之前 后台进程定期落盘 2. 两个核心结构体 这两个结构体定义代码在xlog.c,它们在日志落盘过程中非 ...
- postgresql源码学习(51)—— 提交日志CLOG 原理 用途 管理函数
一. CLOG是什么 CLOG(commit log)记录事务的最终状态. 物理上,是$PGDATA/pg_xact目录下的一些文件 逻辑上,是一个数组,下标为事务id,值为事务最终状态 1. 事务最 ...
- JDK源码学习笔记——Integer
一.类定义 public final class Integer extends Number implements Comparable<Integer> 二.属性 private fi ...
最新文章
- RBAC权限设计实例(转)
- shiro单点登录原理_SSO单点登录三种情况的实现方式详解
- 移动前端—H5实现图片先压缩再上传
- 港中文开源基于PyTorch的多任务人脸识别框架
- PAT乙级(1028 人口普查)
- android 8.0 三星,这些三星手机竟到2019年才能升级安卓8.0:等到头发都白了
- 技术分享| Sip与WebRTC互通-SRProxy开源库讲解
- (mac版本)IntelliJ IDEA 常用快捷键
- uni.navigateTo页面跳转时传对象参数
- 【剑指Offer】46. 把数字翻译成字符串
- Python 输出[m,n]之间既能被3整除又能被7整除的数
- md5加密原理!!!【转】
- 用Xlsx xlsx-style 导出excel表格,附带合并单元格,文字居中,文字颜色字体大小等样式 (复制即可实现)
- 计算机网络--自顶向下方法学习笔记
- 软件构造(九) 面向复用的软件构造技术
- 微信小程序开发教程(一)--注册小程序、下载开发工具及新建工程
- 音视频大合集最终篇;学废了
- MacOS设置终端代理
- 虚电路(交换虚电路和永久虚电路)
- C语言 单链表的增删改查
热门文章
- Python 实现串口调试助手
- 对于Markdown文件的一些编辑方法说明
- 疫情好转,宅在家几个月,历经几个月的投简历、视频面试,突然收到(余额宝)视频面试,四面成功拿下offer
- stm32f407小车控制板:电机函数
- oracle挑库发放次数,EBS OM发运状态 wsh_delivery_details.RELEASED_STATUS
- 数据堂公司董事长齐红威应邀参加安徽省政府组织的企业家恳谈会
- 工程之星位置服务器,工程之星4.0——转换参数、坐标转换等操作步骤
- 华为模拟器eNSP基础配置命令
- 请描述计算机硬件故障检测工具的使用,电脑硬件故障检测工具(SyvirPC) v3.00免费版...
- 通过大规模机器学习自动调优数据库参数