前言
事务处理是DBMS中最关键的技术,对SQLite也一样,它涉及到并发控制,以及故障恢复等等。在数据库中使用事务可以保证数据的统一和完整性,同时也可以提高效率。假设需要在一张表内一次插入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{ }
在SQLite中,如果没有为当前的SQL命令(SELECT除外)显示的指定事务,那么SQLite会自动为该操作添加一个隐式的事务,以保证该操作的原子性和一致性。当然,SQLite也支持显示的事务,其语法与大多数关系型数据库相比基本相同。见如下示例:
sqlite> BEGIN TRANSACTION; sqlite> INSERT INTO testtable VALUES(1); sqlite> INSERT INTO testtable VALUES(2); sqlite> COMMIT TRANSACTION; --显示事务被提交,数据表中的数据也发生了变化。 sqlite> SELECT COUNT(*) FROM testtable; COUNT(*) ---------- 2 sqlite> BEGIN TRANSACTION; sqlite> INSERT INTO testtable VALUES(1); sqlite> ROLLBACK TRANSACTION; --显示事务被回滚,数据表中的数据没有发生变化。 sqlite> SELECT COUNT(*) FROM testtable; COUNT(*) ---------- 2
Page Cache之事务处理——SQLite原子提交的实现
下面通过具体示例来分析SQLite原子提交的实现(基于Version 3.3.6的代码):
CREATE TABLE episodes( id integer primary key,name text, cid int); insert into episodes(name,cid) values("cat",1); --插入一条记录
它经过编译器处理后生成的虚拟机代码如下:
sqlite> explain insert into episodes(name,cid) values("cat",1); 0|Trace|0|0|0|explain insert into episodes(name,cid) values("cat",1);|00| 1|Goto|0|12|0||00| 2|SetNumColumns|0|3|0||00| 3|OpenWrite|0|2|0||00| 4|NewRowid|0|2|0||00| 5|Null|0|3|0||00| 6|String8|0|4|0|cat|00| 7|Integer|1|5|0||00| 8|MakeRecord|3|3|6|dad|00| 9|Insert|0|6|2|episodes|0b| 10|Close|0|0|0||00| 11|Halt|0|0|0||00| 12|Transaction|0|1|0||00| 13|VerifyCookie|0|1|0||00| 14|Transaction|1|1|0||00| 15|VerifyCookie|1|0|0||00| 16|TableLock|0|2|1|episodes|00| 17|Goto|0|2|0||00|
1、初始状态(Initial State)
当一个数据库连接第一次打开时,状态如图所示。图中最右边(“Disk”标注)表示保存在存储设备中的内容。每个方框代表一个扇区。蓝色的块表示这个扇区保存了原始数据。图中中间区域是操作系统的磁盘缓冲区。开始的时候,这些缓存是还没有被使用,因此这些方框是空白的。图中左边区域显示SQLite用户进程的内存。因为这个数据库连接刚刚打开,所以还没有任何数据记录被读入,所以这些内存也是空的。
2、获取读锁(Acquiring A Read Lock)
在SQLite写数据库之前,它必须先从数据库中读取相关信息。比如,在插入新的数据时,SQLite会先从sqlite_master表中读取数据库模式(相当于数据字典),以便编译器对INSERT语句进行分析,确定数据插入的位置。
在进行读操作之前,必须先获取数据库的共享锁(shared lock),共享锁允许两个或更多的连接在同一时刻读取数据库。但是共享锁不允许其它连接对数据库进行写操作。
shared lock存在于操作系统磁盘缓存,而不是磁盘本身。文件锁的本质只是操作系统的内核数据结构,当操作系统崩溃或掉电时,这些内核数据也会随之消失。
3、读取数据
一旦得到shared lock,就可以进行读操作。如图所示,数据先由OS从磁盘读取到OS缓存,然后再由OS移到用户进程空间。一般来说,数据库文件分为很多页,而一次读操作只读取一小部分页面。如图,从8个页面读取3个页面。
4、获取Reserved Lock
在对数据进行修改操作之前,先要获取数据库文件的Reserved Lock,Reserved Lock和shared lock的相似之处在于,它们都允许其它进程对数据库文件进行读操作。Reserved Lock和Shared Lock可以共存,但是只能是一个Reserved Lock和多个Shared Lock——多个Reserved Lock不能共存。所以,在同一时刻,只能进行一个写操作。
Reserved Lock意味着当前进程(连接)想修改数据库文件,但是还没开始修改操作,所以其它的进程可以读数据库,但不能写数据库。
5、创建恢复日志(Creating A Rollback Journal File)
在对数据库进行写操作之前,SQLite先要创建一个单独的日志文件,然后把要修改的页面的原始数据写入日志。回滚日志包含一个日志头(图中的绿色)——记录数据库文件的原始大小。所以即使数据库文件大小改变了,我们仍知道数据库的原始大小。
从OS的角度来看,当一个文件创建时,大多数OS(Windows、Linux、Mac OS X)不会向磁盘写入数据,新创建的文件此时位于磁盘缓存中,之后才会真正写入磁盘。如图,日志文件位于OS磁盘缓存中,而不是位于磁盘。
以上5步的实现代码:
//事务指令的实现 //p1为数据库文件的索引号--0为main database;1为temporary tables使用的文件 //p2不为0,一个写事务开始 case OP_Transaction: { //数据库的索引号 int i = pOp->p1; //指向数据库对应的btree Btree *pBt; assert( i>=0 && i<db->nDb ); assert( (p->btreeMask & (1<<i))!=0 ); //设置btree指针 pBt = db->aDb[i].pBt; if( pBt ){ <