SQLite学习笔记(七)&&事务处理

说到事务一定会提到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操作数据文件都是安全的,即使在发生了异常的情况下。

参考文档

https://www.sqlite.org/atomiccommit.html

时间: 2024-10-18 04:12:31

SQLite学习笔记(七)&&事务处理的相关文章

SQLite 学习笔记

SQLite 学习笔记. 一.SQLite 安装    访问http://www.sqlite.org/download.html下载对应的文件.    1.在 Windows 上安装 SQLite.需要下载 sqlite-shell-win32-*.zip 和 sqlite-dll-win32-*.zip 压缩文件.        创建文件夹 C:\sqlite,并在此文件夹下解压上面两个压缩文件,将得到 sqlite3.def.sqlite3.dll 和 sqlite3.exe 文件.   

sqlite学习笔记8:C语言中使用sqlite之创建表

前面已经说了如何打开和关闭数据库,这次要说得是如何执行SQL语句,来创建一张表. 要用的的函数: sqlite3_exec(sqlite3* db, const char *sql, sqlite_callback callback, void *data, char **errmsg) 参数: db:已经打开的数据库实例 sql:SQL语句,是一个字符串 callback:是一个回调函数 data:做为回调函数的第一个参数 errmsg:用于带回错误信息 该回调函数有两种返回值类型. 1.返回

第十七篇:博采众长--初探WDDM驱动学习笔记(七)

基于WDDM驱动的DirectX视频加速重定向框架设计与实现 现在的研究生的论文, 真正质量高的, 少之又少, 开题开得特别大, 动不动就要搞个大课题, 从绪论开始到真正自己所做的内容之间, 是东拼西凑地抄概念, 抄公式, 达到字数篇幅的要求, 而自己正真做了什么, 有哪些实际感受, 做出的内容, 相比前面的东拼西凑就几点内容, 之后就草草结束, 步入感谢的段落. 原因不光只有学生自己, 所谓的读研, 如果没有一个环境, 学生有再大的愿望, 再强的毅力, 到头来也只是空无奈. 有些导师要写书,

马哥学习笔记七——LAMP编译安装之MYSQL

1.准备数据存放的文件系统 新建一个逻辑卷,并将其挂载至特定目录即可.这里不再给出过程. 这里假设其逻辑卷的挂载目录为/mydata,而后需要创建/mydata/data目录做为mysql数据的存放目录. 2.新建用户以安全方式运行进程: # groupadd -r mysql # useradd -g mysql -r -s /sbin/nologin -M -d /mydata/data mysql # chown -R mysql:mysql /mydata/data 3.安装并初始化my

sqlite学习笔记9:C语言中使用sqlite之插入数据

前面创建了一张表,现在给他插入一些数据,插入数据跟创建表差不多,仅仅是SQL语言不一样而已,完整代码如下: #include <stdio.h> #include <stdlib.h> #include "sqlite/sqlite3.h" #define DB_NANE "sqlite/test.db" sqlite3 *db = NULL; char* sql = NULL; char *zErrMsg = NULL; int ret =

sqlite学习笔记10:C语言中使用sqlite之查询和更新数据

前面说到的 sqlite_exec() 中的第三个参数, SQLite 将为 sql 参数内执行的每个 SELECT 语句中处理的每个记录调用这个回调函数. 本节添加了两个函数,selectFromTable和updateTable. 实例程序如下: #include <stdio.h> #include <stdlib.h> #include "sqlite/sqlite3.h" #define DB_NANE "sqlite/test.db&quo

sqlite学习笔记11:C语言中使用sqlite之删除记录

最后一节,这里记录下如何删除数据. 前面所有的代码都继承在这里了,在Ubuntu14.04和Mac10.9上亲测通过. #include <stdio.h> #include <stdlib.h> #include "sqlite/sqlite3.h" #define DB_NANE "sqlite/test.db" sqlite3 *db = NULL; char* sql = NULL; char *zErrMsg = NULL; con

Lua学习笔记(七):迭代器与泛型for

1.迭代器与闭包 迭代器是一种支持指针类型的结构,它可以遍历集合的每一个元素.在Lua中我们常常使用函数来描述迭代器,每次调用该函数就返回集合的下一个元素. 迭代器需要保留上一次成功调用的状态和下一次成功调用的状态,也就是他知道来自于哪里和将要前往哪里.闭包提供的机制可以很容易实现这个任务.记住:闭包是一个内部函数,它可以访问一个或者多个外部函数的外部局部变量.每次闭包的成功调用后这些外部局部变量都保存他们的值(状态).当然如果要创建一个闭包必须要创建其外部局部变量.所以一个典型的闭包的结构包含

python学习笔记七:条件&循环语句

1.print/import更多信息 print打印多个表达式,使用逗号隔开 >>> print 'Age:',42 Age: 42   #注意个结果之间有一个空格符 import:从模块导入函数 import 模块 from 模块 import 函数 from 模块 import * 如果两个模块都有open函数的时候, 1)使用下面方法使用: module1.open()... module2.open()... 2)语句末尾增加as子句 >>> import ma