sqlite wal 分析

动手点关注

117506996e440005e7b035f116eec580.gif

干货不迷路

sqlite 提供了一种 redo log 型事务实现,支持读写的并发,见 write-ahead log(https://sqlite.org/wal.html)。本文将介绍 wal 原理,并源码剖析 checkpoint 过程,同时讨论下 wal 使用中的一些注意点。由于 sqlite 的复杂性,会省略掉一些细节,重点放在核心流程和 wal 并发的实现。

1. wal 原理

1.1 redo log

sqlite wal 是一种简单的 redo log 事务实现,redo log 概念这里简述下。数据库事务需要满足满足 acid,其中原子性(a),即一次事务内的多个修改,要么全部提交成功要么全部提交失败,不存在部分提交到 db 的情况。 redo log 的解决思路是将修改后的日志按序先写入 log 文件(wal 文件),每个完成的事务会添加 checksum,可鉴别事务的完整性。事务写入日志文件后,即代表提交成功,读取时日志和 db 文件合并的结果构成了 db 的完整内容。同时定期 checkpoint,同步 wal 中的事务到 db 文件,使 wal 文件保持在合理的大小。日志文件持久化到磁盘后,已提交成功的事务按序 checkpoint 执行的结果都是一样的,不受 crash 和掉电的影响。

sqlite 的 wal 也是这种思路的实现,只是 sqlite 提供的是一种简化实现,同时只允许一个写者操作日志文件,日志也是 page 这种物理日志。redo log 还能将 undo log 的随机写转化为顺序写,具有更高的写入性能,这里不赘述。

想对 redo log 进一步了解,可以参考以下资料:

https://zhuanlan.zhihu.com/p/35574452

https://developer.aliyun.com/article/1009683

1.2 sqlite wal

sqlite wal 写操作不直接写入 db 主文件,而是写到“db-wal”文件(以下简称'wal'文件)的末尾。读操作时,将结合 db 主文件以及 wal 的内容返回结果。wal 模式同时具有简单的 mvvc 实现,支持文件级别的读写并发,提供了相对 delete(rollback) 模式 (undo log 事务) 更高的并发性。 具体可看图加深理解。

下图中:

  1. pgx.y,x 表示当前 page 的 num,y 表示当前 page 的版本,每个提交的事务都保存当前修改后的 page 副本;

  2. 图中 wal 中提交了两个事务,wal 中蓝色框表示一个完整事务修改的所有 page;

  3. wal 实际中保存的单位是 wal frame,除了修改的页面还会保存 page number checksum 等信息,这里为了突出展示了 page, 详细格式见:https://www.sqlite.org/fileformat2.html

7c9e20e8b0042c6b8418ae8b0ca76e19.png

关于写

  1. 写操作总是发生在 wal 文件上;

  2. 写操作总是追加在 wal 文件末尾,由 commit 触发;

  3. 写入 wal 文件中是原始 page 修改后的副本;

  4. 写操作对 wal 文件的访问是独占串行的;

  5. 事务写入只有成功落盘(写入磁盘)才算成功提交,checkpoint 前会调用 wal 文件的 fsync,保证日志提交持久性和一致性;

  6. 没有调用 fsync 不代表日志提交一定失败,会由文件系统定期回写;

  7. 如果 fsync 回写之前发生 crash 或系统崩溃,导致事务 2 的 pg4.2 写 wal 失败,可校验出事务 2 不完整,则 wal 中成功提交的事务只有事务 1; 如果 pg0.1 回写失败,则 wal 中没有成功提交的事务。

222915526758fb06372df900246656a3.png关于读

  1. 读与写可以并发;

  2. 每个读事务会记录 wal 文件中一个 record 点,作为它的 read mark,每个事务执行过程中 read mark 不会发生改变,新提交的事务产生的修改不会影响旧的事务。read mark 会选择事务完整提交后的位置。原始 db 文件和 wal 中 read mark 之前的记录构成了数据库的一个固定的版本记录;

  3. 读事务读一个 page 优先读 wal 文件,没有则读原始文件;

  4. 如果一个 page 在 wal 中有多个副本,读 read mark 前的最后一个;

  5. 同一个 read mark 可以被多个读事务使用。

05e39a378802df0f72b41008ced8d272.png

关于 checkpoint:

  1. checkpoint 针对 wal 中已经成功落盘的事务,每次 checkpoint 前会执行 fsync;

  2. 每次 checkpoint 从前到后按序回写 wal 文件中尚未提交的事务到 db;

  3. 如果 checkpoint 中途 crash,由于事务已持久化到 wal 文件,下次启动重新按序回写 wal 中的事务即可;

  4. wal 中所有的事务 checkpoint 后,wal 文件会从头开始使用;

  5. checkpoint 并不一定都会提交 wal 中全部的事务,如果只是部分提交,下次写入还是会写入 wal 文件的末尾,wal 文件可能会变很大;

  6. 只有 truncate 的 checkpoint 才能清理已经异常变大的 wal 文件,会 truncate 文件大小到 0。

2. wal 实现

wal 的实现大部分代码集中在 wal.c 中,从 sqlite 的架构划分应该主要算是 pager 层的实现。

https://www.sqlite.org/arch.html。wal 实现从逻辑上由 3 部分组成:

2.1 wal 和 wal-index 文件格式

文件格式定义,官方文档见:

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

https://www.sqlite.org/fileformat2.html

这一层细节比较多,主要是些二进制定义。核心是 wal 格式提供了一种 page 格式的 redo log 组织格式,保证 crash 后 recover 过程满足一致性。

wal-index 文件(db-shm)只是一种对 wal 文件的快速索引,后文为了省事,也统称 wal 文件。

2.2 文件多副本抽象

即 wal 和 db 文件对外表现为一个统一的文件抽象,并提供文件级别的 mvcc,对 pager 层屏蔽 wal 细节。

由于 wal 和 db 一样都是以 pgno 的方式索引 page,按 pgno 替换就可以构造出不同版本的 b 树,比较简单。mvcc 主要通过 read lock 的 read mark 实现,前面有介绍过, 后面并发控制部分会详细举例介绍。

具体实现可看:

写入:https://github.com/sqlite/sqlite/blob/version-3.15.2/src/pager.c#L3077

读取:https://github.com/sqlite/sqlite/blob/version-3.15.2/src/wal.c#L2593

2.3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值