SQLite数据库事务处理详解教程

作者:袖梨 2022-11-14

说到事务一定会提到ACID,所谓事务的原子性,一致性,隔离性和持久性。对于一个数据库而言,通常通过并发控制和故障恢复手段来保证事务在正常和异常情况下的ACID特性。sqlite也不例外,虽然简单,依然有自己的并发控制和故障恢复机制。Sqlite学习笔记(五)&&SQLite封锁机制 已经讲了一些锁机制的原理,本文将会详细介绍一个事务从开始,到执行,最后到提交所经历的过程,其中会穿插讲一些sqlite中锁管理,缓存管理和日志管理的机制,同时会介绍在异常情况下(软硬件故障,比如程序异常crash,主机掉电等),sqlite如何将数据库恢复到事务之前的状态。本文大量参考了 sqlite的官方文档,结合自己的理解,希望能把这个过程说清楚。

1.事务提交

1.1 开启一个事务

在向数据库文件写数据前,sqlite首先需要访问sqlite_master表获取元数据信息,用来对SQL语句进行语义分析,判断语句的合法性。从数据库读数据第一步,是对数据库文件上一个Shared Lock。Shared Lock允许多个事务同时读一个数据库文件,但是Shared Lock会阻止写事务向数据库文件写入数据。


1.2 读数据

获取Shared Lock后,我们可以从数据库文件中读取数据了。我们假设缓存中没有我们的page,因此需要通过读文件读取我们需要的page。这里说明下,sqlite的数据库文件实质是有一个个大小相同的page组成,默认情况下,一个page大小为1024B。通常情况下,我们需要读取若干个 page,并把这些page缓存在应用本地的cache中,这样下次访问就不需要再次从文件中读取。这里我们假设需要读取3个page,用绿色块表示。


1.3 获取Reserved Lock

在向数据库写数据之前,Sqlite需要获取一个Reserved Lock,Reserved Lock与Shared Lock类似,同时允许其他事务读取数据库。Reserved Lock与Shared Lock兼容,但与Reserved Lock互斥,即同一个时刻只允许有一个Reserved Lock。持有Reserved Lock表示事务准备要修改数据文件了,由于还没有真正修改文件,因此允许其他事务继续进行读操作,但不允许其他事务进行写数据库操作。


1.4 创建日志文件

在sqlite中,有两种日志技术,影子分页技术和WAL(write ahead log)技术。影子分页技术是sqlite默认采用的方式,后面的讨论都是基于这种假设。在操作数据文件之前,sqlite首先创建一个日志文件,并将准备要修改的page的内容写入日志,通过这种方式保留了恢复事务的所有原始信息。无论是数据库文件,还是日志文件,最基本的操作单位都是page。


1.5 修改数据

前面提到,sqlite修改数据前,先将page读到cache中,因此修改会首先修改cache中的数据。由于每个连接都有自己独立的page cache,因此写事务修改自己page cache中的数据,不会影响其他事务,其他事务依然会读到原始的page数据,不会导致脏读。下图中红色表示修改块,从图中可以看到,只有用户自身 cache的page变成了红色。

1.6 刷日志文件

修改完成后,首先将日志文件写入磁盘。这个过程非常重要,只有通过刷盘操作(fsync)将日志持久化,才能在掉电的情况下,通过日志恢复数据页。同时,这个动作也非常耗时。


1.7 获取Exclusive Lock

现在我们需要将之前对page cache的修改写入数据库文件,达到持久化目的。在这个动作之前,首先需要持有Exclusive Lock,获取该锁实际包含两个步骤,首先持有一个Pending Lock,然后再持有Exclusive Lock。Pending Lock允许持有读锁事务继续进行读操作,但不允许新的读事务进来。由于新的读事务被阻止,则将读事务数目限制在一定的范围,而已有的读事务迟早都会执行完,写事务最终可以获取到Exclusive Lock,通过这种方式避免写事务饿死的情况。

1.8 将修改写入数据文件并刷盘

一旦持有了Exclusive Lock,则此时sqlite中只有一个事务,没有其他读事务去读文件。因此,这时候向数据文件中写数据是安全的。为了保证写入动作真正落入磁盘,还需要进行刷盘动作。与刷日志一样,将数据文件修改刷盘动作也是为了保证掉电情况下,更新依然可以持久化,同样这个操作也很耗时。其中红色块表示修改块,此时用户进程空间,OS buffer,以及DISK都已经修改。


1.9 删除日志文件

进行这步时,日志文件和数据文件修改都已经固化到磁盘。如果在1.8步之前,发生掉电,由于日志文件已经安全落盘,因此可以将数据库恢复到事务开始前的状态。由于数据文件修改已经固化,我们可以将日志文件删除。通过日志文件的存在与否,判断我们是需要将事务回滚还是提交。由于删文件也是一个比较耗时的动作,sqlite对此进行了优化,通过参数选项,可以选择将日志全部初始化0,或是直接将文件截断,达到提高性能的目的。


1.10 释放Exclusive Lock

最后一步是释放Exclusive Lock,这样其他事务才有机会读、写数据文件。这里有一个问题,每个连接都有自己的page cache,如果page cache中的内容已经被改了,并写入到了文件中,那么其它事务如何感知,将自己本地的old-page清理,重新从文件中读?sqlite通过一个计数器来控制,这个计数器存在数据库文件的第一个page中。每次数据文件修改时,这个值也会同时自增。事务开始时,会读取计数器,在读取page 时,会再次检查计数器是否发生变化,如果发生变化,说明有事务提交,则将本地的cache全部清空,重新从数据库文件中获取。


2.事务回滚

正常情况下,通过上述的事务提交流程,就可以保证事务的ACID特性。但是事务在执行过程中发生异常呢,这时候就需要通过事务回滚来将数据库恢复到事务开始前的状态。下面假设一种情况,来介绍回滚流程。

2.1 发生故障

假设在1.8之前,写数据库文件时,发生了掉电故障。当故障恢复后,情形可能如右图所示,只有部分页写入了磁盘,甚至有一个页可能只写入了一部分。由于执行到这个步骤时,日志已经安全落盘,因此可以借助日志进行恢复。


2.2 热日志

任何一个连接在操作数据库之前,会首先判断是否有热日志存在,因为有热日志存在意味着可能需要故障恢复。所谓热日志,是指需要事务提交过程中发生了故障,需要利用日志恢复。


2.3 回滚未完成的操作

在利用日志进行恢复前,首先持有Exclusive Lock,这样避免多个连接同时进行故障恢复,持有Exclusive Lock后,才可以开始修改数据库文件。sqlite从日志文件中读取原始的数据页,然后将数据页写回到数据文件中。由于日志文件头部记录了事务开始时数据文件的大小,sqlite利用这个信息来讲数据文件进行截断到原来的大小,保持文件大小恢复到事务开始时的水平。


2.4 删除日志文件

当所有日志文件中的数据页都已经拷贝到数据文件中后,进行一次刷盘操作,确保修改持久化,这时候日志文件可以被删除了。恢复完成后,将Exclusive Lock 降级到Shared Lock。这个过程完成后,数据库完成恢复。由于整个过程都是sqlite自动完成,用户完全无感知。对于用户而言,任何时候使用sqlite操作数据文件都是安全的,即使在发生了异常的情况下。



sqlite处理事务的一个例子

事务在数据库中是一个重要的概念,使用事务可以保证数据的统一和完整性。同时也可以提高效率。拿我们上面创建的persons表来说,假设我要一次插入20个人的名字才算是操作成功,那么,在不使用事务的情况下,如果插入过程中出现异常或者在插入过程中出现一些其他数据库操作的话,就很有可能影响了操作的完整性。所以事务可以很好地解决这样的情况,首先事务是可以把启动事务过程中的所有操作视为事务的过程。等到所有过程执行完毕后,我们可以根据操作是否成功来决定事务是否进行提交或者回滚。提交事务后会一次性把所有数据提交到数据库,如果回滚了事务就会放弃这次的操作,而对原来表的数据不进行更改。

那么,如何启动,提交还有回滚事务呢?SQLite中分别是:BEGIN、COMMIT和ROLLBACK。下面来看一下例子:

@try{

char *errorMsg;

if (sqlite3_exec(_database, "BEGIN", NULL, NULL, &errorMsg)==SQLITE_OK) {

NSLog(@”启动事务成功”);

sqlite3_free(errorMsg);

sqlite3_stmt *statement;

if (sqlite3_prepare_v2(_database, [@"insert into persons(name) values(?);" UTF8String], -1, &statement, NULL)==SQLITE_OK) {

//绑定参数

const char *text=[@”张三” cStringUsingEncoding:NSUTF8StringEncoding];

sqlite3_bind_text(statement, index, text, strlen(text), SQLITE_STATIC);


if (sqlite3_step(statement)!=SQLITE_DONE) {

sqlite3_finalize(statement);

}

}


if (sqlite3_exec(_database, "COMMIT", NULL, NULL, &errorMsg)==SQLITE_OK) {

NSLog(@”提交事务成功”);

}

sqlite3_free(errorMsg);

}else{

sqlite3_free(errorMsg);

}

}

@catch(NSException *e){

char *errorMsg;

if (sqlite3_exec(_database, "ROLLBACK", NULL, NULL, &errorMsg)==SQLITE_OK) {

NSLog(@”回滚事务成功”);

}

sqlite3_free(errorMsg);

}

@finally{

}


相关文章

精彩推荐