离线数据持久化SQLite对比解析 - 第4节

4.1 事务隔离级别:READ UNCOMMITTED 与 SERIALIZABLE

事务是数据库系统保证数据一致性的基本单位,具备 ACID 特性。隔离性(Isolation)控制并发事务之间的可见程度。SQLite 只支持 READ UNCOMMITTED 和 SERIALIZABLE 两种隔离级别,不存在 READ COMMITTED 或 REPEATABLE READ。

4.1.1 核心概念对比

隔离级别脏读不可重复读幻读SQLite 默认?
READ UNCOMMITTED允许可能可能否(需 PRAGMA 显式设置)
SERIALIZABLE禁止禁止禁止是
  • READ UNCOMMITTED:读操作不加共享锁,可能读到其他事务尚未提交的数据(脏读)。仅在极少读多写少且允许脏读的场景下使用。
  • SERIALIZABLE:SQLite 的默认级别,所有事务按顺序串行化执行,完全隔离但并发度低。

关键事实:SQLite 的 SERIALIZABLE 并非通过两阶段锁或 MVCC 实现,而是依靠数据库级锁——写事务获得 EXCLUSIVE 锁后,阻止所有其他读/写。

4.1.2 设置隔离级别

通过 PRAGMA 命令更改当前连接的事务行为:

-- 设为 READ UNCOMMITTED(允许脏读)
PRAGMA read_uncommitted = 1;

-- 恢复为 SERIALIZABLE
PRAGMA read_uncommitted = 0;

注意:READ UNCOMMITTED 使得读操作不获取共享锁(SHARED),因此在写事务持有 PENDING 或 EXCLUSIVE 锁时仍可读取数据库快照,但可能读到未提交的中间数据。

4.2 锁状态:UNLOCKED → SHARED → RESERVED → PENDING → EXCLUSIVE

SQLite 使用五种锁状态管理并发访问,每种状态代表数据库文件当前被持有的锁类型。锁状态只能沿特定方向升级,不能跳级或降级。

4.2.1 五状态转换规则

  • UNLOCKED:初始状态,没有锁。任何操作发生时必须升级到 SHARED(读)或 RESERVED/EXCLUSIVE(写)。
  • SHARED:读锁,允许多个连接同时持有 SHARED 锁。此时可以读数据库,但不能写。
  • RESERVED:写预留锁,表示当前连接计划写入但尚未开始。允许其他连接继续读(继续持有 SHARED),但阻止其他连接获取 RESERVED 或更高锁。
  • PENDING:升级锁,表示连接即将开始写入。此时不允许新 SHARED 锁获得,但已有 SHARED 锁的连接可以继续读。等待已有 SHARED 锁释放后,升级为 EXCLUSIVE。
  • EXCLUSIVE:独占锁,只允许当前连接访问。写入操作必须在 EXCLUSIVE 状态下进行。其他连接不能读也不能写。

4.3 死锁场景:为什么 SQLite 极少死锁?

SQLite 使用单写者策略和严格锁升级顺序,使得死锁理论上的可能性极低,但并非不存在。

4.3.1 典型死锁条件

死锁四要素:互斥、持有并等待、非抢占、循环等待。在 SQLite 中,RESERVED 到 PENDING 的等待阶段可能造成两个事务互相阻塞。

-- 模拟死锁(极低概率,但概念上存在)
-- 连接 A:写事务,持有 RESERVED,等待升级到 PENDING
-- 连接 B:写事务,持有 RESERVED,等待升级到 PENDING
-- 两者均需要其他 SHARED 锁释放,但互相等待对方先释放 RESERVED

实际生产中,SQLite 的忙等待超时(busy_timeout)和单线程写提交大幅降低了死锁发生概率。大多数“死锁”其实是活锁——写事务因无法获得 EXCLUSIVE 锁而反复重试。

4.3.2 死锁检测与处理

  • SQLite 没有内置死锁检测器,依赖超时机制:PRAGMA busy_timeout 设置等待毫秒数。
  • 超时后返回 SQLITE_BUSY 错误码,应用程序应回滚当前事务并重试。
  • 避免长事务,保持写事务简短,减少锁持有时间。

4.4 常见误区

  1. 误区:SQLite 支持所有标准隔离级别。
    现实:只支持 READ UNCOMMITTED 和 SERIALIZABLE,无 READ COMMITTED。
  2. 误区:SHARED 锁可以升级为 RESERVED,RESERVED 可以直接升级为 EXCLUSIVE。
    现实:RESERVED → PENDING → EXCLUSIVE 两步,PENDING 阶段等待已有 SHARED 释放。
  3. 误区:死锁在 SQLite 中不可能发生。
    现实:概率极低但概念上存在,尤其在多个写事务并发且长时间持有 RESERVED 锁时可能发生循环等待。
  4. 误区:EXCLUSIVE 只能写,不能读。
    现实:EXCLUSIVE 是读写独占,持有 EXCLUSIVE 的连接可以正常读取和写入。

4.5 应用边界

场景推荐锁控制方法原因
只读查询(报表、分析)SERIALIZABLE + SHARED 锁避免脏读,简单可靠
高并发写入(日志写入)WAL 模式 + SERIALIZABLEWAL 允许读操作不被写阻塞,减少锁竞争
临时中间数据(脏读可接受)PRAGMA read_uncommitted=1减少锁开销,提升读取性能
多线程频繁短事务PRAGMA busy_timeout=5000避免 SQLITE_BUSY 立即失败,给写操作缓冲时间

4.6 练习题与实用清单

📘 概念检测

  1. SQLite 默认的隔离级别是什么?它与 READ UNCOMMITTED 的核心区别是什么?
  2. 请描述从 UNLOCKED 到 EXCLUSIVE 的完整锁升级过程,写出每步的锁状态名称。
  3. 为什么 SQLite 中两个写事务同时持有 RESERVED 锁且互相等待时可能形成死锁?写出死锁发生的条件。
  4. 在命令行中设置 3 秒的忙等待超时,应使用什么 PRAGMA?

📋 实用清单

检查项说明
确认当前连接隔离级别PRAGMA read_uncommitted;
配置忙等待超时PRAGMA busy_timeout = 5000;
检测锁冲突时重试逻辑捕获 SQLITE_BUSY(5)或 SQLITE_LOCKED(6),回滚后重试
启用 WAL 模式减少锁竞争PRAGMA journal_mode=WAL;
死锁恢复策略设置重试次数上限,避免无限等待

📌 代码示例:死锁处理

def safe_execute(conn, sql, retries=3):
    for i in range(retries):
        try:
            conn.execute(sql)
            conn.commit()
            return
        except sqlite3.OperationalError as e:
            if "database is locked" in str(e):
                time.sleep(0.1)
                continue
            else:
                raise
    raise Exception("操作超时,无法完成写入")
SQLite 锁状态转换与死锁路径图 UNLOCKED SHARED RESERVED PENDING EXCLUSIVE 死锁发生路径 事务 A(RESERVED)等待事务 B(RESERVED)释放 → 循环等待 双方均持有 RESERVED 且需要其他 SHARED 释放 → 无法升级到 PENDING UNLOCKED SHARED RESERVED PENDING EXCLUSIVE