游乐游手机版
首页/数据库/文章详情

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

时间:2026-04-20 06:04
一、概述: 要深入理解SQLite数据库的稳定与可靠特性,其核心的锁与并发控制机制是必须掌握的关键。这套机制的具体实现,主要由pager_module模块负责。它如同一位严谨的守护者,严格保障着数据库事务的ACID(原子性、一致性、隔离性、持久性)特性,确保每一次数据操作要么完整提交,要么彻底回滚,

一、概述:

要深入理解SQLite数据库的稳定与可靠特性,其核心的锁与并发控制机制是必须掌握的关键。这套机制的具体实现,主要由pager_module模块负责。它如同一位严谨的守护者,严格保障着数据库事务的ACID(原子性、一致性、隔离性、持久性)特性,确保每一次数据操作要么完整提交,要么彻底回滚,不留中间状态。此外,该模块还高效地管理着内存缓存,将磁盘文件的数据页缓存在内存中,从而显著提升数据读写效率。

值得注意的是,pager模块的设计是高度抽象的。它并不处理数据库文件的具体逻辑结构,例如B-Tree、编码格式或索引细节。在pager的视角中,整个数据库文件被简单地视为一系列大小固定(默认通常为1024字节,即1KB)的数据块的集合,每个块被称为一个“页”。页的编号从1开始,直观且连续,1号页即为文件的起始页。

二、文件锁机制详解:

为了实现多进程间的并发访问控制,SQLite设计了一套精密的文件锁状态机。在当前主流版本中,主要定义了五种锁状态,它们构成了协调读写操作的基础框架。

1). UNLOCKED(未锁定):

顾名思义,此状态表示数据库文件当前未被任何进程锁定。这是数据库的初始和空闲状态,任何进程都可以尝试对其进行读取或写入操作。

2). SHARED(共享锁):

当进程需要读取数据时,会获取共享锁。此状态下,数据库处于“只读模式”,允许多个进程同时持有共享锁进行读取,但禁止任何写入操作。只要存在一个有效的共享锁,写操作就必须等待。这是实现高并发读性能的核心。

3). RESERVED(保留锁):

这是一个“写意向锁”。当一个进程主要在读取数据,但已计划在稍后进行写入时,它会先获取一个保留锁。同一时间,一个数据库文件上只能存在一个保留锁,但它可以与多个共享锁共存。这种设计允许“读后写”的事务提前声明意图,避免了部分死锁情况。与Oracle等数据库的细粒度锁(行锁、表锁)相比,SQLite的文件级保留锁对并发性的影响更为显著。

4). PENDING(待定锁):

当持有保留锁的进程准备开始实际写入,但发现仍有其他进程持有共享锁时,它会将锁升级为PENDING状态。此时,写进程必须等待所有现有的读操作完成。关键之处在于,进入PENDING状态后,系统将拒绝新的读请求,以防止写操作被源源不断的读请求“饿死”。待所有现有共享锁释放后,PENDING锁才能进一步升级为排他锁。

5). EXCLUSIVE(排他锁):

这是进行数据写入所必需的终极锁状态。排他锁是独占的,一旦某个进程持有,其他任何类型的锁都无法再获取。因此,为了最大化数据库的并发能力,SQLite会极力优化,尽可能缩短持有排他锁的时间。

必须明确的是,SQLite将所有数据存储于单个文件,并仅提供文件级别的锁,这是一种粗粒度的锁策略。这与MySQL、PostgreSQL等支持行级锁、表级锁的“重量级”关系型数据库在架构上有本质区别。这直接影响了SQLite在高并发写入场景下的扩展性。这并非设计缺陷,而是一种针对嵌入式、轻量级应用场景的权衡。因此,选择合适的数据库时,必须清晰认识SQLite的适用边界。

三、回滚日志与崩溃恢复:

回滚日志是SQLite确保数据一致性与事务原子性的核心安全机制。当一个事务准备修改数据库时,其首要操作并非直接覆盖磁盘数据,而是将待修改页的原始内容完整备份到一个独立的回滚日志文件中。

对于涉及多个数据库文件的事务(ATTACH DATABASE),情况稍复杂:每个被修改的数据库都会生成自己的回滚日志(子日志),同时还会产生一个主日志文件用于全局协调。主日志记录了所有相关子日志的文件名,而每个子日志中也存储了主日志的信息。对于单数据库事务,该位置则为空。

这种日志常被称为“HOT”日志。它之所以“热”,是因为其唯一使命就是在系统崩溃或程序意外退出时,将数据库恢复到事务开始前的一致状态。在事务成功提交的正常流程中,该文件会被删除,仿佛从未存在过。

四、数据写入的完整流程:

下面,我们逐步拆解一个写事务的完整生命周期,看锁、日志与缓存如何协同工作。

准备阶段: 进程首先获取共享锁(SHARED),随即尝试获取保留锁(RESERVED)。保留锁是写入的“准入证”。若获取失败(通常因其他进程已持有),则立即返回SQLITE_BUSY错误。

日志记录: 成功获取保留锁后,进程创建回滚日志文件。在修改内存中的数据页缓存之前,会先将这些页的原始内容完整写入日志文件并同步到磁盘。此时,修改仅发生在内存,磁盘原文件未变,其他进程仍可正常读取。

写入与锁升级: 当缓存满或应用发起提交时,数据需写回磁盘。在此之前,写进程必须确保环境安全:
1. 强制将回滚日志中的所有数据刷入物理磁盘,这是崩溃恢复的保证。
2. 进行锁升级:先获取PENDING锁,再等待并获取排他锁(EXCLUSIVE)。此过程会等待所有现有共享锁释放,并阻止新读请求。
3. 将内存中已修改的“脏页”写回数据库磁盘文件。

若此次写盘仅因缓存满而非事务提交,则排他锁会持续持有。在后续修改新数据页前,需再次将更新的日志内容刷盘。这意味着从第一次刷盘到最终提交前,数据库将对其他所有进程锁定。

事务提交: 当最终提交事务时,流程如下:
4. 确认持有排他锁,且所有内存更改已写入磁盘文件。
5. 再次强制将数据库文件的更改刷入物理磁盘,确保持久化。
6. 关键步骤:删除回滚日志文件。 此操作是事务提交成功的最终标志。若删除前崩溃,下次启动时仍会利用该日志进行恢复。
7. 释放排他锁和PENDING锁。PENDING锁释放后,等待的读进程便可重新进入。

多数据库事务提交: 对于多数据库事务,提交逻辑更严谨:
4. 确保每个数据库均持有排他锁并拥有有效日志。
5. 创建主日志文件,记录所有子回滚日志的文件名。
6. 将主日志文件名回写到每个子日志的指定位置,建立双向关联。
7. 将所有数据库的更改持久化到磁盘。
8. 删除主日志文件(多数据库提交的关键标志)。
9. 删除各个数据库自身的回滚日志文件。
10. 释放所有数据库上的锁。

解决“写饥饿”: SQLite 3.x 版本针对“写饥饿”问题做了重要改进。在SQLite 2.x中,若数据库持续有读操作(共享锁不断),写操作可能永远无法获得锁。SQLite 3引入的PENDING锁机制解决了此问题:当有进程持有PENDING锁时,已存在的读操作可继续完成,但新的读请求会被阻塞。这确保了写操作在现有读操作结束后能获得执行机会。

五、SQL层的事务控制:

SQLite 3将底层锁机制与SQL语言层面的事务语义进行了优雅集成。默认情况下,每条SQL语句都在自动提交模式下运行,执行后立即提交。

当使用BEGIN TRANSACTION显式开启事务后,行为发生变化:BEGIN语句本身并不立即加锁。锁的获取与实际操作绑定——执行第一个SELECT时获取共享锁;执行第一个INSERT/UPDATE/DELETE时获取保留锁;而排他锁仅在提交阶段,数据从内存写入磁盘的短暂瞬间被持有。

一个需要注意的细节是:在同一数据库连接上,如果存在未完成的操作(如一个正在逐行输出结果的SELECT),自动提交会被延迟。即使另一个线程通过该连接执行了写操作,其提交也必须等待之前的读操作完全结束。

至此,关于SQLite核心并发与事务机制的探讨告一段落。本系列的后续内容将聚焦实战,提供具体的编程指南与典型应用示例,帮助您更好地运用SQLite。我们下篇再见。

来源:https://www.jb51.net/article/65433.htm
上一篇MongoDB查询文档的各种技巧和最佳实践 下一篇NINEDATA 实战指南:常见用法整理
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
Redis 7.0增量AOF重写RDB前导码配置详解
数据库 · 2026-07-02

Redis 7.0增量AOF重写RDB前导码配置详解

先说一个几乎所有人都踩过的典型误区:很多人把 aof-use-rdb-preamble yes 当作开启“增量重写”的开关。实际上,这个配置只干了一件事——让重写后的 AOF 文件头部带上 RDB 快照。它解决的是加载速度问题,跟“增量重写”本身的概念压根不是一回事。真正的增量重写,依赖的是 Red

在Python Tornado异步框架中安全执行SQL命令的方法与最佳实践
数据库 · 2026-07-02

在Python Tornado异步框架中安全执行SQL命令的方法与最佳实践

直接在Tornado里用SQLAlchemy同步执行SQL,结果就是阻塞IOLoop,所谓“异步框架里写同步数据库代码”,等于白搭。安全执行的关键不是“怎么写SQL”,而是“怎么不卡住事件循环”。 为什么不能在RequestHandler里直接调用session execute() 因为sessio

利用SQL触发器实现在INSERT数据时自动同步到审计表
数据库 · 2026-07-02

利用SQL触发器实现在INSERT数据时自动同步到审计表

先说结论:可以用触发器把 INSERT 数据同步到审计表,但必须用 AFTER INSERT,并且审计表的字段顺序、类型、字符集得和源表严格一致。否则,轻则写入错位、数据截断,重则直接报错、丢数据。下面把这些坑一个一个掰开说。 能,但必须用 AFTER INSERT,且审计表字段顺序、类型、字符集要

如何用SQL编写按不同工作日统计员工出勤率
数据库 · 2026-07-02

如何用SQL编写按不同工作日统计员工出勤率

在实际业务中,统计不同工作日的出勤率是HR系统里的高频需求。如果直接按日期函数分组,很容易掉进语言环境、索引失效或分母口径的坑里。下面就来拆解具体的实现要点。 必须用 CASE WHEN 将日期映射为固定 weekday 标签(如 Mon )再分组,避免语言环境导致的分组断裂;需过滤 DOW IN

Spring Boot 3动态拼接SQL为何引发严重安全漏洞
数据库 · 2026-07-02

Spring Boot 3动态拼接SQL为何引发严重安全漏洞

SQL注入漏洞的核心成因,本质上是因为用户输入直接参与了SQL语句的字符串拼接,而未采用参数化绑定机制。在MyBatis中使用${}、QueryWrapper中调用apply()与last()、JPA的@Query注解进行拼接等操作,都会绕过PreparedStatement的安全防护。动态字段必须