SQLite事务管理

事务管理对数据库一致性是相当重要的。数据库实现ACID属性以确保一致性。SQLite依赖于本地文件锁和页日志来实现ACID属性。SQLite只支持扁平事务,并不支持事务嵌套和保存点能力。算法

1.1 事务类型sql

SQLite执行在一个事务中的每条语句,支持读事务和写事务。应用程序只能是在读或写事务中才能从数据库中读数据。应用程序只能在写事务中才能向数据库中写数据。应用程序不须要明确告诉SQLite去执行事务中的单个SQL语句,SQLite时自动这样作的,这是默认行为,这样的系统叫作自动提交模式。这些事务被叫作自动事务,或系统级事务。对于一个select语句,SQLite创建一个读事务,对于一个非select语句,SQLite先创建一个读事务,再把它转换成写事务。每一个事务都在语句执行结束时被提交或被终止。应用程序不知道有系统事务,应用程序只是把SQL语句提交给SQLite,由SQLite再去处理关于ACID的属性,应用程序接收从SQLite执行SQL返回的结果。一个应用程序能够在相同的数据库链接上引起执行select语句(读事务)的并行执行,可是只能在一个空闲链接上引发一个非select语句(写事务)的执行。自动提交模式可能对某些应用程序来说代价太高,尤为是那些写密集的应用程序,由于对于每个非select语句,SQLite须要重复打开,写入,关闭日志文件。在自动提交模式中,在每条语句执行的最后,SQLite丢弃页缓冲。每条语句执行都会从新构造缓冲区,从新构造缓冲区是花费大,低效的行动,由于会调用磁盘I/O。而且,存在并发控制的开销,由于对每一句SQL语句的执行,应用程序须要从新获取和释放对数据库文件的锁。这种开销会使性能显著降低(尤为是大型应用程序),并只能以打开一个包括了不少SQL语句的用户级别的事务来减轻这种状况(如:打开多个数据库)。应用程序能够用begin命令手动的开始一个新的事务,这种事务被称为用户级事务(或用户事务)。当begin执行后,SQLite离开默认的自动提交模式,在语句结束时不会调用commit或abort。也不会丢弃该页的缓冲。连续的SQL语句是整个事物的一部分,当应用程序执行commit/respectively/rollback指令时,SQLite提交或分别或停止事务。若是当事务停止或失败,或应用程序关闭链接,则整个事务回滚。SQLite在事务完成时恢复到自动提交模式上来。
数据库

 SQLite只支持扁平事务。一个应用程序不能一次性在一个数据库链接中打开多个用户事务。若是你在用户事务中执行begin命令,返回一个错误。在事务中,每一个非select语句都被以一个单独的语句级的子事务执行。虽然SQLite没有通常的保存点能力,但它能为当前语句子事务确保保存点。若是当前的语句执行失败,SQLite不会退出用户事务,它会从新装载数据库到语句开始执行以前的状态,事务从那里再继续。失败的语句不会改变以前的SQL语句的执行结果,除非主用户事务本身停止。SQLite能很好的帮助一个长的用户事务承受其中一些语句的失败。windows

BEGIN;
INSERT INTO tablel values(100);
INSERT INTO table2 values(20, 100);
UPDATE tablel SET x=x+1 WHERE y> 10;
INSERT INTO table3 VALUES (1,2,3);
COMMIT;缓存

在上边的例子中,四个语句中的每一个都被看成一个单独执行的事务,依次执行。若是在更新操做的第十行出现了约束错,则更新操做的前九行将会回滚,可是其余三个插入操做的更改将会在执行commit命令时被提交。安全

 

1.2 锁管理数据结构

SQLite为了序列化的执行事务,采用锁机制来调节来自事务的访问数据库的请求。严格遵循两段锁协议,如:只在事务结束时释放锁。SQLite只有数据库级别的锁,没有行级页级表级的锁,即对整个数据库文件加锁,而不是对文件中部分数据项加锁。多线程

子事务经过用户事务的容器得到锁。用户事务持有全部的锁,直到用户事务的提交或停止,与子事务的结果无关。并发

1.2.1 锁类型及其兼容性dom

从单一的事务类型来看,一个数据库文件能够是如下五种锁状态之一:

未加锁(NOLOCK):事务不持有对数据库文件的锁,事务不能对数据库文件读和写。若是其余的事务所拥有的锁状态容许的话,这些事务就能对数据库进行读写。当一个事务启动了数据库文件时,未加锁是默认状态。

共享锁(SHARED):此锁只容许读数据库文件。任意数量的事务均可以同时在同一文件上持有此锁,所以能够有任意多的并发读事务。当一个或多个事务在文件上持有共享锁时,别的事务不容许写数据库文件。

保留锁(RESERVED):此锁容许从数据库文件中读取。保留锁指一个事务在未来某些时间点要写数据库文件,可是当前对数据库只是读操做。在一个文件上最多只能有一个保留锁,可是保留锁能够与任何数量的共享锁共存。其余的事务可能会得到此文件的共享锁,但不能是其余类型的锁。

未决锁(PENDING):此锁容许读书据库文件。一个未决锁表示事务想尽快的写入数据库文件。此锁等待当前全部的共享锁清除以获取一个排他锁。在文件上最多只能有一个未决锁,可是与共享锁的区别是:其余事务能持有已得到的锁,可是不能在得到新的锁(共享锁或其余锁)。

排他锁/独占锁(EXCLUSIVE):这是唯一的容许写数据库文件的锁(也容许读)。在文件上只容许有唯一的排他锁,不能与其余类型的排他锁共存。

矩阵以下:

  (新申请)共享锁 (新申请)保留锁 (新申请)未决锁 (新申请)排他锁
(已有)共享锁 Y Y Y N
(已有)保留锁 Y N N N
(已有)未决锁 N N N N
(已有)排他锁 N N N N

像SQLite这样的数据库系统至少须要排他锁,其余的锁的模式只是增长事物的并发性,只有使用了排他锁,事务才能串行的执行事务。有了共享锁和排他锁,系统能同时并发的执行不少读事务。在实践中,事务在共享锁下读取一个数据元素,修改这个元素,申请一个排他锁再把数据元素写回到文件中。若是两个事务同时这样作的话可能会产生死锁,在死锁中事务执行不能取得进展。保留锁和未决锁都是为了减小这些死锁而设计。这两种锁也帮忙改善了所谓的读者写着问题(读者数量永远大于写者数量)。

1.2.2 锁获取协议

在读取数据库文件的第一页(任何一页)以前,事务获取一个共享锁表示它打算从文件中读取页。在修改数据库中的任何一页以前,事务获取一个保留锁表示它打算在不久的未来写。得到了保留锁,事务就能在缓冲页内进行修改。在写入数据库前,事务须要得到对数据库的排他锁。普通的锁事务是从无锁->共享锁->保留锁->未决锁->排他锁。共享锁到未决锁的直接转化只能是由于日志文件须要回滚。可是若是这样就没有其余的事务会从共享锁转化到保留锁。未决锁是一个中间锁,在锁管理器外是不可见的:尤为是页面管理器不会要求锁管理器获得一个未决锁,通常会要求一个排他锁。所以未决锁只是为得到排他锁的一个中间步骤。为了防止在已得到未决锁后得到排他锁失败,则页面管理器会执行后续的请求来升级未决锁为排他锁。

虽然锁解决了并发控制问题,可是引入一个新问题。假设两个事务都对一个文件持有共享锁,他们都请求保留锁。其中一个获得了保留锁,另外一个等待。过了一会,持有保留锁的事务要请求排他锁,等待另外一个事务清除共享锁。可是共享锁将永远不会清除由于那个持有共享锁的事务一直在等待。这种状况叫作死锁。有两种方法来对付死锁:1.预防 2.检测和破坏。SQLite避免死锁的造成,SQLite老是在非阻塞状态下获取文件锁。若是其不能表明一个事务获取锁,将会重试有限次数(重试次数将会由程序在运行时预设,默认次数是一次)。若是全部的重试失败的话,将会返回SQLITE_BUSY错误代码给应用程序。应用程序会后退并稍后重试或者停止事务。所以,不存在着造成死锁的可能性。然而至少在理论上,事务没有成功得到锁会致使饥饿。可是SQLite不是应用在企业级的高并发程序中,饥饿就不是个大问题。

1.2.3 锁实现

SQLite用本地操做系统的文件锁函数实现SQLite本身的锁系统(锁实现是平台相关的,由于不一样系统由不一样的API)。Linux实现了两个锁模式,读所和写锁, 来锁定文件相邻区域。为了不混淆,用读锁和写锁分别表明共享锁和排他锁。Linux把锁分配给进程(单线程应用程序),或线程(多线程应用程序)。许多线程均可以在同一文件区域上持有读锁,可是只能有一个线程能够在一个文件区域上持有写锁。写锁是排他的,不管读锁仍是写锁。读锁和写锁能够在一个文件上共存,可是是在不一样的区域。一个线程只能在一个区域持有一种锁,若是在一个已经加锁的区域应用一个新的锁,则现有的锁会转换成新的锁模式。SQLite用本地操做系统在不一样文件区域的锁模式实现了SQLite本身的四种锁模式(这是经过fcntl系统调用实现设置和释放本地区域的锁)。

在指定范围的字节上设置一个读锁来得到一个共享锁。

在特定范围的全部字节上设置一个写锁来得到一个排他锁。

在共享范围的外部的一个字节上设置一个写锁来得到一个保留锁,这个字节叫作保留锁字节。

在共享范围的外部的不一样于保留锁字节的另外一个字节来设定一个写锁来得到未决锁。

SQLite在共享区域保留510个字节(区域大小在文件头处以名为SHARED_SIZE的宏定义)。区域从SHARED_FIRST开始。

PENDING_BYTE宏(在0X40000000处定义,这是在过1G的第一个字节处定义锁字节的开始)。

紧挨着PENDING_BYTE宏的下一个字节定义了PENDING_BYTE宏,紧挨着PENDING_BYTE的下一个字节定义了SHARED_FIRST

全部的锁定字节都要装进到一个单独的数据库页中,即便最小的页大小是512字节的。

(1+1+510=512)即(PENDING_BYTE+RESERVED_BYTE+SHARED_SIZE=512)。

在windows上,锁是强制性的,即锁对全部进程都是强制性的,不管进程之间是否有合做。锁定的空间是由操做系统保留的。所以,SQLite不能在被锁定的空间内存储实际的数据。所以页面管理器不会分配涉及到锁定的页面,这个页面也不会应用到其余平台上去,也不会被跨平台的其余数据库所使用。PENDING_BYTE在这么高的地址是由于这样SQLite就不会分配一个无用的页,除非是一个很大的数据库。为了得到对数据库文件的共享锁,线程首先会对PENDING_BYTE得到一个本地的读锁来确保没有其它的进程/线程在文件上有一个未决锁。若是成功了,从SHARED_FIRST开始的SHARED_SIZE范围的字节就会被读锁定,而后,在PENDING_BYTE上的读锁被释放。

某些版本的windows只支持写锁,所以为了得到对一个文件的共享锁,一个单独的在范围外的字节会被写锁定,这个字节是被随机选取的,因此这两个独立的读者能同时访问文件,除非他们不幸的的选择了一样的字节来设置写锁。在这样的系统中,读并发会被共享区域的规模大小所限制。线程在得到共享锁以后只能得到保留锁。为了得到一个保留锁,首先要对RESERVED_BYTE加写锁,同时线程不释放它对文件的共享锁(这样确保了其余线程不能得到文件的排他锁)。

一个线程在得到未决锁的过程当中不会得到保留锁,这个属性用来在SQLite在系统崩溃后回滚一个日志文件。若是线程从共享锁->保留锁->未决锁,在加未决锁时不会释放那两个锁。线程只在得到了未决锁后才能得到排他锁。为了得到排他锁,会在整个共享空间上加一个写锁。由于其它线程至少须要一个字节的读锁,因此排他锁能够肯定没有其它的锁会被得到。

1.3 日志管理

日志就是当事务或子事务语句停止时,被用于恢复数据库的信息库,也用于应用程序崩溃,系统崩溃,掉电。SQLite为每一个数据库维护一个日志文件(内存数据库没有日志文件)。只确保事务的回滚(undo而非redo),日志文件常常被叫作回滚日志。日志文件常见于数据库文件相同的目录,并有相同的文件名,可是有-journal尾部。SQLite只容许在一个数据库文件上最多有一个写事务。每次写事务都即时的创建日志文件,而且当事务结束时删除文件。

1.3.1 日志结构

SQLite把回滚日志记录划分到可变大小的日志段中。每一个段以一个段头开始,后接一个或多个日志记录。段头的格式是:

magic number:8字节,0xD9, 0xD5, 0x05, 0xF9, 0x20, 0xA1, 0x63, and 0xD7。只用来作全面的检查,无特别意义。

number of records:4字节,用于记录在这个日志段中有多少个有效的记录数。

random number:4字节,用于估计各个日志记录的校验和。不一样的日志段可能有不一样的随机数。

initial database page count:4字节,表示当前事务开始时有多少页在文件中。

sector size:表示日志所在的磁盘扇区大小。

unused space:表示是头中未被使用的留做填充区间。

SQLite支持异步事务处理比普通的事务快。SQLite不建议使用异步事务,可是能够经过执行SQLite的编译命令来设置异步模式,这种模式经常使用于开发阶段来减小开发时间。对于一些不测试从失败恢复的测试程序也有好的效果。异步事务既不刷新日志,也不刷新数据库文件。日志文件将有唯一的日志段,日志段数是-1(0XFFFFFFFF),实际值是文件的大小。回滚日志文件一般包括一个日志段。可是在某些状况下,是个多段文件,SQLite屡次写段头记录。(在刷新缓冲区章)每次写头记录,都在扇区边界写。在一个多段的日志文件中,number of recordes字段不会是0XFFFFFFFF。

1.3.2 日志记录结构

从当前写事务的非select语句产生日志记录。SQLite在页级用旧值记录技术。在对任何页作第一次修改以前,该页的原始内容(连同其页号)都被做为一个记录写入日志文件。记录中也包括了一个32位的校验和。校验和涵盖了页号和页面的内容。出如今日志段头的32位随机值用来作校验密钥。随机值很重要,由于出如今日志文件尾的垃圾数据多是其余文件的数据,可是这个文件如今已经被删除了。若是垃圾数据来自于一个过期的日志文件,校验和多是正确的。可是经过初始化校验和与随机值,不一样的日志文件对应不一样的值。SQLite最小化该风险。

1.3.3 多数据库事务日志

一个应用程序能够用执行SQLite的attach命令,在一个打开的链接上附加额外的数据库。若是一个事务修改了多个数据库,则每一个数据库有其本身的回滚日志。他们是独立的回滚日志,互相不知道对方的存在。为了解决这个问题,SQLite额外的维护了一个单独的聚合日志叫作主日志。主日志不包含任何用于回滚目的的日志记录,可是,主日志包含了每一个参与事务的单独的回滚的日志的名称。每一个单独的回滚日志页包含了主日志的名字。若是没有附加数据库,或者没有正参与当前用于更新事务的附加数据库,则主日志就不会被建立,普通的回滚日志也不会包含任何关于主日志的信息。主日志通常位于主数据库文件相同的目录,有相同的名字,可是付以-mj,后跟八位的随机数字字母串。主日志也是临时性的文件,当事务试图提交时建立,当提交处理完成时删除。

1.3.4 声明日志

当在一个用户事务中,SQLite为最新执行的非select语句维护了一个语句子日志。日志要求从语句执行失败恢复数据库。语句日志是独立的,普通的回滚日志文件,是被任意命名的临时文件(以sqlite_开头)。文件不是为崩溃恢复所须要的,而是为语句停止所须要的。当语句执行完后,SQLite删除文件。日志没有段头记录,number of log records值保存在一个内存中的数据结构中,因此数据库文件大小存在语句开始的位置。这些日志记录没有校验和。

1.3.5 日志协议

SQLite遵循预写日志协议(WAL),以保证数据库变化的耐用性,和应用程序,系统,电源故障发生时数据库的可恢复性。把日志记录写到磁盘上是懒惰的,SQLite不强制马上写到磁盘上。然而,在把下一页写到磁盘上以前,会强制把全部的日志记录写到磁盘上,这叫作刷新日志。刷新日志确保被写入到日志文件的全部的页都被写到了磁盘上。直到把日志写到磁盘上之前,修改数据库文件都是不安全的。若是数据库在日志刷新到磁盘上之前就被修改了,发生了断电,未刷新的日志记录将会被丢失,SQLite将不能彻底的回滚事务对数据库的影响,结果是数据库损坏。

1.3.6 提交协议

默认的提交逻辑是:提交强制日志,提交强制数据库。当应用程序提交事务时,SQLite确保在回滚日志中的全部记录都要被写到磁盘上。在提交末尾,回滚日志被删除,事务完成。若是在此以前系统崩溃,事务提交会失败,当数据库下次被读取时,会发生回滚。然而,在删除回滚日志文件前,全部对数据库的改变都会被写回磁盘。这样作确保数据库收到从事务开始到日志文件被删除以前的全部更新。

SQLite在提交异步事务时不执行数据库和日志文件的的刷新。所以,当发生故障时,数据库可能被破坏,异步事务是会产生警告的。

 

1.4 事务性操做

像其余DBMS同样,SQLite的事务管理包括两部分,正常处理,恢复处理。在正常处理中,页面管理器在日志文件中保存恢复信息,若是须要的话会在恢复处理期使用保存的信息。正常处理涉及了从数据库文件中读取页,向数据库文件中写入页,提交事务和子事务。此外,页面管理器把刷新页面缓冲区做为正常处理的工做。大多数事务和语句子事务本身提交。可是有时候,一些事务和子事务停止本身。更罕见的,有应用程序或系统错误。在任一状况下,SQLite都须要经过执行一些回滚操做来把数据库恢复成一个可接受的一致性状态。在语句和事务停止的状况下,内存中的可靠信息对恢复是有用的。在崩溃的状况下,可能会损坏数据库,由于没有内存信息。

1.4.1 读操做

为了操做数据库页,客户端B+树模块须要以页号为参数使用sqlite3_get函数。客户端须要调用这个函数,即便页不在数据库文件中:页会被页管理器建立。若是一个共享锁或更强的锁尚未被加到文件上的话,函数会得到文件的一个共享锁。若是获取共享锁失败,那么有其余事务正持有不兼容的锁,会返回给调用者一个SQLITE_BUSY的错误代码。不然,会执行一个缓冲区读操做,并给调用者返回页面。缓冲区读操做会钉住页面。在页面管理器首次得到对一个数据库文件的共享锁时,对调用者来讲已经开始了对文件的隐式的读事务。在这点上,会决定文件是否须要恢复。若是文件确实须要恢复,页面管理器会在把页面返回给调用者以前执行恢复。

1.4.2 写操做

在修改一个页面以前,客户端B+树模块必定要已经钉住该页面(调用sqlite3pager_get)。对页面调用sqlite3pager_write函数使得此页面是可写的。第一次在任何页面上调用sqlite3pager_write函数,页管理器须要得到对数据库文件的保留锁。若是页面管理器不能得到该锁,意味着有其余事务得到了保留锁或更强的锁。在这种状况下,写入失败,页面管理器返回给调用者SQLITE_BUSY。页面管理器第一次获取一个保留锁时,即读事务升级成写事务。在此时,页面管理器创建并代开回滚日志。初始化第一段的头记录,在记录上记录了原始的数据库文件的大小,并把记录写入日志文件。

为了使一个页面可写,页面管理器写把页面的原始内容写入一个新的日志记录并写到回滚日志中。一旦页面变成可写的,客户端就能够不通知页面管理器任意次数的写页面。对页面的更改不会被马上的写回数据库文件。

一旦一个页面的镜像被拷贝到回滚日志中去,这一页就不会出如今新的日志记录中,即便当前的事务在此页上屡次调用写函数。这种日志的一个很好的属性就是也能从日志中被拷贝并被从新装载回去。此外,撤销操做(undo)是等幂的,所以撤销操做是不产生任何补偿日志记录的。SQLite从不在日志中保存一个新的页(即增长,例如附加以当前事务附加到数据库上),由于页中没有旧的值。可是,日志文件在被建立的时候,就在日志段头记录指出数据库文件的初始大小。若是数据库文件在事务中膨胀的话,文件将会以回滚的形式被截断到原始的尺寸。

1.4.3 缓存刷新

刷新缓存是页面管理器的内部操做,客户端不可能直接调用刷新缓存。最终,页面管理器想把一些已经修改的页面写回到数据库文件,或者是由于缓冲区已满,须要进行缓冲区替换,或者是由于事务准备提交修改。页面管理器执行以下步骤:

1. 肯定是否须要刷新日志文件。若是事务是异步的,而且已经向日志文件中写入了新数据,而且数据库不是一个临时文件(若是是一个临时数据库,咱们不关心在电源故障后是否能回滚,因此没有日志刷新),那么页面管理器就决定作一第二天志刷新。在这种状况下,会作一个对日志文件的fsync系统调用,确保至今的写的全部的日志记录都已经在磁盘上了(而不是在操做系统空间或在磁盘控制器的内部缓存中)。此时,页面管理器不在当前日志段头写number of log records值(此值是回滚文件的重要资源,当段头被格式化,值由于同步记录被设成0,由于异步被设成0XFFFFFFFF)。在日志被刷新后,页面管理器把值写入当前的日志段头,并再次刷新到文件中(日志文件被刷新两次,第二次刷新致使存储值的磁盘块被重写。若是假设重写是原子的,那么能保证在刷新这个环节上日志不会被破坏。不然,会存在一些小的风险)。由于磁盘操做不是原子的,也将不会重写值。会为新来的日志记录,建立一个新的日志段。在此状况下,SQLite使用新的多段日志文件。

2. 尝试对数据库文件得到排他锁(若是其余的事务仍持有共享锁,则尝试失败,会返回SQLITE_BUSY给调用者。事务停止)。

3. 把全部的已更改的页或有选择的页写回到数据库文件上。写回页面就地进行。标志着缓冲区复制了这些页(此次就不把数据库文件刷新到磁盘上了)。若是是由于页缓冲区满了,而要写数据库文件,页面管理器不马上提交事务。事务可能会继续改变其它的页。在随后的更改写到数据库文件以前,这三步又会在重复一边。

页面管理器为写文件而得到的排他锁直到事务结束才释放。意味着其余的应用程序将不会打开其余的(读或写)事务,从页面管理器第一次写数据库文件,直到事务提交或停止为止。对于短的事务,更新都在缓冲区内进行,排他锁将在提交时被收回。

1.4.4 提交操做

SQLite遵照一个稍微不一样的提交协议取决于提交的事务是值修改了一个数据库仍是修改了多个数据库。

1.4.4.1 单数据库状况

提交一个读事务是简单的。页面管理器从数据库文件释放了共享锁,并清除了页缓冲区。为了提交一个写事务,页面管理器执行以下步骤:

1. 得到对数据库文件的排他锁(若是锁获取失败,向调用者返回SQLITE_BUSY。如今不能提交事务,由于其余的事务还在读数据库。)。把全部的修改过的页面写回数据库文件,按照缓存刷新的算法1到3.

2. 页面个管理器作一个fsync系统调用,把数据库文件刷新到磁盘上。

3. 删除日志文件。

4. 最终,释放数据库文件的排他锁,并清除页缓冲区。

事务提交点出如今回滚日志文件被删的瞬间。在那以前,若是电源故障或发生崩溃,事务就会被认为在提交过程当中发生失败。下次SQLite读取数据库时,将会回滚此次事务对数据库的影响。

1.4.4.2 多数据库状况

提交协议会涉及多一点,相似于一个事务在分布式系统中提交。虚拟机模块实际驱动提交协议,这做为协调提交。每一个页管理器作本身本地那部分的提交。对于只改写了一个数据库(不包括临时数据库)的一个读事务或写事务来讲,协议在每一个涉及的数据库上执行了一次正常的提交。若是事务修改了多个数据库文件,以下的提交协议被执行:

1. 释放事务没更新的数据库的共享锁。

2. 获取事务已更新的数据库的排他锁。

3. 新建一个新的主日志文件。把全部单个的回滚日志文件名填写到主日志文件,并把主日志和日志目录刷新到磁盘上(临时数据库名不包含在主日志中)。

4. 把主日志文件名字写入此主日志文件包括的全部单独的回滚日志文件中去,并刷新回滚日志(直到事务提交时,页面管理器才知道是多数据库的一部分。只有在这时才知道是多数据库事务的一部分。)。

5. 刷新单独的数据库文件。

6. 删除主日志文件而且刷新日志目录。

7. 删除全部的回滚日志文件。

8. 释放数据库文件的排他锁,而且清除页面缓存。

事务被认为在主日志文件删除时已经被提交。在此以前,若是电力故障或系统崩溃,事务会被认为是在提交处理过程当中失败。当SQLite下次读取数据库时,将会恢复他们各自以前的状态,并开始事务。

若是主数据库是临时文件(或内存数据库)。SQLite不保证多数据库事务的原子性。就是说,全局的恢复多是行不通的。由于并无创建一个主日志。虚拟机逐个的执行简单的commit命令。所以每一个数据库文件都被保证是内部是原子的。所以在发生故障时,有的数据库会改变有的不会。

1.4.5 语句执行

对于语句子事务级别的操做:读,写,提交在下边讨论。

1.4.5.1 读操做

一个语句子事务经过主用户事务来读取一页。全部的规则都遵循主事务

1.4.5.2 写操做

写操做分为两部分:锁定和日志。一个与句子事务经过主用户事务得到锁。可是语句日志不一样,经过使用单独的临时语句日志文件处理。QSLite把一部分日志记录写在在语句日志中,一部分写在主回滚日志中。当一个子事务试图经过调用sqlite3pager_write操做使一个页变得可写时,页面管理器器执行以下两个备选的操做:1. 若是这个页不在回滚日志中,会加一个新的记录到回滚日志中。2. 不然,若是页不在语句日志中,会加一个新的日志记录到语句日志中(当事务写第一条记录到文件时,页面管理器创建日志文件)。页面管理器不会刷新语句日志,由于不要求崩溃恢复。若是电源故障发生,则主回滚日志会处理数据库的回滚。一个缓冲区中的页可能又是回滚日志中的页的一部分,又是语句日志中的页的一部分:回滚日志有最老旧的页镜像。

1.4.5.3 提交操做

语句提交很简单,页面管理器删除语句日志文件。

1.4.6 事务停止

在SQLite中恢复事务停止是很简单的。页面管理器也许或也许不须要去消除事务对数据库文件的影响。若是事务只在数据库文件上持有一个保留锁或未决锁,就会保证文件不会被改变,页面管理器删除日志文件,并放弃全部页面缓冲区的脏页。不然,一些页就会被事务写回数据库文件,页面管理器执行以下回滚操做:页面管理器逐个的读取回滚日志记录,并从新从记录中加载页镜像。(数据库页最多只能被事务记录一次,日志记录在页镜像以前存储。)所以,在日志记录扫描结束时,数据库被从新加载成它开始事务以前的原始状态。若是事务扩大了数据库,页面管理器就会截断数据库到原先的规模。页面管理器就会首先刷新数据库文件,而后删除回滚日志文件。

1.4.7 语句停止

语句操做节指出,一条语句可能又被加载到语句日志中,又被加载到回滚日志中。SQLite须要从语句日志和回滚日志中回滚全部的日志记录。当一个语句子事务在回滚日志中写第一条日志记录时,页面管理器会保存记录在内存中数据结构的位置。从这条记录开始,直到回滚日志文件的结束,都被子事务写。页面管理器从日志记录从新加载页面镜像。而后删除语句日志文件,可是留着回滚日志文件而不改变内容。当语句子事务开始时,页面管理器记录数据库的大小。页面管理器截断数据库文件到不一样大小。

1.4.8 从失败恢复

在崩溃或系统故障后,不一致的数据可能会留在数据库文件中。当没有应用程序正在进行更新数据库时,可是此时出现了一个回滚日志文件,意味着以前的事务可能已经失败,SQLite可能要在能够被再次正常使用以前先要去除事务的影响,恢复数据库。若是相应的数据库文件是无锁的或是有共享锁,那么说回滚日志文件是热的。当一个写事务在完成途中或者是失败了,那么日志是热的。然而,若是是多数据库事务,并且也没有主日志,那么说回滚日志不是热的,这意味着失败发生时,事务被提交。一个热的日志意味着它须要被回滚以从新加载以保持数据库的一致性。

若是不涉及主日志,那么当存在数据库文件没有保留锁或更强的锁,而且文件回滚日志存在时,那么回滚日志就是热的。(有保留锁的事务创建一个回滚日志文件,这个文件不是热的。)若是一个主日志名出如今回滚日志中,若是主日志存在而且在相应数据库文件上没有保留锁,那么回滚日志是热的。当一个数据库开始时,在大多数的DBMS中,事务管理器当即在数据库上启动一个恢复恢复操做。SQLite用一个延迟的恢复。如在读操做节中,当执行从数据库中第一次读一页(任何页),页面管理器经过恢复逻辑而且仅当恢复日志是热的时候,恢复数据库。

若是当前的应用程序只对数据库文件有读的权限,在文件或包含目录上没有写的权限,则恢复会失败,应用程序会从SQLite库中获得一段意外的错误代码。

在实际上从文件读取一页以前,会执行以下的恢复步骤:

1. 得到对数据库文件的共享锁。若是不能得到锁,会返回一个SQLITE_BUSY给应用程序。

2. 检查数据库文件是否有热日志,若是数据库没有热日志,恢复操做结束。若是有热日志,日志会按随后的回滚算法回滚。

3. 若是得到了对数据库文件的排他锁(经过未决锁)。(页面管理器不会获取一个保留锁,由于这会使得其余的页面管理器认为日志再也不是热的而且读数据库。须要一个排他锁由于做为恢复工做的一部分,将要写数据库文件。)若是获取锁失败,意味着其余的页面管理器已经在试图作回滚。在这种状况下,会丢弃全部锁,关闭数据库文件,并返回SQLITE_BUSY给应用程序。

4. 从日志文件中读取全部的日志记录并撤销他们。这将从新加载数据库到它执行崩溃了的事务之前的状态,所以,数据库在一个一致的状态。

5. 刷新数据库文件。这保护了在电源故障或其余崩溃的状况下的数据库的完整性。

6. 删除回滚日志。

7. 若是是安全状态下的话,删除主日志文件。(此步骤是可选的)

8. 释放排他锁(和未决锁),可是保留共享锁。(这是由于页面管理器用sqlite3_get函数执行恢复)在前面的算法成功执行以后,数据库文件保证被从新加载到已失败事务执行以前的状态。这时从文件读是安全的。

若是没有单个的回滚日志指向主日志的话,主日志是过期的。为了弄清主日志是不是陈旧的,页面管理器首先读取主日志来得到全部回滚日志的名字。而后检查每一个回滚日志。若是其中任何一个存在并指回到主日志,则主日志是不过期的。若是全部的回滚日志要么丢失要么指向其余的主日志要么根本没有主日志的话,则主日志是过期的,页面管理器删除日志。没有要求过期的主日志应该被删除。这样作的惟一目的就是释放被占用的磁盘空间。

1.4.9 检查点

为了减小在失败恢复时的工做量,大多数的DBMS按期在数据库上执行检查点。SQLite同一时间在一个数据库文件上最多只能有一个写事务。日志文件只包含了来自特定事务的日志记录。此时,SQLite不须要执行检查点,而且也没有任何的内嵌的检查点的逻辑。当一个事务提交时,SQLite确保在删除日志文件以前,全部来自于事务的更新都是在数据库文件中的。

相关文章
相关标签/搜索