SQLite教程(十二):锁和并发控制详解

一、概述:

在SQLite中,锁和并发控制机制都是由pager_module模块负责处理的,如ACID(Atomic, Consistent, Isolated, and Durable)。在含有数据修改的事务中,该模块将确保或者所有的数据修改全部提交,或者全部回滚。与此同时,该模块还提供了一些磁盘文件的内存Cache功能。
    事实上,pager_module模块并不关心数据库存储的细节,如B-Tree、编码方式、索引等,它只是将其视为由统一大小(通常为1024字节)的数据块构成的单一文件,其中每个块被称为一个页(page)。在该模块中页的起始编号为1,即第一个页的索引值是1,其后的页编号以此类推。
   
二、文件锁:

在SQLite的当前版本中,主要提供了以下五种方式的文件锁状态。
    1). UNLOCKED:
    文件没有持有任何锁,即当前数据库不存在任何读或写的操作。其它的进程可以在该数据库上执行任意的读写操作。此状态为缺省状态。
    2). SHARED:
    在此状态下,该数据库可以被读取但是不能被写入。在同一时刻可以有任意数量的进程在同一个数据库上持有共享锁,因此读操作是并发的。换句话说,只要有一个或多个共享锁处于活动状态,就不再允许有数据库文件写入的操作存在。
    3). RESERVED:
    假如某个进程在将来的某一时刻打算在当前的数据库中执行写操作,然而此时只是从数据库中读取数据,那么我们就可以简单的理解为数据库文件此时已经拥有了保留锁。当保留锁处于活动状态时,该数据库只能有一个或多个共享锁存在,即同一数据库的同一时刻只能存在一个保留锁和多个共享锁。在Oracle中此类锁被称之为预写锁,不同的是Oracle中锁的粒度可以细化到表甚至到行,因此该种锁在Oracle中对并发的影响程序不像SQLite中这样大。
    4). PENDING:
    PENDING锁的意思是说,某个进程正打算在该数据库上执行写操作,然而此时该数据库中却存在很多共享锁(读操作),那么该写操作就必须处于等待状态,即等待所有共享锁消失为止,与此同时,新的读操作将不再被允许,以防止写锁饥饿的现象发生。在此等待期间,该数据库文件的锁状态为PENDING,在等到所有共享锁消失以后,PENDING锁状态的数据库文件将在获取排他锁之后进入EXCLUSIVE状态。
    5). EXCLUSIVE:
    在执行写操作之前,该进程必须先获取该数据库的排他锁。然而一旦拥有了排他锁,任何其它锁类型都不能与之共存。因此,为了最大化并发效率,SQLite将会最小化排他锁被持有的时间总量。
   
    最后需要说明的是,和其它关系型数据库相比,如MySQL、Oracle等,SQLite数据库中所有的数据都存储在同一文件中,与此同时,它却仅仅提供了粗粒度的文件锁,因此,SQLite在并发性和伸缩性等方面和其它关系型数据库还是无法比拟的。由此可见,SQLite有其自身的适用场景,就如在本系列开篇中所说,它和其它关系型数据库之间的互换性还是非常有限的。

三、回滚日志:

当一个进程要改变数据库文件的时候,它首先将未改变之前的内容记录到回滚日志文件中。如果SQLite中的某一事务正在试图修改多个数据库中的数据,那么此时每一个数据库都将生成一个属于自己的回滚日志文件,用于分别记录属于自己的数据改变,与此同时还要生成一个用于协调多个数据库操作的主数据库日志文件,在主数据库日志文件中将包含各个数据库回滚日志文件的文件名,在每个回滚日志文件中也同样包含了主数据库日志文件的文件名信息。然而对于无需主数据库日志文件的回滚日志文件,其中也会保留主数据库日志文件的信息,只是此时该信息的值为空。
    我们可以将回滚日志视为"HOT"日志文件,因为它的存在就是为了恢复数据库的一致性状态。当某一进程正在更新数据库时,应用程序或OS突然崩溃,这样更新操作就不能顺利完成。因此我们可以说"HOT"日志只有在异常条件下才会生成,如果一切都非常顺利的话,该文件将永远不会存在。

四、数据写入:

如果某一进程要想在数据库上执行写操作,那么必须先获取共享锁,在共享锁获取之后再获取保留锁。因为保留锁则预示着在将来某一时刻该进程将会执行写操作,所以在同一时刻只有一个进程可以持有一把保留锁,但是其它进程可以继续持有共享锁以完成数据读取的操作。如果要执行写操作的进程不能获取保留锁,那么这将说明另一进程已经获取了保留锁。在此种情况下,写操作将失败,并立即返回SQLITE_BUSY错误。在成功获取保留锁之后,该写进程将创建回滚日志。
    在对任何数据作出改变之前,写进程会将待修改页中的原有内容先行写入回滚日志文件中,然而,这些数据发生变化的页起初并不会直接写入磁盘文件,而是保留在内存中,这样其它进程就可以继续读取该数据库中的数据了。
    或者是因为内存中的cache已满,或者是应用程序已经提交了事务,最终,写进程将数据更新到数据库文件中。然而在此之前,写进程必须确保没有其它的进程正在读取数据库,同时回滚日志中的数据确实被物理的写入到磁盘文件中,其步骤如下:
    1). 确保所有的回滚日志数据被物理的写入磁盘文件,以便在出现系统崩溃时可以将数据库恢复到一致的状态。
    2). 获取PENDING锁,再获取排他锁,如果此时其它的进程仍然持有共享锁,写入线程将不得不被挂起并等待直到那些共享锁消失之后,才能进而得到排他锁。
    3). 将内存中持有的修改页写出到原有的磁盘文件中。
    如果写入到数据库文件的原因是因为cache已满,那么写入进程将不会立刻提交,而是继续对其它页进行修改。但是在接下来的修改被写入到数据库文件之前,回滚日志必须被再一次写到磁盘中。还要注意的是,写入进程获取到的排他锁必须被一直持有,直到所有的改变被提交时为止。这也意味着,从数据第一次被刷新到磁盘文件开始,直到事务被提交之前,其它的进程不能访问该数据库。
    当写入进程准备提交时,将遵循以下步骤:
    4). 获取排他锁,同时确保所有内存中的变化数据都被写入到磁盘文件中。
    5). 将所有数据库文件的变化数据物理的写入到磁盘中。
    6). 删除日志文件。如果在删除之前出现系统故障,进程在下一次打开该数据库时仍将基于该HOT日志进行恢复操作。因此只有在成功删除日志文件之后,我们才可以认为该事务成功完成。
    7). 从数据库文件中删除所有的排他锁和PENDING锁。
    一旦PENDING锁被释放,其它的进程就可以开始再次读取数据库了。
    如果一个事务中包含多个数据库的修改,那么它的提交逻辑将更为复杂,见如下步骤:
    4). 确保每个数据库文件都已经持有了排他锁和一个有效的日志文件。
    5). 创建主数据库日志文件,同时将每个数据库的回滚日志文件的文件名写入到该主数据库日志文件中。
    6). 再将主数据库日志文件的文件名分别写入到每个数据库回滚日志文件的指定位置中。
    7). 将所有的数据库变化持久化到数据库磁盘文件中。
    8). 删除主日志文件,如果在删除之前出现系统故障,进程在下一次打开该数据库时仍将基于该HOT日志进行恢复操作。因此只有在成功删除主日志文件之后,我们才可以认为该事务成功完成。
    9). 删除每个数据库各自的日志文件。
    10).从所有数据库中删除掉排他锁和PENDING锁。
   
    最后需要说明的是,在SQLite2中,如果多个进程正在从数据库中读取数据,也就是说该数据库始终都有读操作发生,即在每一时刻该数据库都持有至少一把共享锁,这样将会导致没有任何进程可以执行写操作,因为在数据库持有读锁的时候是无法获取写锁的,我们将这种情形称为"写饥饿"。在SQLite3中,通过使用PENDING锁则有效的避免了"写饥饿"情形的发生。当某一进程持有PENDING锁时,已经存在的读操作可以继续进行,直到其正常结束,但是新的读操作将不会再被SQLite接受,所以在已有的读操作全部结束后,持有PENDING锁的进程就可以被激活并试图进一步获取排他锁以完成数据的修改操作。
   
五、SQL级别的事务控制:

SQLite3在实现上确实针对锁和并发控制做出了一些精巧的变化,特别是对于事务这一SQL语言级别的特征。在缺省情况下,SQLite3会将所有的SQL操作置于antocommit模式下,这样所有针对数据库的修改操作都会在SQL命令执行结束后被自动提交。在SQLite中,SQL命令"BEGIN TRANSACTION"用于显式的声明一个事务,即其后的SQL语句在执行后都不会自动提交,而是需要等到SQL命令"COMMIT"或"ROLLBACK"被执行时,才考虑提交还是回滚。由此可以推断出,在BEGIN命令被执行后并没有立即获得任何类型的锁,而是在执行第一个SELECT语句时才得到一个共享锁,或者是在执行第一个DML语句时才获得一个保留锁。至于排它锁,只有在数据从内存写入磁盘时开始,直到事务提交或回滚之前才能持有排它锁。
    如果多个SQL命令在同一个时刻同一个数据库连接中被执行,autocommit将会被延迟执行,直到最后一个命令完成。比如,如果一个SELECT语句正在被执行,在这个命令执行期间,需要返回所有检索出来的行记录,如果此时处理结果集的线程因为业务逻辑的需要被暂时挂起并处于等待状态,而其它的线程此时或许正在该连接上对该数据库执行INSERT、UPDATE或DELETE命令,那么所有这些命令作出的数据修改都必须等到SELECT检索结束后才能被提交。

这是该系列中最后一篇关于SQLite理论与应用方面的博客了,后面将发布两篇关于如何利用SQLite编程的博客,其中还将包含四个典型的应用代码示例,望大家继续保持关注。

时间: 2015-05-01

SQLite教程(十四):C语言编程实例代码(2)

三.高效的批量数据插入: 在给出操作步骤之前先简单说明一下批量插入的概念,以帮助大家阅读其后的示例代码.事实上,批量插入并不是什么新的概念,在其它关系型数据库的C接口API中都提供了一定的支持,只是接口的实现方式不同而已.纵观众多流行的数据库接口,如OCI(Oracle API).MySQL API和PostgreSQL API等,OCI提供的编程接口最为方便,实现方式也最为高效.SQLite作为一种简单灵活的嵌入式数据库也同样提供了该功能,但是实现方式并不像其他数据库那样方便明显,它只是通过一

SQLite之Autoincrement关键字(自动递增)

SQLite 的 AUTOINCREMENT 是一个关键字,用于表中的字段值自动递增.我们可以在创建表时在特定的列名称上使用 AUTOINCREMENT 关键字实现该字段值的自动增加. 注意点:整型字段可以使用关键字AUTOINCREMENT. 语法 AUTOINCREMENT关键字的基本用法如下: CREATE TABLE table_name( column1 INTEGER AUTOINCREMENT, column2 datatype, column3 datatype, ..... c

python查询sqlite数据表的方法

本文实例讲述了python查询sqlite数据表的方法.分享给大家供大家参考.具体实现方法如下: import sqlite3 as db conn = db.connect('mytest.db') conn.row_factory = db.Row cursor = conn.cursor() cursor.execute("select * from person") rows = cursor.fetchall() for row in rows: print("%s

Sqlite数据库里插入数据的条数上限是500

今天在向Sqlite数据库里插入数据的时候,报了这样一个错: 复制代码 代码如下: "too many terms in compound SELECT" 去Stackoverflow上查了一下,发现有人回答这个问题:链接 原来一次性向数据库里插入数据的条数不能太多,上限是500条.超出会报错. 解决方案就是只好分多次插入数据库了.

SQLite教程(八):命令行工具介绍

工欲善其事,必先利其器.学好SQLite的命令行工具,对于我们学习SQLite本身而言是非常非常有帮助的.最基本的一条就是,它让我们学习SQLite的过程更加轻松愉快.言归正传吧,在SQLite的官方下载网站,提供了支持多个平台的命令行工具,使用该工具我们可以完成大多数常用的SQLite操作,就像sqlplus之于Oracle.以下列表给出了该工具的内置命令: 命令名 命令说明 .help 列出所有内置命令. .backup DBNAME FILE 备份指定的数据库到指定的文件,缺省为当前连接的

使用Python编写类UNIX系统的命令行工具的教程

引言 您是否能编写命令行工具?也许您可以,但您能编写出真正好用的命令行工具吗?本文讨论使用 Python 来创建一个强健的命令行工具,并带有内置的帮助菜单.错误处理和选项处理.由于一些奇怪的原因,很多人并不了解 Python? 的标准库具有制作功能极其强大的 *NIX 命令行工具所需的全部工具. 可以这样说,Python 是制作 *NIX 命令行工具的最佳语言,因为它依照"batteries-included"的哲学方式工作,并且强调提供可读性高的代码.但仅作为提醒,当您发现使用 Py

使用node打造自己的命令行工具方法教程

一.实现一个简单的功能 二.环境 1.系统: window 10 2.编辑器: vscode 3.node版本: 8.7.0 三.开始玩 1.打开命令行,新建一个pa'ckage.json npm init 这时看到一个新的package.json生成了,使用编辑器打开 2.修改package.json,新增一个bin属性 { "name": "my-cli", "version": "1.0.0", "descri

详解用Node.js写一个简单的命令行工具

本文介绍了用Node.js写一个简单的命令行工具,分享给大家,具体如下: 操作系统需要为Linux 1. 目标 在命令行输入自己写的命令,完成目标任务 命令行要求全局有效 命令行要求可以删除 命令行作用,生成一个文件,显示当前的日期 2. 代码部分 新建一个文件,命名为sherryFile 文件sherryFile的内容 介绍: 生成一个文件,文件内容为当前日期和创建者 #! /usr/bin/env node console.log('command start'); const fs = r

Linux 命令行工具解析和格式化输出 JSON的方法

JSON 是一种轻量级且与语言无关的数据存储格式,易于与大多数编程语言集成,也易于人类理解 -- 当然,如果格式正确的话.JSON 这个词代表 J ava S cript O bject N otation,虽然它以 JavaScript 开头,而且主要用于在服务器和浏览器之间交换数据,但现在正在用于许多领域,包括嵌入式系统.在这里,我们将使用 Linux 上的命令行工具解析并格式化打印 JSON.它对于在 shell 脚本中处理大型 JSON 数据或在 shell 脚本中处理 JSON 数据非

在vbs运行命令行工具后让命令窗口保持打开状态的脚本

问: 您好,脚本专家!如何在运行像 Ping 或 Ipconfig 这样的工具后让命令窗口保持打开状态? -- DB 答: 您好,DB.这个问题让我们想起了往事.有一个脚本专家刚来 Microsoft,那时许多人认为 WMI 和 ADSI 对于脚本编写者来说太难使用.因此,人们建议这个脚本专家不使用 WMI 或 ADSI,而是干脆使用 VBScript 作为调用命令行工具的方法.事实上,这个脚本专家编写的第一章就是一个关于事件日志管理的章节,该章后来成为 Microsoft Windows 20

使用命令行工具npm新创建一个vue项目的方法

Vue.js 提供一个官方命令行工具,可用于快速搭建大型单页应用.该工具提供开箱即用的构建工具配置,带来现代化的前端开发流程. 只需几分钟即可创建并启动一个带热重载.保存时静态检查以及可用于生产环境的构建配置的项目: # 全局安装 vue-cli $ npm install --global vue-cli # 创建一个基于 webpack 模板的新项目 $ vue init webpack my-project $ vue init webpack test //输入命令 ? Project

Nodejs 发布自己的npm包并制作成命令行工具的实例讲解

<span style="font-family:Arial, Helvetica, sans-serif;background-color:rgb(255,255,255);">近日当我在使用npm上已经存在的一个包时,发现它有bug:于是决定自己实现这个功能,自己写一个npm包.</span> 下面我记录一下自己的实现过程. 1. npm init 选择一个文件夹,然后用命令行cd进去,然后执行npm init,这时会生成一长串表单,根据自己的实际情况填写内

node命令行工具之实现项目工程自动初始化的标准流程

一.目的 传统的前端项目初始流程一般是这样: 可以看出,传统的初始化步骤,花费的时间并不少.而且,人工操作的情况下,总有改漏的情况出现.这个缺点有时很致命. 甚至有马大哈,没有更新项目仓库地址,导致提交代码到旧仓库,这就很尴尬了... 基于这些情况,编写命令行工具(CLI)的目的就很明确: 用于新项目工程的初始化利用工具进行初始化,可以节省项目初期的准备时间避免出现改漏的情况杜绝未更新项目版本仓库地址的问题 以下是新的流程示意图: 二.自动化流程分析 以下是自动化流程图: 从流程图可以得出两个重

使用sqlplus命令行工具为oracle创建用户和表空间

用Oracle10g自带的企业管理器或PL/SQL图形化的方法创建表空间和用户以及分配权限是相对比较简单的,本文要介绍的是另一种方法,使用Oracle 9i所带的命令行工具:SQLPLUS 来创建表空间,这个方法用起来更加简明快捷. 假设: 文章假设,如果您用的是Linux系统,那么Oracle用户名为oracle.同时,您是在oracle服务器上操作. 如果是在Windows系统下, 请先点击"开始",然后点"运行",输入cmd并点击"确定",