SQLite 数据库 WAL 工作模式原理简介

闪存
  • 对象序列化系列

Android 对象序列化之你不知道的 Serializable
Android 对象序列化之 Parcelable 深入分析
Android 对象序列化之追求完美的 Serial

  • 数据序列化系列(待更)

《Android 数据序列化之 JSON》
《Android 数据序列化之 Protocol Buffer 使用》
《Android 数据序列化之 Protocol Buffer 源码分析》

  • SQLite 存储系列

Android 存储选项之 SQLiteDatabase 创建过程源码分析
Android 存储选项之 SQLiteDatabase 源码分析
数据库连接池 SQLiteConnectionPool 源码分析
SQLiteDatabase 启用事务源码分析
SQLite 数据库 WAL 模式工作原理简介
SQLite 数据库锁机制与事务简介
SQLite 数据库优化那些事儿


SQLite WAL 模式工作原理分析

什么是 WAL

WAL 的全称是 Write Ahead Logging(预写日志),它是很多数据库中用于实现原子事务的一种机制。SQLite 在 3.7.0 版本引入该特性,在此之前 SQLite 实现原子提交和回滚的方法是 rollback journal(回滚日志)。

WAL 工作原理
  • Rollback Journal

SQLite 在引入 WAL 模式之前,使用的是 Rollback Journal 机制实现原子事务。Rollback Journal 的原理是,在修改数据库文件中数据之前,先将修改所在分页中的数据备份在另外一个地方,然后才将修改写入到数据库文件中;如果事务失败,则将备份数据拷贝回来,撤销修改;如果事务成功,则删除备份数据提交修改。

  • WAL

WAL 的方法正好反过来了,原始内容保留在数据库文件中,修改数据而是写入到另外一个称为 WAL 的文件中,如果事务失败,WAL 中的记录会被忽略,撤销修改;如果事务成功,它将随后的某个时间被写回到数据库文件中,提交修改。

同步 WAL 文件和数据库文件的行为被称为 CheckPoint(检查点)。

在读数据库时,SQLite 将在 WAL 文件中搜索,找到最后一个写入点,记住它,并忽略在此之后的写入数据(这保证了读和写可以并发执行);随后它确定所要读的数据所在页是否在 WAL 文件中,如果在,则读 WAL 文件中的数据,如果不在,则直接读数据库文件中的数据。

在写入数据库时,SQLite 将数据写入到 WAL 文件中即可,但是必须保证独占写入,因此写和写之间不能并发执行。WAL 在实现的过程中,使用了共享内存技术,因此所有的读写进程必须在同一个机器上,否则无法保证数据的一致性。

检查点

当然,最终希望将 WAL 文件中附加的所有事务转移回原始数据库。将 WAL 文件事务移回数据库称为“检查点”。

考虑回滚日志和预写日志之间差异的另一种方法是,在回滚日志方法中,存在两种原始操作,即读取和写入,而对于预写日志,此时会有三种原始操作:读取、写入和检查点。

默认情况下,当 WAL 文件达到 1000 页的阈值大小时,SQLite 自动执行一个检查点。使用 WAL 的应用程序不需要执行任何附加操作即可享受到 CheckPoint。但是如果需要应用程序调整自动检查点阈值,可以使用 SQLITE_DEAULT_WAL_AUTOCHECKPOINT 编译时选项指定其它默认值。

并发

在 WAL 模式数据库上开始读数据库操作时,它首先会记住 WAL 中最后一个有效提交记录的位置。将此点称为“结束标记”。由于 WAL 可以在各种读取器连接到数据库的同时不断增长并添加新的提交记录,因此每个读取器都可能具有自己的结束标记。但是对于任何特定的读取,结束标记在事务期间不会改变,从而确保了单个读取事务只能看到数据库内容,因为它存在于单个时间点。

当读取需要一页内容时,它首先检查 WAL 以查看读取页面是否出现在该页面中,如果是,它将拉入 WAL 中出现在读者终点标记之前的页面的最后副本。如果在 WAL 中没有页面的副本位于读者的结尾标记之前,则从原始数据库文件中读取页面。读取可以存在于单独的进程中,因此避免强迫每个读取扫描整个 WAL 寻找页面(WAL 文件可以增长到数兆字节,具体取决于运行检查点的频率),这种数据结构成为“wal-index”保留在共享内存中,这有助于读取以最少的 I/O 速度在 WAL 中定位页面。wal-index 大大提高了读取的性能,但是共享内存的使用意味着所有读取者必须在同一台计算机上。

写入者仅将新内容附加到 WAL 文件的末尾。因为写入不做任何会干扰读取数据行为的事情,所以写和读可以同时运行。但是,由于只有一个 WAL 文件,所以一次只能有一个写入器。

检查点操作从 WAL 文件中获取内容,并将其回传到原始数据文件中。检查点可以与读取任务同时运行,但是当检查点到达 WAL 中的页面,超过任何当前读取任务的结束标记时,必须停止。检查点必须在该点处停止,因为它可能会覆盖读取者正在使用的部分数据库文件。该检查点会记住(wal-index中)到达的距离,并继续将内容从 WAL 传输到下一次调用中断位置的数据库。

因此,长时间运行的读取事务可能会阻止检查指针运行。但是大概每个读取事务最终都会结束,并且检查指针将能够继续。

每当发生写操作时,写入者都会检查指针取得了多少进展,如果整个 WAL 已传输到数据库并进行了同步,并且如果没有读取正在使用 WAL,则写入者将 WAL 重新从 0 开始,并在 WAL 开始时进行新交易。这种机制可以防止 WAL 文件无限制地增长。

性能问题

写事务非常快,绝大多数情况下,WAL 会提高 SQLite 的事务性能,因为它们只涉及一次写入内容(相对回滚日志事务而言则是两次),而且写入都是顺序的。但是在某些极端情况下,却会导致 SQLite 事务的性能下降。

但是随着 WAL 文件大小的增加,读取性能会下降,因为每个读取任务都必须检查 WAL 文件中的内容,并且检查 WAL 文件所需要的时间与 WAL 文件的大小成正比。wal-index 有助于更快地在 WAL 文件中查找内容,但是性能随着 WAL 文件大小的增加而下降。因此为了保持良好的读取性能,要通过定期运行检查点来减小 WAL 文件的大小。

检查点确实需要同步操作,以避免断点或硬重启后数据库损坏的可能性。在将内容从 WAL 移到数据库之前,必须将 WAL 同步到持久性存储,并且在重置 WAL 之前必须同步数据库文件。CheckPoint 还需要更多寻求。检查指针会尽最大努力对数据库进行顺序的页面写入,但是即使这样,页面写入之间通常也会散布着许多查找操作。这些因素共同导致检查点比写入事务慢。

默认策略是允许连续的写事务增长 WAL,直到 WAL 变为大约 1000 页,然后为每个后续 COMMIT 运行检查点操作,直到 WAL 重置为小于 1000 页为止。默认情况下,检查点将由执行 COMMIT 的同一线程自动运行,该线程是 WAL 超过其大小限制。这会导致大多数 COMMIT 操作非常快,但偶尔的 COMMIT(触发检查点的操作)会慢的多。如果不希望这种影响,则应用程序可以禁用自动检查点,并在单独的线程或单独的进程中运行定期检查点。

WAL 的优点与缺点

优点:

  1. 读操作不会阻塞写操作,同时写操作也不会阻塞读操作。这是并发管理的“黄金准则”。
  2. 在大多数操作场景中,与回滚日志相比,WAL 相当快。
  3. 磁盘 I/O 变得更可预见,更少的 fsync 系统调用,因为所有的 WAL 写操作是线性写入日志文件,很多 I/O 变的连续并能够按计划执行。

缺点:

  1. 所有的处理被绑定到单个主机上。也就是说,不能再如 NFS 这样的网络文件系统上使用 WAL。
  2. 为满足 WAL 和相关共享内存的需要,使用 WAL 引入了里两个额外的半持久性文件-wal 和-shm。对于那些使用SQLite 数据库作为应用程序文件格式是不具有吸引力的。这也影响了只读环境,因为-shm文件必须是可写的,并且/或数据库所在目录也必须是可写的。
  3. 对于非常大的事务,WAL 的性能将会降低。虽然 WAL 是一个高性能选项,但是非常大或运行时间非常长的事务会引入额外的开销。

推荐阅读更多精彩内容