你的位置:首页 > 数据库

[数据库]sqlite之WAL模式


链接

  1. 概述
    在3.7.0以后,WAL(Write-Ahead Log)模式可以使用,是另一种实现事务原子性的方法。
    • WAL的优点
      1. 在大多数情况下更快
      2. 并行性更高。因为读操作和写操作可以并行。
      3. 文件IO更加有序化,串行化(more sequential)
      4. 使用fsync()的次数更少,在fsync()调用时好时坏的机器上较为未定。
    • 缺点
      1. 一般情况下需要VFS支持共享内存模式。(shared-memory primitives)
      2. 操作数据库文件的进程必须在同一台主机上,不能用在网络操作系统。
      3. 持有多个数据库文件的数据库连接对于单个数据库时原子的,对于全部数据库是不原子的。
      4. 进入WAL模式以后不能修改page的size。
      5. 不能打开只读的WAL数据库(Read-Only Databases),这进程必须有"-shm"文件的写权限。
      6. 对于只进行读操作,很少进行写操作的数据库,要慢那么1到2个百分点。
      7. 会有多余的"-wal"和"-shm"文件
      8. 需要开发者注意checkpointing
  2. 原理

    回滚日志的方法是把为改变的数据库文件内容写入日志里,然后把改变后的内容直接写到数据库文件中去。在系统crash或掉电的情况下,日志里的内容被重新写入数据库文件中。日志文件被删除,标志commit着一次commit的结束。

    WAL模式于此此相反。原始为改变的数据库内容在数据库文件中,对数据库文件的修改被追加到单独的WAL文件中。当一条记录被追加到WAL文件后,标志着一次commit的结束。因此一次commit不必对数据库文件进行操作,当正在进行写操作时,可以同时进行读操作。多个事务的内容可以追加到一个WAL文件的末尾。

    1. checkpoint
      最后WAL文件的内容必须更新到数据库文件中。把WAL文件的内容更新到数据库文件的过程叫做一次checkpoint
      回滚日志的方法有两种操作:读和写。WAL有三种操作,读、写和checkpoint。
      默认的,SQL会在WAL文件达到1000page时进行一次checkpoint。进行WAL的时机也可以由应用程序自己决定。
    2. 并发性
      当一个读操作发生在WAL模式的数据库上时,会首先找到WAL文件中最后一次提交,叫做"end mark"。每一个事务可以有自己的"end point",但对于一个给定额事务来说,end mark是固定的。
      当读取数据库中的page时,SQLite会先从WAL文件中寻找有没有对应的page,从找出离end mark最近的那一条记录;如果找不到,那么就从数据库文件中寻找对一个的page。为了避免每次事务都要扫描一遍WAL文件,SQLite在共享内存中维护了一个"wal-index"的数据结构,帮助快速定位page。
      写数据库只是把新内容加到WAL文件的末尾,和读操作没有关系。由于只有一个WAL文件,因此同时只能有一个写操作。
      checkpoint操作可以和读操作并行。但是如果checkpoint把一个page写入数据库文件,而且这个page超过了当前读操作的end mark时,checkpoint必须停止。否则会把当前正在读的部分覆盖掉。下次checkpoint时,会从这个page开始往数据库中拷贝数据。
      当写操作时,会检查WAL文件被拷贝到数据库的进度。如果已经完全被拷贝到数据库文件中,已经同步,并且没有读操作在使用WAL文件,那么会把WAL文件清空,从其实开始追加数据。保证WAL文件不会无限制增长。
    3. 性能
      写操作是很快的,因为只需要进行一次写操作,并且是顺序的(不是随机的,每次都写到末尾)。而且,把数据刷到磁盘上是不必须的。(如果PRAGMA synchronous是FULL,每次commit要刷一次,否则不刷。)
      读操作的性能有所下降,因为需要从WAL文件中查找内容,花费的时间和WAL文件的大小有关。wal-index可以缩短这个时间,但是也不能完全避免。因此需要保证WAL文件的不会太大。
      为了保护数据库不被损坏,需要在把WAL文件写入数据库之前把WAL文件刷入磁盘;在重置WAL文件之前要把数据库内容刷入数据库文件。此外checkpoint需要查找操作。这些因素使得checkpoint比写操作慢一些。
      默认策略是很多线程可以增长WAL文件。把WAL文件大小变得比1000page大的那个线程要负责进行checkpoint。会导致绝大部分读写操作都是很快的,随机有一个写操作非常慢。也可以禁用自动checkpoint的策略,定期在一个线程或进程中进行checkpoint操作。
      高效的写操作希望WAL文件越大越好;高效的读操作希望WAL文件越小越好。两者存在一个tradeoff。
  3. 激活和配置WAL模式
    PRAGMA journal_mode=WAL;,如果成功,会返回"wal"。

    1. 自动checkpoint
      可以手动checkpoint

      sqlite3_wal_checkpoint(sqlite3 *db, const char *zDb)

      配置checkpoint

      sqlite3_wal_autocheckpoint(sqlite3 *db, int N);
    2. Application-Initiated Checkpoints
      可以在任意一个可以进行写操作的数据库连接中调用sqlite3_wal_checkpoint_v2()sqlite3_wal_checkpoint()

    3. WAL模式的持久性
      当一个进程设置了WAL模式,关闭这个进程,重新打开这个数据库,仍然是WAL模式。
      如果在一个数据库连接中设置了WAL模式,那么这个数据库的所有连接都将被设为WAL模式。

  4. 只读数据库
    如果数据库需要恢复,而你只有读权限,没有写权限,那么你不能读取这个数据库,因为进行读操作的第一步就是恢复数据库。
    类似的,因为WAL模式下的数据库进行读操作时,需要类似数据库恢复的操作,因此如果只有读权限,也不能对打开数据库。
    WAL的实现需要有一个基于WAL文件的哈希表在共享内存中。在Unix和Windows的VFS实现中,是基于MMap的。将共享内存映射到同目录下的"-shm"文件中。因此即使是对WAL模式下的数据库文件进行读操作,也需要写权限。
    为了把数据库文件转化为只读的文件,需要先把这个数据库的日志模式改为"delete".

  5. 避免过大的WAL文件

  6. WAL-index的共享内存实现
    在WAL发布之前,曾经尝试过将wal-index映射到临时目录,如/dev/shm或/tmp。但是不同的用户看到的目录是不同的,所以此路不通。
    后来尝试将wal-index映射到匿名的虚拟内存块中,但是无法在不用的Unix版本中保持一致。
    最终决定采用将wal-index映射到同目录下。这样子会导致不必要的磁盘IO。但是问题不大,是因为wal-index很少超过32k,而且从不会调用sync操作。此外,最后一个数据库连接关闭以后,这个文件会被删除。
    如果这个数据库只会被一个进程使用,那么可以使用heap memory而不是共享内存。

  7. 不用共享内存实现WAL
    在3.7.4版本以后,只要SQLite的lock mode被设为EXCLUSIVE,那么即使共享内存不支持,也可以使用WAL模式。
    换句话说,如果只有一个进程使用SQLite,那么不用共享内存也可以使用WAL。
    此时,将lock mode改为normal是无效的,需要实现取消WAL模式。