1.锁的状态

大多数情况下,锁的生命周期在事务的生命周期之中。它们不一定同时开始,但总时同时结 束。当你结束一个事务时,也会释放它相关的锁。或者说,锁直到事务结束或系统崩溃时才 会释放。如果系统在事务没有结束的情况下崩溃,那么下一个访问数据库的连接会处理这种情况。

在SQLite中有5种不同的锁状态,连接(connection)任何时候都处于其中的一个状态。下图显示了锁的状态以及状态的转换。

(1)一个事务可以在 UNLOCKED、RESERVED 或 EXCLUSIVE 三种状态下开始。默认情况下在 UNLOCKED 时开始。

(2)白色框中的 UNLOCKED、PENDING、SHARED 和 RESERVED 可以在一个数据库的同一时刻存在。

(3) 从 灰 色的 PENDING 开 始 ,事情就变 得严格 起 来, 意 味着 事务想 得 到排它锁(EXCLUSIVE)(注意与白色框中的区别)。虽然锁有这么多状态,但是从体质上来说,只有两种情况:读事务和写事务

2.读事务

SELECT 语句执行时锁的状态变化过程,非常简单:一个连接执行 SELECT语句,触发一个事务,从UNLOCKED到SHARED,当事务COMMIT时,又回到UNLOCKED,就这么简单。

db = open('foods.db')
db.exec('BEGIN')
db.exec('SELECT * FROM episodes')
db.exec('SELECT * FROM episodes')
db.exec('COMMIT')
db.close()

两个 SELECT 命令在一个事务下执行。第一个 exec()执行时,连接处于 SHARED,然后第二个 exec()执行。当事务提交时,连接又从 SHARED回到 UNLOCKED 状态,状态变化如下: UNLOCKED→PENDING→SHARED→UNLOCKED 如果没有 BEGIN 和 COMMIT 两行,两个 SELECT 都运行于自动提交状态,状态变化如下: UNLOCKED→PENDING→SHARED→UNLOCKED→PENDING→SHARED→UNLOCKED仅仅是读数据,但在自动提交模式下,却会经历两个加解锁的循环,太麻烦。而且,一个写进程可能插到两个 SELECT 中间对数据库进行修改,这样,你就不能保证第二次能够读到同样的数据了,而使用 BEGIN..COMMIT 就可以有此保证。

3.写事务

和读事务一样,它也会经历 UNLOCKED→PENDING→SHARED 的变化过程,但接下来就会看到 PENDING 锁是如何起到关口作用的了。

保留(RESERVED)状态

当一个连接(connection)要向数据库写数据时,从 SHARED 状态变为 RESERVED 状态。如果它得到 RESERVED 锁,也就意味着它已经准备好进行写操作了。即使它没有把修改写入数据库,也可以把修改保存到位于 pager 的缓冲区中(page cache)了。

当一个连接进入 RESERVED 状态,pager 就开始初始化回卷日志(rollback journal)。回卷日志是一个文件,用于回卷和崩溃恢复。在 RESERVED 状态下,pager 管理着三种页:

(1)已修改的页:包含被 B-tree 修改的记录,位于 page cache 中。

(2)未修改的页:包含没有被 B-tree 修改的记录。

(3)日志页:这是修改页以前的版本,日志页并不存储在 page cache 中,而是在 B-tree 修改页之前写入日志。

Page cache非常重要,正是因为它的存在,一个处于 RESERVED 状态的连接可以真正的开始工作,而不会干扰其它的(读)连接。所以,SQLite 可以高效地处理在同一时刻的多个读连接和一个写连接。

未决(PENDING)状态

当一个连接完成修改,需要真正开始提交事务时,执行该过程的 pager 进入 EXCLUSIVE 状态。从 RESERVED 状态开始,pager 试着获取 PENDING 锁,一旦得到,就独占它,不允许任何其它连接获得 PENDING 锁。既然写操作持有 PENDING 锁,其它任何连接都不能从 UNLOCKED状态进入 SHARED 状态,即不会再有新的读进程,也不会再有新的写进程。只有那些已经处于SHARED 状态的连接可以继续工作。而处于 PENDING 状态的写进程会一直等到所有这些连接释放它们的锁,然后对数据库加 EXCUSIVE 锁,进入 EXCLUSIVE 状态,独占数据库。

排它状态

在EXCLUSIVE状态下,主要的工作是把修改的页从page cache写入数据库文件,这是真正进行写操作的地方。在pager将修改页写到文件之前,还必须先处理日志。它检查是否所有的日志都写入了磁盘, 因为它们可能还位于操作系统的缓冲区中。所以 pager 得告诉 OS 把所有的文件写入磁盘,这与 synchronous pragma 所做的工作相同。

日志是数据库进行恢复的惟一方法,所以日志对于 DBMS 非常重要。如果日志页没有完全写入磁盘而发生崩溃,数据库就不能恢复到它原来的状态,此时数据库就处于不一致状态。日志写盘完成后,pager 就把所有的修改页写入数据库文件。接下来做什么取决于事务提交的模式,如果是自动提交,那么 pager 清理日志、page cache,然后由 EXCLUSIVE 进入UNLOCKED。如果是手动提交,那么 pager 继续持有 EXCLUSIVE 锁和回卷日志,直至遇到 COMMIT 或者 ROLLBACK。总之,出于性能方面的考虑,进程占有排它锁的时间应该尽可能的短,所以 DBMS 通常都是在真正写文件时才会占有排它锁,这样能大大提高并发性能。

自动提交与效率

调整页缓冲区

回到前面的例子,事务从 BEGIN 开始,跟着 UPDATE。如果在写盘之前,修改操作将缓冲区用完了(也就是说修改操作需要比预设的更多的缓冲区),这时会发生什么呢?

转换为排它

真正的问题是:到底在哪个(精确的)时刻,到底为什么,pager 从 RESERVED 转换为EXCLUSIVE。这会发生在两种情况下:当连接到达提交点主动进入排它状态;或页缓冲区已满不得不进入排它状态。

前面我们仅看到了第 1 种情况,那么,在第 2 种情况下会发生什么呢?此时 pager 已不能再存储更多的已修改页,也就不能再做任何修改操作。它必须转换为排它状态,以使工作能够继续进行。实际上也不完全是这样,实际上有软限制和硬限制的区别。

调整页缓冲区的大小

如何决定需要多大的缓冲区尺寸呢?这由你想做什么而定。假设你想修改 episodes 表的所有记录,那么该表的所有页都会被修改,因此,你就可以计算出 episodes 表总共需要多少个页并对缓冲区做出调整。可以用 sqlite_analyzer 到得所有关于 episodes 表的需要的信息。对每一个表,它都可以做出完备的统计,包括总页数。因为默认的缓冲区大小是 2000个页,所以你没有必要担心。在episodes表中有400条记录,也就是说每页可存放约100条记录。所以,在修改所有记录之前你不需要考虑调整页缓冲区,除非episodes中至少有了196000条记录。还要记住,你只需要在有其它连接并发使用数据库的情况下才需要考虑这些,如果只有你自己使用数据库,这些就都不需要考虑了。

等待加锁

我们前面谈到过 pager 等待从 PENDING 状态进入 EXCLUSIVE 状态,那么在这个期间到底发生了什么呢?首先,任何 exec()或 step()的调用都可能进入等待。当 SQLite 遇到不能获得锁的情况时,它的默认表现总是向函数返回一个 SQLITE_BUSY 并使函数继续寻求锁。无论你执行什么命令,都有可能遇到 SQLITE_BUSY,包括 SELECT 命令,都有可能因为有其它的写进程处于未决状态而遇到 SQLITE_BUSY。当遇到 SQLITE_BUSY 时,最简单的选择是重试。但是,下面我们就会看到这并不一定是最好的选择。

使用“忙”句柄

你可以使用一个忙句柄,而不是一遍遍地重试。忙句柄是一个函数,你创建它用来消磨时间或做其它任何事情——如给岳母发一封邮件(?)。它仅在SQLite不能获得锁时被调用。忙句柄必须做的唯一的事是返回一个值,告诉 SQLite 下一步该做什么。如果它返回 TRUE,SQLite将会继续尝试获得锁;如果它返回 FALSE,SQLite 将向申请锁的函数返回 SQLITE_BUSY。看下面的例子:

counter = 1
def busy()
counter = counter + 1
if counter == 2
return 0
end
spam_mother_in_law(100)
return 1
end
db.busy_handler(busy)
stmt = db.prepare('SELECT * FROM episodes;')
stmt.step()
stmt.finalize()

spam_mother_in_law()完成一个发邮件功能。

step()函数必须获得一个 SHARED 锁以完成 SELECT 操作。如果此时有一个写进程活动,正常情况下 step()会返回 SQLITE_BUSY。但是,在上面程序中却不是这样,而是由 pager 调用 busy()函数,因为它已经被注册队忙句柄。busy()函数增加计数,给你岳母发一封邮件,

并且返回 1,在 pager 中会被翻译成 true——继续申请锁。Pager 再次申请获得 SHARED 锁,但数据库仍然被锁着,于是 pager 再次调用 busy()函数。只有此时,busy()函数返回 0,在pager 中会被翻译成 false——返回 SQLITE_BUSY。

使用正确的事务

编码

现在,你对API、事务和锁已经有了很好的了解了。最后,我们把这 3 个内容在代码中结合到一起。

使用多个连接

如果你曾经为其它的关系型数据库编写过程序,你就会发现有些适用于那些数据库的方法不一定适用于 SQLite。使用其它数据库时,经常会在同一个代码块中打开多个连接,典型的例子就是在一个连接中返复遍历一个表而在另一个连接中修改它的记录。

在 SQLite 中,在同一个代码块中使用多个连接会引起问题,必须小心地对待这种情况。请看下面代码:

c1 = open('foods.db')
c2 = open('foods.db')
stmt = c1.prepare('SELECT * FROM episodes')
while stmt.step()
print stmt.column('name')
c2.exec('UPDATE episodes SET …)
end
stmt.finalize()
c1.close()
c2.close()

问题很明显,当c2试图执行UPDATE时,c1拥有一个SHARED锁,这个锁只有等stmt.finalize() 之后才会释放。所以,是不可能成功写数据库的。最好的办法是在一个连接中完成工作,并且在同一个 BEGIN IMMEDIATE事务中完成。新程序如下:

c1 = open('foods.db')
# Keep trying until we get it
while c1.exec('BEGIN IMMEDIATE') != SQLITE_OK
end
stmt = c1.prepare('SELECT * FROM episodes')
while stmt.step()
print stmt.column('name')
c1.exec('UPDATE episodes SET …)
end 61
stmt.finalize()
c1.exec('COMMIT')
c1.close()

在这种情况下,你应该在单独的连接中使用语句(statement)来完成读和写,这样,你就不必担心数据库锁会引发问题了。但是,这个特别的示例仍然不能工作。如果你在一个语句(statement)中返复遍历一个表而在另一个语句中修改它的记录,还有一个附加的锁问题。

表锁

即使只使用一个连接,在有些边界情况下也会出现问题。不要认为一个连接中的两个语句(statements)就能协调工作,至少有一个重要的例外。当在一个表上执行了 SELECT 命令,语句对象会在表上创建一个 B-tree 游标。如果表上有一个活动的 B-tree 游标,即使是本连接中的其它语句也不能够再修改这个表。如果做这种尝试,将会得到 SQLITE_BUSY。

看下面的例子:

c = sqlite.open("foods.db")
stmt1 = c.compile('SELECT * FROM episodes LIMIT 10')
while stmt1.step() do
# Try to update the row
row = stm1.row()
stmt2 = c.compile('UPDATE episodes SET …')
# Uh oh: ain't gonna happen
stmt2.step()
end
stmt1.finalize()
stmt2.finalize ()
c.close()

这里我们只使用了一个连接。但当调用stmt2.step()则不会工作,因为stmt1拥有episodes表的一个游标。在这种情况下,stmt2.step()有可能成功地将锁升级到 EXCLUSIVE,但仍会返回 SQLITE_BUSY,因为 episodes 的游标会阻止它修改表。完成这种操作有两种方法:

l 遍历一个语句的结果集,在内存中保存需要的信息。定案这个读语句,然后执行修改操作。

l 将 SELECT 的结果存到一个临时表中并用读游标打开它。这时同时有一个读语句和一个写语句,但它们在不同的表上,所以不会影响主表上的写操作。写完成后,删掉临时表就是了。当表上打开了一个语句,它的 B-tree 游标在两种情况下会被移除:

l 到达了语句结果集的尾部。这时 step()会自动地关闭语句的游标。从 VDBE 的角度,当到达结果集的尾部时,CDBE 遇到 Close 命令,这将导致所有相关游标的关闭。

l 程序显式地调用了 finalize(),所有相关游标将关闭。在很多编程语言扩展中,statement 对象的 close()函数会自动调用 sqlite3_finalize()。

有趣的临时表

临时表使你可以做到不违反规则。如果你确实需要在一个代码块中使用两个连接,或者使用两个语句(statement)操作同一个表,你可以安全地在临时表上如此做。当一个连接创建了一个临时表,不需要得到 RESERVED 锁,因为临时表存在于数据库文件之外。有两种方法可以做到这一点,看你想如何管理并发。

请看如下代码:

c1 = open('foods.db')
c2 = open('foods.db')
c2.exec('CREATE TEMPORARY TABLE temp_epsidodes as SELECT * from episodes')
stmt = c1.prepare('SELECT * FROM episodes')
while stmt.step()
print stmt.column('name')
c2.exec('UPDATE temp_episodes SET …')
end
stmt.finalize()
c2.exec('BEGIN IMMEDIATE')
c2.exec('DELETE FROM episodes')
c2.exec('INSERT INTO episodes SELECT * FROM temp_episodes')
c2.exec('COMMIT')
c1.close()
c2.close()

上面的例子可以完成功能,但不好。episodes 表中的数据要全部删除并重建,这将丢失episodes 表中的所有完整性约束和索引。下面的方法比较好:

c1 = open('foods.db')
c2 = open('foods.db')
c1.exec('CREATE TEMPORARY TABLE temp_episodes as SELECT * from episodes')
stmt = c1.prepare('SELECT * FROM temp_episodes')
while stmt.step()
print stmt.column('name')
c2.exec('UPDATE episodes SET …') # What about SQLITE_BUSY?
end
stmt.finalize()
c1.exec('DROP TABLE temp_episodes')
c1.close()
c2.close()

定案的重要性

使用SELECT语句必须要意识到,其SHARED锁(大多数时候)直到finalize()被调用后才会释放。请看下面代码:

stmt = c1.prepare('SELECT * FROM episodes')
while stmt.step()
print stmt.column('name')
end
c2.exec('BEGIN IMMEDIATE; UPDATE episodes SET …; COMMIT;')
stmt.finalize()

如果你用 C API 写了与上例等价的程序,它实际上是能够工作的。尽管没有调用 finalize(), 但第二个连接仍然能够修改数据库。在告诉你为什么之前,先来看第二个例子:

c1 = open('foods.db')
c2 = open('foods.db')
stmt = c1.prepare('SELECT * FROM episodes')
stmt.step()
stmt.step()
stmt.step()
c2.exec('BEGIN IMMEDIATE; UPDATE episodes SET …; COMMIT;')
stmt.finalize()

假设episodes 中有 100 条记录,程序仅仅访问了其中的 3 条,这时会发生什么情况呢?第2个连接会得到 SQLITE_BUSY。

在第1个例子中,当到达语句结果集尾部时,会释放 SHARED 锁,尽管还没有调用 finalize()。

在第2个例子中,没有到达语句结果集尾部,SHARED 锁没有释放。所以,c2 不能执行 UPDATE 操作。

这个故事的中心思想是:不要这么做,尽管有时这么做是可以的。在用另一个连接进行写操

作之前,永远要先调用 finalize()。

共享缓冲区模式

SQLite提供一种可选的并发模式,称为共享缓冲区模式,它允许在单一的线程中操作多个连接。在共享缓冲区模式中,一个线程可以创建多个连接来共享相同的页缓冲区。进而,这组连接可以有多个“读”和一个“写”同时工作于相同的数据库。缓冲区不能在线程间共享,它被严格地限制在创建它的线程中。因此,“读”和“写”就需要准备处理与表锁有关的一些特殊情况。

当 readers 读表时, SQLite 自动在这些表上加锁,writer就不能再改这些表了。如果writer试图修改一个有读锁的表,会得到SQLITE_LOCKED。如果readers运行在read-uncommitted 模式(通过read_uncommitted pragma 来设置),则当 readers 读表时,writer也可以写表。在这种情况下,SQLite不为readers所读的表加读锁,结果就是 readers 和 writer 互不干扰。也因此,当一个 writer 修改表时,这些 readers 可能得到不一致的结果。

Sqlite锁与事务相关推荐

  1. SQL SERVER的锁机制(三)——概述(锁与事务隔离级别)

    五.锁与事务隔离级别 事务隔离级别简单的说,就是当激活事务时,控制事务内因SQL语句产生的锁定需要保留多入,影响范围多大,以防止多人访问时,在事务内发生数据查询的错误.设置事务隔离级别将影响整条连接. ...

  2. 3天,我把MySQL索引、锁、事务、分库分表撸干净了!

    最近项目增加,缺人手,面试不少,但匹配的人少的可怜.跟其他组的面试官聊,他也抱怨了一番,说候选人有点儿花拳绣腿,回答问题不落地,拿面试最常问的MySQL来说,并不只是懂"增删改查" ...

  3. 一文读懂MySQL事务锁、事务级别

    锁 性能分:乐观(比如使用version字段比对,无需等待).悲观(需要等待其他事务) 乐观锁,如它的名字那样,总是认为别人不会去修改,只有在提交更新的时候去检查数据的状态.通常是给数据增加一个字段来 ...

  4. 3天,把MySQL索引、锁、事务、分库分表撸干净了!

    最近项目增加,缺人手,面试不少,但匹配的人少的可怜.跟其他组的面试官聊,他也抱怨了一番,说候选人有点儿花拳绣腿,回答问题不落地,拿面试最常问的MySQL来说,并不只是懂"增删改查" ...

  5. plsql tables 没有表_InnoDB 层锁、事务、统计信息字典表 | 全方位认识 information_schema...

    在上一篇<InnoDB 层系统字典表|全方位认识 information_schema>中,我们详细介绍了InnoDB层的系统字典表,本期我们将为大家带来系列第六篇<InnoDB 层 ...

  6. 深入理解 MySQL ——锁、事务与并发控制

    本文对 MySQL 数据库中有关锁.事务及并发控制的知识及其原理做了系统化的介绍和总结,希望帮助读者能更加深刻地理解 MySQL 中的锁和事务,从而在业务系统开发过程中可以更好地优化与数据库的交互. ...

  7. MySQL之锁、事务、优化、OLAP、OLTP

    本节目录 一 锁的分类及特性 二 表级锁定(MyISAM举例) 三 行级锁定 四 查看死锁.解除锁 五 事务 六 慢日志.执行计划.sql优化 七 OLTP与OLAP的介绍和对比 八 关于autoco ...

  8. mysql之锁与事务

    相关博文推荐: mysql之高性能索引 Mysql之锁与事务 平时的业务中,顶多也就是写写简单的sql,连事务都用的少,对锁这一块的了解就更加欠缺了,之前一个大神分享了下mysql的事务隔离级别,感觉 ...

  9. 深入理解 MySQL—锁、事务与并发控制

    本文转载自"vivo 互联网技术",已获授权. 本文对 MySQL 数据库中有关锁.事务及并发控制的知识及其原理做了系统化的介绍和总结,希望帮助读者能更加深刻地理解 MySQL 中 ...

  10. sql server 锁与事务拨云见日(下)

    sql server 锁与事务拨云见日(下) 原文:sql server 锁与事务拨云见日(下) 在锁与事务系列里已经写完了上篇中篇,这次写完下篇.这个系列俺自认为是有条不紊的进行,但感觉锁与事务还是 ...

最新文章

  1. 大家都能读懂的IT生活枕边书
  2. 时间一天一天过去,很快;时间如果过的慢,更是没有意思
  3. 用 for/in 在 Java 5.0 中增强循环
  4. python3.7.2安装-CentOS 7中Python3.7.2的安装
  5. 一些基本的ABAP技巧
  6. 02-线性结构2 一元多项式的乘法与加法运算 (20 分)
  7. js 多个定时器_Node.js系列深入浅出Node模块化开发——CommonJS规范
  8. NLog 2.0.0.2000 使用实例
  9. 使用java7的try-resource-with语法用httpclient抓取网页并用jsoup获取网页对象
  10. cs231n-svm作业
  11. python3 模块 public缀_Python publicsuffixlist包_程序模块 - PyPI - Python中文网
  12. deck.gl 调研
  13. android 仿QQ音乐歌单效果
  14. 最近看的电影综艺推荐
  15. 二进制数的补码及运算
  16. python实验二序列_Python合集之Python序列(二)
  17. SQL Server 让你的数据来去自如——批处理
  18. (仿超级课程表)结合MaterialSheetFab实现简单的课程表功能
  19. 我对SNS游戏的初步理解
  20. falcon - 清除数据

热门文章

  1. CentOS7.6重装系统步骤
  2. opencv 叠加文字_利用opencv为视频添加动态字幕
  3. app推送怎么实现更好的效果?
  4. 计算机网络体系结构——各层的功能
  5. Win7重建100M BCD系统保留分区
  6. C# VB .NET生成条形码,支持多种格式类型
  7. 第一阶段✦第一章☞信息化知识
  8. 信息系统项目管理师必背核心考点(四十四)规划风险应对
  9. A6 词根:-vict- = -vinc-;单词:evince、vincible
  10. 步步为营 SharePoint 开发学习笔记系列 二、安装和配置