SQLite事务管理

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

1.1 事务类型

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能很好的帮助一个长的用户事务承受其中一些语句的失败。

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 锁类型及其兼容性

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

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

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

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

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

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

矩阵如下:

 (新申请)共享锁(新申请)保留锁(新申请)未决锁(新申请)排他锁
(已有)共享锁YYYN
(已有)保留锁YNNN
(已有)未决锁NNNN
(已有)排他锁NNNN

像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确保在删除日志文件之前,所有来自于事务的更新都是在数据库文件中的。

    原文作者:sqlite
    原文地址: https://www.cnblogs.com/amdb/p/4045051.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞