`

SQLite中的锁

阅读更多

 

  在SQLite中,锁是一个重要而又基本的概念。

  与锁密切相关的一个基本概念是事务。首先,为了更好的理解锁的概念,我们简单回顾一下什么是事务?

  事务定义了一组SQL命令的边界,这组命令或者作为一个整体被全部执行,或者都不执行,这被称为数据库完整性的原子性原则,关于事务的典型例子就是银行转帐。

  事务由3个命令控制:begin、commit和rollback。begin之后和commit之前的所有操作都可以被取消。commit命令提交所执行的所有操作。与之类似,rollback还原begin之后的所有操作。

  那么在数据库中,事务和锁在查询处理中密切相关。查询总是在事务内执行,事务又涉及到锁,所以如果锁控制不当,会产生很多问题。接下来,让我们谈谈SQLite中的锁。

  SQLite采用粗粒度的锁。当一个连接要写数据库时,所有其他的连接被锁住,直到写连接结束它的事务。SQLite有一个加锁表,用来帮助不同的写数据库都能够在最后一刻再加锁,以保证最大的并发性。

  SQLite有5种不同的锁状态:未锁定(unlocked)、共享(shared)、保留(reserved)、待定(pending)和排它(exclusive)。每个数据库连接在同一时刻只能处于其中的一个状态(见下图),每种状态(未锁定(unlocked)状态除外)都有一种锁与之对应。

  图中所示,所有的事务都是从未锁定(unlocked)、保留(reserved)或排他锁开始的。默认情况下,一切都从未锁定(unlocked)开始。白色的锁状态----未锁定(unlocked)、待定(pending)、共享(shared)和保留(reserved),全部都可以在同一时间同一数据库的不同连接种存在。不过,从灰色的待定锁开始,限制就多了。灰色的待定状态代表锁正在被某个连接拥有,即某个想要获取独占锁的写操作。与此对应,白色的待定状态表示连接获取和释放共享锁的途径。尽管有这些不同的锁状态,但是所有的SQLite事务都可以归结为两种类型之一:读事务和写事务。这也是图种描绘的最终内容:读操作、写操作以及它们如何在一起工作。

  下面,我们就从读事务和写事务两个方面讨论。

  • 读事务

      从select语句的锁进程开始,它的路径比较简单。执行select语句的连接启动事务,从未锁定转到共享锁,提交之后回到未锁定状态,操作结束。

  这里我们提出两个疑问:

  1. 执行两个语句会发生什么?

  2. 它们的锁路径是什么?

  这两个疑问取决于是否运行在自动提交模式下。

  考虑如下实例:

db = open('worlds.db')
db.exec('begin')
db.exec('select * from sometables')
db.exec('select * from sometables')
db.exec('commit')
db.close()

  这里有明确的begin命令开始,两个select命令在一个事务种执行,因此它们在同一个共享状态下执行。第一个exec()运行,让连接进入共享状态;然后第二个exec()运行;最后,手动提交命令,让连接从共享状态回到未锁定状态。代码的锁路径如下所示:

UNLOCKED->PENDING->SHARED->UNLOCKED

  现在考虑没有begin和commit命令的情况。两个select命令运行在自动提交模式下。因此,它们各自经历完整的路径。现在,代码的锁路径如下所示:

UNLOCKED->PENDING->SHARED->UNLOCKED->PENDING->SHARED->UNLOCKED

  由于该代码只是读取数据,因此,可能不会产生多大差异,但在自动提交模式下的确会两次锁定文件,而不是其他方式中那样只锁定一次。这样的做法,你可能会发现,可以在两个select exec()调用之间插入修改数据库的操作,因此,这样做无法确保两个命令返回相同的结果。相反,如果有begin...commit命令,可以保证它们的结果完全相同。

  • 写事务

      下面说说数据库写操作,例如update语句。首先,连接必须遵从与select相同的路径,先到共享状态。所有的操作----写操作或读操作----都必须经历;未知锁->待定锁->共享锁。

     保留状态(reserved)

      连接尝试向数据库写入内容时,必须从共享锁转换到保留锁。如果它获得保留锁,则准备好开始进行数据修改。即使连接真的不能在此时修改数据库,它也可以将修改内容存储在本地pager内的内存缓存中,也就是上一篇中提到的页面缓存。

      当连接进入保留状态时,pager初始化回滚日志。回滚日志是一个文件,用于回滚和故障恢复。具体地说,它拥有将数据库还原到事务开始之前的原来状体啊的数据库页。当B-tree修改页时,pager将这些数据库页都存放到日志文件。比如说,对于update修改的每条记录,页面获取与原始记录相关的数据库页,并将它们复制到日志中。日志就拥有事务开始之前的一些数据库内容。因此,要撤销事务时,pager只是简单地将日志文件中的内容复制回数据库中。这样,数据库就还原到事务开始前的状态。

      保留状态下,pager实际上管理三种页:已修改页、未修改页和日志页。已修改页是包含B-tree已改变记录的页,这些页存储在页缓存中。未修改页是B-tree读取但并未改变的页,它们是诸如select命令之类的结果。最后是日志页,它就是已修改页的原始版本。日志页不会存储在页面缓存中,但B-tree修改前会将其写入日志。

      因为页面缓存,写操作连接的确可以在保留状态完成实际的工作,而不用干扰其他(读操作)连接。因此,SQLite可以有效地让多个读操作和一个写操作同一时间在同一数据库中工作。唯一需要注意的是,写操作连接要将所做的修改存储在页面缓存中,而不是数据库文件中。此外要注意,给定数据库同一时间只能有一个保留或独占连接----但是多个读操作可以和一个写操作并存。

      待定状态(pending)

      当连接完成update操作,并提交事务时,pager开始进入独占状态的过程。一旦获得待定锁,并继续持有该锁,阻止其他连接获取待定锁。这里的待定锁也被称为网关锁,因为写操作继续持有待定锁,其他连接无法从未锁定转换到共享状态,结果是没有可以进入数据库的新连接:没有新的读操作,没有新的写操作。挂起状态实际上是损耗阶段。写操作保证它可以排队等待数据库----只要每个人都守规矩且行为得当,最终都可以获得独占锁。只有其他已有共享锁的连接可以继续正常工作。待定状态下,写操作等待这些连接完成并释放其共享锁。

      独占状态(exclusive)

      独占状态中,主要工作是将修改的页从页面缓存刷新到数据库文件。这时要慎重,因为pager开始实际修改数据库了。

      在pager开始写入修改的页前,首先要处理日志。它会检查日志的完整内容是否已写入磁盘。这种情况下,依然很可能,即使pager已将页写入日志文件,但是操作系统具有很多缓存,因此,可能还有一部分内容在操作系统的缓存中(也许不是全部)。

      将日志提交到磁盘非常重要,因为如果程序或系统在pager写入数据库文件时崩溃,日志是日后还原数据库文件的唯一方式。如果在系统崩溃前,日志页没有完整地写入磁盘,那么数据库就无法还原到其原来的状态,因为内存中的日志页在系统崩溃时丢失了。这种情况下,如果运气好的话,还有一个处于不一致状态的数据库,如果不走运的话,也许数据库也损坏了。

      一旦处理完日志,pager就可以将所有已修改的页复制到数据库文件。下一步做什么取决于事务模式。比如此处说的情况,事务自动提交,然后pager清理日志,清除页缓存,从独占锁回到未锁定状态。如果该事务未提交,pager继续持有独占锁,日志继续发挥作用,直到发出COMMIT或者ROLLBACK命令。

  至此,以读事务写事务为两大方面,对锁的5种状态之间的转换以及所起到的作用,分别做了举例和总结。对于SQLite中5种锁的状态的转换,仅仅是需要了解的基本概念,相关的知识还有很多,比如自动提交模式下对于效率有什么影响?页面缓存的大小对于保留锁状态过渡到独占锁状态的影响?等等还有很多其他方面需要了解,这里只是做了基本介绍。

分享到:
评论

相关推荐

    C#解决SQlite并发异常问题的方法(使用读写锁)

    SQLite是文件级别的数据库,其锁也是文件级别的:多个线程可以同时读,但是同时只能有一个线程写。Android提供了SqliteOpenHelper类,加入Java的锁机制以便调用。但在C#中未提供类似功能。 作者利用读写锁...

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

    主要介绍了SQLite教程(十二):锁和并发控制详解,本文讲解了锁和并发控制机制概述、文件锁、回滚日志、数据写入、SQL级别的事务控制等内容,需要的朋友可以参考下

    SQLite3使用详解.rar

    数据库中的一个表被锁 SQLITE_NOMEM = 7; 内存分配失败 SQLITE_READONLY = 8; 试图对一个只读数据库进行写操作 SQLITE_INTERRUPT = 9; 由sqlite_interrupt()结束操作 SQLITE_IOERR = 10; 磁盘I/O发生错误 ...

    SQLite开发笔记3(在arm-linux平台上建立嵌入式C数据库)

    SQLite开发笔记3--在arm-linux平台上建立嵌入式C数据库(SQLite 3.6.5)的使用笔记

    SQLite3数据库中的文件锁和同步机制

    SQLite3 提供了一个新的锁和同步机制来提高并发,减少死锁。  SQLite3的锁和同步有PagerModule(pager.c)负责处理。PagerModue负责SQLite事务的ACID,也提供缓存功能。PagerModue不需要知道BTree,字符编码, 索引...

    C#多线程读写sqlite

    多线程读写sqlite数据库,同步锁,计时测试读写性能,

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

    今天在向Sqlite数据库里... 您可能感兴趣的文章:SQLite教程(八):命令行工具介绍SQLite教程(十二):锁和并发控制详解SQLite教程(十四):C语言编程实例代码(2)python查询sqlite数据表的方法SQLite之Autoincre

    Android例子源码解决多线程读写sqlite数据库锁定问题

    我们可以得知SQLite是文件级别的锁:多个线程可以同时读,但是同时只能有一个线程写。Android提供了SqliteOpenHelper类,加入Java的锁机制以便调用。如果多线程同时读写(这里的指不同的线程用使用的是不同的Helper...

    sqlite3.dll

    使用sqlite数据库锁必须的dll文件, 直接导入Unity中, 使用十分便捷, 仅供大学学习参考

    VB6.0 操作SQLite 数据库的完整示例代码

    示例代码打开前,先【右键->以管理员身份】运行【register.bat】文件注册控件。这个DLL控件功能全面,操作方便。

    SqliteHelper.zip

    采用ReaderWriterLockSlim锁,实现读写锁分离,多线程操作sqlite数据库。参考 让C#轻松实现读写锁分离--封装ReaderWriterLockSlim C#解决SQlite并发异常问题的方法(使用读写锁)2篇文章,已经封装sqlite常用增删改...

    SQLiteManager操作SQLite数据库

    SQLite适合开发android手机时用到的小型数据库,可以方便的进行增删改查!

    sqlite数据库锁定问题.zip

    我们可以得知SQLite是文件级别的锁:多个线程可以同时读,但是同时只能有一个线程写。Android提供了SqliteOpenHelper类,加入Java的锁机制以便调用。如果多线程同时读写(这里的指不同的线程用使用的是不同的Helper...

    C# SQLite开发包及实例源码.zip

    SQLite 数据库中所有的信息(比如表、视图、触发器等)都包含在一个文件夹内,方便管理和维护。 4.跨平台 SQLite 目前支持大部分操作系统,不至电脑操作系统更在众多的手机系统也是能够运行,比如:Android。 5.多...

    sqlite3..zip

    sqlite3 小工具,可以用来处理svn的锁死问题

    sqlite网络中间件(适用于cs架构开发)

    源码中大部分函数是按照易语言sqlite组件中的函数做的。我就不写帮助了.大家看知识库就行. 我是做个例子, 开发的时候如果直接调用会很慢。 另有几个以"快_"开头的函数,是我自己开发常用的.速度比较快. 服务器客户...

    SQLite学习手册(带目录)

    SQLite学习手册(锁和并发控制) 一、概述 二、文件锁 三、回滚日志 四、数据写入 五、SQL级别的事物控制 SQLite学习手册(实例代码<一>) 一、获取表的Schema信息 二、常规数据插入 SQLite学习手册(实例代码<二>) 三、...

Global site tag (gtag.js) - Google Analytics