亲测可用,若有疑问请私信
SQLite实现原子提交和回滚的默认方法是回滚日志。从3.7.0版本(2010-07-21)开始,可以使用一个新的“写前日志”选项(以下称为“WAL”)。
wal模式优点
wal模式缺点
传统的日志回滚模式是这样工作的:首先拷贝一份原始的数据库内容到一个单独的回滚日志文件中,然后将更新的内容直接写入到原始数据库;当发生崩溃或者回滚操作时,将一开始备份的数据放回到数据库文件中,以将数据库还原到其原始状态;当回滚日志文件被删除时,会触发提交操作。
wal模式恰恰相反;保留原始的数据库文件,将更新的内容append到一个单独的wal文件中;当有特殊的标记表明事务提交操作的内容append到wal文件时,就会触发提交操作;这样,在没有改动原始数据文件的情况下就发生了提交操作;更新的内容只是提交到了wal文件,所以其他读进程仍然可以继续操作原始的数据库文件;多个事务的更新都是append到同一个wal文件中的。
检查点
最终事务提交到wal文件的内容还是要更新到原始数据文件的,这一过程称为“检查点”;
在回滚日志方法中,有2个操作:读和写;而在wal模式中,有3个操作:读、写和检查点;
默认情况下,当wal文件达到阈值1000页(可以通过编译选项SQLITE_DEFAULT_WAL_AUTOCHECKPOINT设置)后,sqlite会自动执行检查,而不需要应用程序手动执行;不过应用程序可以调整阈值,或者关闭自动检查功能,然后手动执行检查。
并发性
在wal模式下,当一个读取操作到来时,它会首先记住上一次发生在wal文件的有效提交点,我们把这个点成为“end mark”;wal文件大小是不断增长的,原因是不同的连接都有可能会发生事务的提交,他们可能都有自己的“end mark”;不过,对于每个连接来说,在事务期间他们的“end mark”都是不变的,从而确保单个读取事务只看到数据库内容在某个时间点上的存在。
当一个读操作需要读取某个页的内容时,它首先会检查wal文件中是否存在该页,如果存在,则直接拉取“end mark”前的页,如果不存在,则从原始数据库文件读取;由于每个连接可以是由不同的进程发起的,为了避免每个连接都扫描wal去寻找页(wal文件大小可能会长到很大,这取决于检查点的频率),共享内存里面有一个叫做“wal-index”的数据结构,可以帮助快速的找到页的位置,并且耗费了极小的IO;不过这就使得发起连接的进程都必须在同一台机器上,这就是为什么wal模式无法在网络文件系统上工作的原因。
写操作仅仅只是把新内容append到wal文件,不影响读操作,所以读和写可以同时进行;然而,由于只有一个wal文件,所以同一时间只能有一个写操作。
检查点操作从wal文件取数据放到原始数据库文件中,检查点可以和读操作同时进行;但是,如果检查点超过任何一个读操作的“end mark”时,那么检查点操作必须停止下来,否则可能会覆盖读操作正在使用的页;通过共享内存的“wal-index”,检查点会记住它传输了哪些页到原始数据库文件,然后在下次执行检查点操作时将剩余的页继续传输到原始数据库文件中;因此,长时间运行的读操作可能会阻止检查点的进行,但是,所有的读操作最后都会结束,检查点因此得以继续进行。
任何时候写操作到来时,都会检查检查点的进行程度,如果整个的wal文件都被传输到原始数据库并且完成同步,那么如果此时没有读操作,则写操作会重置检查点到wal文件最开始,并且开始新的事务;通过这个机制,可以阻止wal文件的无限制增长。
性能分析
写事务非常快,因为他们仅仅只有一次写内容(回滚日志模式有2次写),而且是顺序写;只要开发者愿意在断点或者硬重启后牺牲持久性,还可以省略同步到磁盘的步骤(如果PRAGMA synchronous被设置为FULL,会在每次提交事务时同步WAL,但是如果PRAGMA synchronous被设置为NORMAL,则忽略这个同步。)
另一方面,每个读操作都必须检查wal文件的页,而且检查的时间跟wal文件的大小成正比;虽然内存共享可以帮助快速的查找wal文件的页,但是wal文件的大小仍然会降低查找的速度;因此,适当定期的运行检查点是很重要的。
检查点确实需要同步wal文件到原始数据库以防止断点或者硬重启导致的数据库损坏;检查点尽可能的按顺序页同步wal文件(传输到原始数据库的wal文件页是升序的),但是还是有很多查找操作穿插在同步wal之间;这些因素结合在一起就导致了检查点要比写事务慢得多。
默认策略是允许连续的写事务将wal文件大小增长到大约1000页,之后每个事务提交都会运行检查点,直到wal文件减小到1000页以下;默认情况下,检查点将由执行事务提交的线程自动执行,该线程会将wal推过其大小限制;通过这一策略,使得绝大部分的事务提交操作变得非常快,但是wal文件超过大小限制后触发的检查点会使得事务提交变得非常慢;如果不希望出现这种现象,开发者可以禁用自动检查点,然后在另外的线程或者进程中定期的进行检查点操作。
注意,如果PRAGMA synchronous被设置为NORMAL,那么检查点成为唯一触发IO繁忙或者同步文件的操作;因此,如果开发者在单独的线程或者进程中运行检查点,那么发起查询和更新操作的线程或者进程永远不会阻塞wal的同步;这可以防止因磁盘繁忙而出现的应用程序挂起现象;这样配置的缺点就是,事务不再持久性,在发生断电或者重启后可能出现事务回滚的现象。
同时也要注意,需要权衡读和写的性能;如果想最大限度提高读性能,则应该让wal文件尽可能的小,经常运行检查点,甚至可以每次事务提交就运行一次;如果想最大限度提高写性能,则应该减少检查点的频次,在执行检查点前可以让wal文件尽可能的大;默认策略是wal文件达到1000页大小的时候执行检查点,该策略在工作站的测试程序中表现最佳,但是可能并不适合其他应用程序。
默认的模式是delete,输入
pragma journal_mode;
即可查看当前数据库的模式,输入
pragma journal_mode=wal;
即可改变数据库的模式,如果成功的话会返回wal,否则返回之前的模式;
自动检查点
默认情况下,任何时候当一个事务提交导致wal文件大小超过1000页或者更多,或者当操作数据库文件的最后一个连接关闭时,都会自动触发检查点,该配置适合大部分应用程序;不过,如果想更多的控制,则可以通过PRAGMA database_name.wal_checkpoint;或者调用sqlite3_wal_checkpoint() 接口来强制执行检查点;通过PRAGMA wal_autocheckpoint;或者调用sqlite3_wal_autocheckpoint()接口可以改变自动检查点阈值,甚至直接禁止自动检查;应用程序还可以通过sqlite3_wal_hook()接口注册回调函数,该回调函数在事务提交的时候会被调用,回调函数内可以在合适时机调用sqlite3_wal_checkpoint() 或者 sqlite3_wal_checkpoint_v2()接口(自动检查机制实际上也是围绕sqlite3_wal_hook()实现的)。
应用程序启动的检查点
应用程序可以通过调用sqlite3_wal_checkpoint() or sqlite3_wal_checkpoint_v2()来启动检查点,需要有可写数据库权限;检查点有3种类型:PASSIVE, FULL, and RESTART,默认是PASSIVE,它可以在不干扰其他数据库连接的情况下,尽可能完成更多的工作,但是如果存在并发读和写,可能不会运行检查点到完成;通过sqlite3_wal_checkpoint()启动的检查点,以及自动机制检查点,都是PASSIVE类型;FULL和RESTART类型则会尽可能的运行检查点到完成,而且只能通过sqlite3_wal_checkpoint_v2()调用来启动;
wal模式的持久性
不像其他模式的短暂性,wal模式是持久性的;如果一个连接设置了数据库文件为wal模式,则关闭、再打开数据库,模式还是wal;其他模式则相反,如果在一个连接中设置了该模式,那么当关闭、再打开时,数据库文件恢复到之前的模式,也就是说其他模式的生命期是一个连接周期。
如果数据库文件模式是wal,那么当打开数据库时(实际测试发现是发起查询语句时),sqlite会自动生成一个跟数据库文件同名,并且带“-wal”后缀的文件,成为“写前日志”;该日志文件名可以在编译的时候通过参数改变;
只要有数据库连接,就会有wal文件,直到所有连接关闭,才会被删除掉;如果最后一次连接没有正确的关闭数据库,或者配置了SQLITE_FCNTL_PERSIST_WAL,那么wal文件会被保留在磁盘中;wal文件是数据库状态的一部分,如果拷贝或者移动数据库文件时,应该同步拷贝或者移动wal文件;如果数据库丢失了wal文件,那么事务提交的内容可能会丢失,数据库文件也有可能被损坏;删除wal文件唯一最安全的方法就是正常关闭数据库连接(sqlite3_close());
在version 3.22.0 (2018-01-22)版本之前,无法打开只读权限的wal模式数据库;
新版本之后,以下几种情况仍然可以打开只读权限数据库:
尽管可以打开只读WAL模式数据库,但是在将SQLite数据库映像烧录到只读磁盘之前,最好将其转换为PRAGMA journal_mode=DELETE模式。
正常情况下,新的内容会append到wal文件中,直到增长到大约1000页(大概4M),会自动运行检查点以使wal文件可以循环利用;检查点不会清空wal文件(除非设置了PRAGMA schema.journal_size_limit;),而是重新开始覆盖wal文件,这是因为正常情况下覆盖写要比append写的速度要快;当最后一个连接关闭时,执行最后的检查点,然后删除wal文件以及关联的shm文件。
所以大部分情况下,开发者不需要关心wal文件,sqlite自动注意并检查着;但是这也有可能会导致wal文件无限制的增长,耗尽磁盘可用空间以及降低查询速度,下面列出了一些可能发生这种现象的操作以及如何避免它们:
wal-index是通过一个为了健壮性而映射成的普通文件;预发布版中,wal-index是存储在易失性共享内存中的,例如linux的/dev/shm或者unix的/tmp;这样导致的问题是拥有不同root目录的进程(通过chroot)会得到不同的共享内存区域,导致数据库不完整;其他方法,比如创建匿名的共享内存块,这种方法没办法在各种版本的unix上移植,另外也没有任何办法在Windows上创建匿名共享内存块;唯一的方法只有在数据库文件相同目录下映射一个共享内存文件。
使用普通磁盘文件来提供共享内存的方法有个缺点是将共享内存数据写入到磁盘会导致不必要的磁盘IO;然而,开发者们并不认为需要太关心这个问题,因为wal-index很少会超过32KB大小,并且永远不需要同步;另外,当最后一个连接关闭时,会删除掉wal-index文件,这通常会阻止掉真正的磁盘IO的发生。
对于不能个别应用程序,如果觉得默认的共享内存实现方法不合适,则可以通过VFS(The SQLite OS Interface or "VFS")设定出其他方法;例如,如果一个数据库文件只会被单个进程中的线程访问,则可以通过堆内存而不是共享内存来实现wal-index。
从version 3.7.4 (2010-12-07)开始,在第一个连接到来前,只要设定了PRAGMA schema.locking_mode=EXCLUSIVE; 即使共享内存不可用,wal模式数据库仍然可以被创建、读取、写入;换句话说,如果能保证访问数据库的进程是唯一的,则该进程不需要共享内存就可以与数据库交互;
只有在第一次访问数据库之前可以设定PRAGMA schema.locking_mode=EXCLUSIVE;只要设定了该选项,sqlite将永远不会调用任何的共享内存方法,这样wal-index就不会被创建了;只要数据库模式是wal,那么将一直保持EXCLUSIVE模式,就算调用PRAGMA schema.locking_mode=NORMAL;也不会生效,唯一方法是先将数据库模式由wal转为其他模式。
如果PRAGMA schema.locking_mode=NORMAL;那么共享内存wal-index会被创建,这意味着底层的VFS必须支持“version 2”的共享内存,否则将无法打开wal模式数据库,也无法将数据库从其他模式更改为wal模式;对于数据库连接,只要使用了内存共享wal-index,locking mode就可以在NORMAL and EXCLUSIVE之间自由切换;只有当忽略共享内存wal-index,且在第一次访问数据前设定locking mode为EXCLUSIVE,才会锁定在EXCLUSIVE模式。
前面说到,wal模式的优点之一是读写互不影响,这只是大部分情况,也有少数情况下可能会返回SQLITE_BUSY,所以我们应该为发生这种情况做好准备。以下几种场景可能会导致这种情况发生:
wal模式的数据库文件格式没有发生变化,但是wal文件和wal-index是新的概念,旧版的sqlite无法恢复发生崩溃的wal模式数据库;为了防止旧版的sqlite(version 3.7.0,2010-07-22之前)试图恢复wal模式的数据库,数据库文件格式版本号(位于数据库头部的18和19字节)从1增加到2;这样,如果旧版的sqlite试图恢复wal模式数据库时,将收到一个错误“file is encrypted or is not a database”。
不过,可以改变数据库模式,PRAGMA journal_mode=DELETE;这样数据库文件格式版本号又恢复为1,旧版的sqlite就可以打开数据库了。