`
chinamming
  • 浏览: 151346 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

SQLite剖析(7):锁和并发控制

 
阅读更多
本文整理自http://sqlite.org/lockingv3.html。
在SQLite中,锁和并发控制机制都是由pager.c模块负责处理的,用于实现ACID(Atomic, Consistent, Isolated和Durable)特性。在含有数据修改的事务中,该模块将确保或者所有的数据修改全部提交,或者全部回滚。与此同时,该模块还提供了一些磁盘文件的内存Cache功能。
事实上,pager模块并不关心数据库存储的细节,如B-Tree、编码方式、索引等。它只是将其视为由统一大小(通常为1024字节)的数据块构成的单一文件,其中每个块被称为一个页(page)。页的起始编号为1,即数据库的首个1024字节称为"page 1",其后的页编号以此类推。pager通过OS接口模块(如os_unix.c, os_win.c)与操作系统通信。
1、锁
从单个进程的角度来看,一个数据库文件可以有五种不同的锁状态:
(1)UNLOCKED: 文件没有持有任何锁,即当前数据库不存在任何读或写的操作。其它的进程可以在该数据库上执行任意的读写操作。此状态为缺省状态。
(2)SHARED: 在此状态下,该数据库可以被读取但是不能被写入。在同一时刻可以有任意数量的进程在同一个数据库上持有共享锁,因此读操作是并发的。换句话说,只要有一个或多个共享锁处于活动状态,就不再允许有数据库文件写入的操作存在。
(3)RESERVED: 假如某个进程在将来的某一时刻打算在当前的数据库中执行写操作,然而此时只是从数据库中读取数据,那么我们就可以简单的理解为数据库文件此时已经拥有了保留锁。当保留锁处于活动状态时,该数据库只能有一个或多个共享锁存在,即同一数据库的同一时刻只能存在一个保留锁和多个共享锁。在Oracle中此类锁被称之为预写锁,不同的是Oracle中锁的粒度可以细化到表甚至到行,因此该种锁在Oracle中对并发的影响程度不像SQLite中这样大。
(4)PENDING: 该锁的意思是说,某个进程正打算在该数据库上执行写操作,然而此时该数据库中却存在很多共享锁(读操作),那么该写操作就必须处于等待状态,即等待所有共享锁消失为止,与此同时,新的读操作将不再被允许,以防止写锁饥饿的现象发生。在此等待期间,该数据库文件的锁状态为PENDING,在等到所有共享锁消失以后,PENDING锁状态的数据库文件将在获取排他锁之后进入EXCLUSIVE状态。
(5)EXCLUSIVE: 在执行写操作之前,该进程必须先获取该数据库的排他锁。然而一旦拥有了排他锁,任何其它锁类型都不能与之共存。因此,为了最大化并发效率,SQLite将会最小化排他锁占有的时间总量。
2、回滚日志
当一个进程要修改数据库文件的时候(并且不在WAL模式下),它首先将未改变之前的内容记录到回滚日志文件中。回滚日志还要记录数据库的初始大小,以便以后进行回滚操作。如果SQLite中的某一事务正在试图修改多个数据库中的数据(使用了ATTACH命令),那么此时每一个数据库都将生成一个属于自己的回滚日志文件,用于分别记录属于自己的数据改变,与此同时还要生成一个用于协调多个数据库操作的主数据库日志文件,在主数据库日志文件中并不包含要回滚的页数据,它只是包含各个数据库回滚日志文件的文件名。在每个回滚日志文件中也同样包含了主数据库日志文件的文件名信息。然而对于无需主数据库日志文件的回滚日志文件,其中也会保留主数据库日志文件的信息,只是此时该信息的值为空。
我们可以将回滚日志视为"HOT"日志文件,因为它的存在就是为了恢复数据库的一致性状态。当某一进程正在更新数据库时,应用程序或OS突然崩溃,这样更新操作就不能顺利完成,于是产生HOT日志。因此我们可以说HOT日志只有在异常条件下才会生成,如果一切都非常顺利的话,该文件将永远不会存在。
在没有主数据库日志情况下,如果一个日志有非零头部,并且相关的数据库文件没有RESERVED锁,则它是HOT的。在有主数据库日志情况下,如果一个日志的主数据库日志存在,且在相关的数据库文件上没有RESERVED锁,则它也是HOT的。理解一个日志什么时候是HOT的非常重要,可以把前面的这些规则写成下面形式:
* 一个日志是HOT的,如果
* 它存在,且
* 它的空间大小大于512字节,且
* 日志头部非零,结构良好,且
* 它的主数据库日志存在,或者主数据库文件名为空字符串,且
* 在相关的数据库文件没有RESERVED锁。
在读数据库之前,SQLite总是先检查它是否有一个HOT日志。如果有,则在读数据库之前先执行回滚,以保证数据库状态是一致的。当一个进程想要读取数据库时,先要完成以下步骤:
(1)打开数据库文件并获取一个共享锁。如果不能获取共享锁,则立刻失败并返回SQLITE_BUSY。
(2)检查数据库文件是否有HOT日志,如果没有,则工作完成,立刻返回。如果有,则这个日志必须根据下面的算法步骤进行回滚。
(3)对数据库文件获取等待锁,再获取排他锁(注意不要获取保留锁,因为这会让其他进程认为日志不再是HOT的了)。如果获取失败,意味着另外一个进程正尝试做回滚操作。这时只能释放所有的锁,关闭数据库,返回SQLITE_BUSY。
(4)读取日志文件并且回滚之前的修改。
(5)等待回滚写入到持久存储设备,以恢复数据库的完整性。
(6)删除日志文件(或者如果设置了PRAGMA journal_mode=TRUNCATE指令,则把日志缩短成0字节;如果设置了PRAGMA journal_mode=PERSIST指令,则把日志头部清零)。
(7)删除主数据库日志,如果这样做安全的话。该步是可选的,只是为避免过期的主数据库日志文件塞满磁盘。
(8)释放排他锁和等待锁,但仍保持共享锁。
在这些算法步骤成功完成后,就可以安全读取数据库了。一旦所有的读取完成,释放共享锁。
过期的主数据库日志不再有任何用途,删除它只是为了释放磁盘空间。一个主数据库日志是过期的,如果没有单独的日志文件指向它。为了断定一个主数据库日志是否过期,SQLite首先读取主数据库日志文件以获取所有日志文件名。然后检查这些日志文件,看其中是否有主数据库日志文件名字段指向该主数据库日志的,如果有则主数据库文件不是过期的,否则主数据库文件过期。
3、数据写入
如果某一进程要想在数据库上执行写操作,那么必须像前面描述一样先获取共享锁(如果有HOT日志,则要回滚未完成的更改),在共享锁获取之后再获取保留锁。因为保留锁预示着在将来某一时刻该进程将会执行写操作,所以在同一时刻只有一个进程可以持有一把保留锁,但是其它进程可以继续持有共享锁以完成数据读取的操作。如果要执行写操作的进程不能获取保留锁,那说明另一进程已经获取了保留锁。在此种情况下,写操作将失败,并立即返回SQLITE_BUSY错误。在成功获取保留锁之后,该写进程将创建回滚日志。日志的头部初始化为数据库文件的原有大小。日志头部中也有主数据库日志文件名的字段,初始时为空字符串。
在对任何数据做修改之前,写进程会将待修改页中的原有内容先行写入回滚日志文件中,然而将要发生变化的页起初并不会直接写入磁盘文件,而是先保留在内存中。这样数据库仍然是未修改的,其它进程就可以继续读取该数据库中的数据。
或者是因为内存中的cache已满,或者是应用程序已经提交了事务,最终,写进程将数据更新到数据库文件中。然而在此之前,写进程必须确保没有其它的进程正在读取数据库,同时回滚日志中的数据确实被物理地写入到磁盘文件中(以便系统崩溃或断电时能用它来进行回滚)。其步骤如下:
(1)确保所有的回滚日志数据被物理地写入磁盘文件,以便在出现系统崩溃时可以将数据库恢复到一致的状态。
(2)对数据库文件获取等待锁,再获取排他锁,如果此时其它的进程仍然持有共享锁,写入线程将不得不被挂起并等待直到那些共享锁消失之后,才能进而得到排他锁。
(3)将内存中持有的修改页写入到原有的磁盘文件中。
如果写入到数据库文件的原因是因为cache已满,那么写入进程将不会立刻提交,而是继续对其它页进行修改。但是在后续的修改被写入到数据库文件之前,回滚日志必须被再一次刷新到磁盘中。还要注意的是,写入进程获取的排他锁必须被一直持有,直到所有的更改被提交为止。这意味着从数据第一次被刷新到磁盘文件开始,直到事务被提交之前,其它的进程不能访问该数据库。
当写入进程准备提交更改时,将执行以下步骤:
(4)获取排他锁,同时通过上面的步骤1-3确保所有内存中的变化数据都被写入到磁盘文件中。
(5)将数据库文件的所有修改物理地写入到磁盘中。
(6)删除日志文件(或者如果PRAGMA journal_mode为TRUNCATE或PERSIST,截短日志文件或者对头部清零)。如果在删除之前出现系统故障,进程在下一次打开该数据库时仍将基于该HOT日志进行恢复操作。因此只有在成功删除日志文件之后,我们才可以认为该事务成功完成。
(7)从数据库文件中删除所有的排他锁和PENDING锁。
一旦PENDING锁被释放,其它的进程就可以开始再次读取数据库了。在当前的实现中,保留锁也会被释放,但这不是必须的。将来的SQLite版本可能提供一个SQL命令"CHECKPOINT",用于提交当前事务所做的所有更改,但持有保留锁,以便可以做更多的更改,而不给任何其他进程写数据的机会。
如果一个事务中包含多个数据库的修改,那么它的提交逻辑将更为复杂,见如下步骤:
(4)确保每个数据库文件都已经持有了排他锁和一个有效的日志文件。
(5)创建主数据库日志文件,其文件名是随机的。同时将每个数据库的回滚日志文件的文件名写入该主数据库日志文件,并刷新到磁盘上。
(6)再将主数据库日志文件的文件名分别写入到每个数据库回滚日志文件的指定位置,并刷新到磁盘。
(7)将所有的数据库变化持久化到数据库磁盘文件中。
(8)删除主日志文件,如果在删除之前出现系统故障,进程在下一次打开该数据库时仍将基于该HOT日志进行恢复操作。因此只有在成功删除主日志文件之后,我们才可以认为该事务成功完成。
(9)删除每个数据库各自的日志文件。
(10)从所有数据库中删除掉排他锁和PENDING锁。
最后需要说明的是,在SQLite2中,如果多个进程正在从数据库中读取数据,也就是说该数据库始终都有读操作发生,即在每一时刻该数据库都持有至少一把共享锁,这样将会导致没有任何进程可以执行写操作,因为在数据库持有读锁的时候是无法获取写锁的,我们将这种情形称为“写饥饿”。在SQLite3中,通过使用PENDING锁则有效的避免了“写饥饿”情形的发生。当某一进程持有PENDING锁时,已经存在的读操作可以继续进行,直到其正常结束,但是新的读操作将不会再被SQLite接受,所以在已有的读操作全部结束后,持有PENDING锁的进程就可以被激活并试图进一步获取排他锁以完成数据的修改操作。
4、数据库文件是怎么损坏的
pager模块是非常健壮的,但有时候也会被破坏。如果一个流氓进程打开数据库文件或日志,写入无用的数据,则数据库将损坏。对这种情况,无需更多讨论。
在Unix上,SQLite使用POSIX建议的锁来实现加锁功能。在Windows上则使用LockFile(), LockFileEx()和UnlockFile()系统调用。SQLite假设这些系统调用能正确工作,否则数据库也有可能损坏。有一点要注意,POSIX建议的锁比较简单,但甚至在许多NFS上都没有实现(包括当前的Max OS X版本),有很多报告称Windows下的网络文件系统也有锁的问题,因此你最好避免在网络文件系统上使用SQLite。
Unix下SQLite使用fsync()系统调用来把数据刷新到磁盘,Windows下则使用FlushFileBuffers()。重申一下,SQLite假设这些操作系统服务函数是正确工作的。但有报告称fsync()和FlushFileBuffers()并不总是能正确地工作,特别是在廉价的IDE硬盘上。有一些IDE硬盘厂商的控制器芯片报告数据已经写入到磁盘表面,但实际上数据还在硬盘驱动电路的易失性Cache中。也有报告称Windows有时由于一些不确定的原因会忽略FlushFileBuffers()。如果这些报告属实,那意味着因为断电而导致数据库损坏是有可能的。SQLite并不能防止硬件和OS的漏洞。
如果Linux ext3文件系统在/etc/fstab中没有"barrier=1"选项的情况下被挂载,且磁盘驱动的写缓存是激活的,则当掉电或OS崩溃时文件系统损坏就有可能发生,特别对于廉价消费级的硬盘。而带有非易失性写缓存的企业级存储设备发生文件系统损失的可能性则小得多。据说有许多Linux发行版不使用barrier=1选项,并且不禁用写缓存,因此许多Linux发行版对这个问题是比较脆弱的。注意这是操作系统和硬件问题,SQLite无能为力,其他的数据库引擎也有这个问题。
如果发生崩溃或断电,则产生HOT日志,但是这个HOT日志被删掉了。下一进程打开数据库时将不知道数据库需要回滚,数据库处于不一致的状态。有很多原因会导致回滚日志被删除:
(1)系统管理员可能会在OS崩溃或系统掉电后做清理工作,看到日志文件认为它是垃圾,删除掉。
(2)有人(或者某个进程)可能会重命名数据库文件,但却没有得命名相关的日志。
(3)如果数据库文件有别名(硬链接或软链接),且通过链接别名来打开数据库文件,则生成的日志文件将以链接名来命名,若下次打开数据库时使用另一个链接名,将找不到日志。为了避免这个问题,你不应该对SQLite数据库文件创建链接。
(4)断电导致的文件系统损坏可能导致日志被重命名或被删除。
当SQLite在Unix上创建一个日志文件时,会打开这个日志文件所在的目录,并且调用fsync(),试图把目录信息写入磁盘。但假设另外一个进程正在向该目录添加或从该目录中删除不相关的文件,这时突发断电,就有可能导致日志文件从该目录中被删除并移到"lost+found"。这是一个罕见的场景,但有可能发生。避免这种情况的最好方式是使用日志文件系统。
对涉及多个数据库和一个主数据库日志的事务提交,如果这些数据库位于不同的磁盘卷上,在事务提交时发生断电,机器重新起来后磁盘可能用不同的名称来挂载,或者一些磁盘根本就不挂载。这样的情况下,各个日志文件和主数据库日志文件可能互相不能找到对方,最坏的结果是提交变得不再是原子性的了。一些数据库可能回滚,另一些则没有回滚。为了避免这样的问题,我们应该把所有数据库存放在一个磁盘卷上,并且断电后使用同样的名字来挂载硬盘。
5、SQL级别的事务控制
SQLite 3在实现上针对锁和并发控制做了一些精细的变化,特别是对于事务这一SQL语言级别的特征。在缺省情况下,SQLite 3会将所有的SQL操作置于antocommit模式下,这样所有针对数据库的修改操作都会在SQL命令执行结束后被自动提交。在SQLite中,SQL命令"BEGIN TRANSACTION"(其中TRANSACTION关键字可选)用于显式的声明一个事务,禁用autocommit模式,即其后的SQL语句在执行后都不会自动提交,而是需要等到SQL命令"COMMIT"或"ROLLBACK"被执行时,才考虑提交还是回滚。注意BEGIN命令并不获得任何类型的锁,在BEGIN之后,当执行第一个SELECT语句时才得到一个共享锁,当执行第一个DML语句(INSERT, UPDATE或DELETE)时才获得一个保留锁。至于排它锁,只有在数据从内存写入磁盘时开始,直到事务提交或回滚之前才能持有排它锁。
SQL命令COMMIT命令并不实际提交更改到磁盘,它只是重新打开autocommit模式。然后,在命令结束时,正式的自动提交逻辑才实际提交更改到磁盘。SQL命令ROLLBACK也是打开autocommit模式,但是它设置一标志,以告诉自动提交逻辑执行回滚,而不是提交。如果自动提交逻辑提交更改失败,因为另外有进程持有共享锁,则autocommit模式会自动关闭。这允许用户在共享锁释放之后重新COMMIT。
如果多个SQL命令在同一个时刻同一个数据库连接中被执行,autocommit将会被延迟执行,直到最后一个命令完成。比如,如果一个SELECT语句正在被执行,在这个命令执行期间,需要返回所有检索出来的行记录,如果此时处理结果集的线程因为业务逻辑的需要被暂时挂起并处于等待状态,而其它的线程此时或许正在该连接上对该数据库执行INSERT、UPDATE或DELETE命令,那么所有这些命令作出的数据修改都必须等到SELECT检索结束后才能被提交。
更多0
分享到:
评论

相关推荐

    sqlite源码及分析

    5. 锁机制:SQLite采用了页级锁定策略,根据工作模式的不同,可以实现共享锁和独占锁,以协调多个并发连接对数据库的访问。 入门教程: 对于初学者,可以从以下几个方面入手学习SQLite: 1. SQL基础:熟悉SQL语言...

    sqlite 源代码分析资料

    4. 锁与事务:SQLite支持多版本并发控制(MVCC),确保在并发环境下数据的一致性。它使用行级锁,并通过 WAL(Write-Ahead Logging)日志模式来实现事务的ACID特性。 5. 动态类型与兼容性:SQLite采用动态类型系统...

    图书:使用SQLite

    4. **事务与并发控制**:了解SQLite如何支持事务处理,确保数据的一致性和完整性,以及在多用户环境下如何实现并发控制。 5. **安全性与备份**:学习设置访问权限、加密数据库、备份与恢复策略,以确保数据的安全和...

    sqlite数据库c++类封装,及源码实例,boost多线程

    SQLite是一个轻量级的、开源的关系型...4. 多线程环境下的并发控制和同步机制,确保数据库操作的线程安全性。 通过对这些知识点的掌握和实践,你不仅可以提升C++编程能力,还能掌握数据库管理和多线程编程的核心技术。

    SQLiteStudio_sqlite3_源码

    2. **查询编辑器**:提供了一个 SQL 查询编辑器,支持语法高亮、自动完成和执行查询,方便数据分析和调试。 3. **数据浏览**:可视化显示表格数据,支持添加、修改和删除记录,方便数据操作。 4. **备份和导入导出**...

    SQLite入门与分析.doc

    对于那些不需要复杂事务处理和并发控制的场景,SQLite 提供了一个简单、高效的解决方案。然而,对于需要高度并发和复杂事务处理的应用,可能需要考虑使用更为强大的数据库系统,如 MySQL 或 PostgreSQL。

    SQLite3源程序分析

    在并发环境中,SQLite3使用锁机制来管理多个事务的访问冲突。 4. 索引与查询优化 SQLite3支持多种类型的索引,包括B树索引、唯一索引、主键索引以及覆盖索引等。优化器在生成执行计划时会考虑索引的存在,选择最佳...

    SQLITE数据库 UPDATE慢

    合理规划并发访问,或者在设计数据库时考虑并发控制,有助于减轻锁竞争。 6. **查询优化**:优化UPDATE语句本身也是提升性能的关键。避免在WHERE子句中使用复杂的表达式或不适当的函数,尽量保持查询条件简单。同时...

    sqlite源码

    7. **锁机制**:SQLite使用共享锁和独占锁来处理并发访问。了解锁的获取和释放策略有助于优化多线程环境下的性能。 8. **错误处理和日志记录**:SQLite源码中包含了丰富的错误检测和处理机制,以及详细的日志记录,...

    sqlite-amalgamation-3140200

    6. 并发控制:SQLite使用共享锁机制来处理多个并发连接,学习这部分可以帮助我们理解如何在多线程环境下正确使用SQLite。 7. 扩展功能:通过sqlite3ext.h,可以了解如何编写自定义的SQL函数和聚合函数,增强SQLite...

    Sqlite3源代码

    6. **锁机制**:为了支持多用户并发访问,SQLite使用了行级锁定和意向锁策略。源代码中包含了锁的申请、释放和冲突检测的实现。 7. **索引**:SQLite支持多种类型的索引,如B-Tree索引、唯一索引、全文索引等。源...

    SQLite学习手册(带目录)

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

    SQLite源码

    4. **锁机制**:SQLite使用页级锁定来实现并发控制,源码中包含各种锁的实现,如共享锁、独占锁等,这对于多线程或多进程环境下的数据库操作至关重要。 5. **内存管理**:SQLite对内存使用有精细的管理,包括内存池...

    Apress.The.Definitive.Guide.to.SQLite.May.2006.rar

    7. **Windows与Linux下的SQLite**:了解在两种不同操作系统环境下如何调试和管理SQLite数据库,包括日志分析和故障排查。 8. **高级特性**:探索SQLite的触发器、视图、存储过程、游标等高级功能,以及如何利用它们...

    sqlite最新源代码

    4. **事务和并发控制**:SQLite支持ACID(原子性、一致性、隔离性和持久性)特性,这涉及到事务管理和并发控制策略,如`mutex.c`、`transaction.c`。 5. **VFS(Virtual File System)**:SQLite的VFS层允许在不同...

    SQLite参考手册(chm格式).7z

    1. **并发性**:SQLite允许多个读取者和一个写入者同时访问数据库,确保了高并发性。 2. **存储类型**:数据类型灵活,支持NULL、整型、浮点型、字符串和BLOB(二进制大对象)。 3. **索引**:支持B树索引,提高查询...

    sqlite3源码库

    通过阅读和研究这些源代码,你可以深入了解SQLite3的工作原理,例如如何解析SQL语句,如何处理内存管理和磁盘I/O,以及如何实现事务和并发控制。 3. 在VS2013中编译SQLite3: 要在Visual Studio 2013中编译SQLite3...

    Android SQLite源码+说明

    7. **安全性**:SQLite的加密机制和权限控制。 通过研究源码,开发者可以更深入地了解SQLite的工作流程,从而优化数据库操作,减少资源消耗,提升应用性能。对于大型或高性能要求的应用,了解SQLite内部机制可以...

Global site tag (gtag.js) - Google Analytics