Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

29 Feb 2020

SQLite的写回滚和写附加模式

SQLite是广为人知的嵌入式数据库。数据库的一个基本的要求是保持数据的一致性,也就是要协调好往数据库里面写数据的操作。

处理好写操作是维护数据一致性的关键。首先,写操作必须并行有序的被接受,写操作之间不能重叠。另外,写操作必须具有原子性,要么写操作完全成功,要么完全失败,不存在棘手的中间状态。最后还要处理好写操作与读操作的关系,如果写操作会影响到读操作,那么就要避免在写的时候读。

SQLite采用了两种方式来保证这种一致性:

写回滚

写回滚是一个简单的模型,用来保证数据一致性。它的思想是,如果一个写操作失败了,数据库可以完全回滚到执行写操作之前的状态,就像这个写操作从来没有发生过一样。

这需要使用到一个日志(journal)文件。简单地说,在执行写操作之前,涉及到的数据库的分页先被保存到一个临时的日志文件中,在这个写操作完全成功呢之后,就可以删除这个日志文件。如果写的过程中出现问题失败了,那么下一次进行数据库操作的时候,SQLite会发现有这个日志文件的存在,就会把日志文件中保存的分页还原到数据库中,这样数据库就恢复了先前的状态。

由于日志文件只有一个,所以一次只允许一个写操作,写操作必须也只能是序列化的。因为同一个数据库文件,可能会有不同进程在使用,写操作可能来自于不同的进程。为此,SQLite引入了锁机制,来保证并行的写操作之间的互斥性。

写操作需要首先申请Reserved锁。这个锁一个数据库只有一个,拥有这个锁的进程/线程准备开始写操作,也就是可以开始准备日志文件。当日志文件准备好(写入硬盘)之后,Reserved锁会升级成为Pending锁。如果说在上Reserved锁的时候,数据库还可以允许新的读操作,在上Pending锁的时候,就不能有新的读操作。当既有的读操作执行完,Pending锁会被升级成Exclusive锁,这时候所有其他的读操作都已经被清场,只允许握有Exclusive锁的写操作来修改数据库。当写操作修改完数据库时,会删除日志文件(或者简单将其置为无效)。

为了知道有多少写操作存在,写操作必须上shared锁。这个锁可以允许多个写操作持有,每个写操作持有一个,这样数据库就知道当前有多少写操作。

一些高级话题

写回滚操作支持同时操作多个数据库连接,这时候需要引入一个主日志文件,用来引用买个数据的日志文件。具体看Multi-file Commit

由于SQLite是通过操作系统提供的接口进行数据读写。而操作系统为了增加响应和吞吐性能,会有一些掩饰性操作

  • 写数据的时候,操作系统通常会在数据到达硬盘之前返回,所以需要额外的sync操作来保证数据真的到达硬盘
  • 读数据的时候,可能只是从操作系统的缓存中读取数据,而不是从硬盘上读取数据

另外为了适应不同的操作系统,SQLite还做了一些假设,比如:

  • 数据写入硬盘的时候是按页写入,并且每页上的数据是顺序写入硬盘,要么写入成功,要么写入失败,一个页上的数据不会覆盖另一个页的数据。
  • 之前SQLite默认的页大小是512字节,后来扩大了一些。总之这是个和文件系统相关的,可以微调的属性。

Optimizations涉及到了一些优化相关的话题;Things That Can Go Wrong解释任何准备都是100%可靠的。

写附加

除了写回滚以外,另一种处理数值一致性的方式是写附加。从前面的章节可以看到,写回滚方式用到了很多锁,影响并发性,得不到锁的操作会返回SQLITE_BUSY,把问题抛给应用程序来处理。写附加相比写回滚有一定的优势,那就是可以减少锁的使用,增加并发性。

写附加,顾名思义,就是把写操作影响的内容附加到原有内容的后面,这样就不会影响正在进行的读操作,因为它们所看到的数据的内容并没有发生改变。

写附加模式需要把写操作修改的内容附加到一个wal记录文件中。该模式下写操作之间依然是互斥的,串行的。所以wal中保存的是一个串行的写操作修改的内容。为了提高wal的索引速度,通常还需要把wal中写操作的索引保存到一个共享内存文件中(以shm)结尾,这个不同进程可以快速知道当前wal中有哪些写操作Implementation Of Shared-Memory For The WAL-Index

写附加模式提供了读写的并行度,但是也引入了不少的问题,主要是关于如何处理wal记录的:

  • 随着写操作的增加,wal记录会日益增大,这其实会影响读操作的性能,因为读操作不仅需要考虑原有数据库中的内容,还要考虑wal中的内容。在合适的时候,需要将wal所记录的内容写回到数据库中。这个步骤叫做Checkpointing。显然,在checkpointing的时候,任何其他操作都做不了。默认设置是当wal记录达到1000页之多的时候将其写回到数据库。
  • 另外wal中的记录的索引需要保存在共享内存中,以便多进程能够同时访问。这个需要比较高级的操作系统的支持。当然,如果不需要多进程同时访问,也可以直接将数据库设置成EXCLUSIVE模式,而不使用共享内存。

另外,虽然写附加模式避免了大部分SQLITE_BUSY,但是依然会有所有少数场景返回SQLITE_BUSY:Sometimes Queries Return SQLITE_BUSY In WAL Mode

其他参考

Categories

Tags

comments powered by Disqus