Kodokon kodokon.com

并发:锁、隔离级别、幻读

理解 SQLite 的锁模型、它的事务,以及 SQL 标准所描述的隔离异常。

12 分钟 · 3 题

在 Kodokon 中打开本课

SQLite 在整个数据库文件这一级别加锁,而不是在行这一级别。在默认模式(回滚日志)下,读者共享一个 SHARED 锁,但每次只能有一个写者获得 EXCLUSIVE 锁。结果就是:写操作被串行化了。让我们创建一张表来配合事务操作。

SQL
CREATE TABLE accounts (
  id INTEGER PRIMARY KEY,
  balance INTEGER NOT NULL
);

INSERT INTO accounts (id, balance)
VALUES (1, 100), (2, 50);

-- Read: deferred transaction (default)
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
COMMIT;
一个由 BEGIN/COMMIT 包裹的显式事务。

锁分阶段演进:UNLOCKED,然后是 SHARED(读)、RESERVED(有写意图)、PENDING,最后是 EXCLUSIVE(写)。如果一个事务已经持有写锁,另一个事务就会收到 SQLITE_BUSY 错误。与其立刻失败,不如用 busy_timeout 设置一个等待时长,并启用 WAL 模式,让读者和写者能够并行推进。

SQL
-- A single writer, concurrent readers
PRAGMA journal_mode = WAL;

-- Wait up to 5 s if the database is busy
PRAGMA busy_timeout = 5000;
WAL 解除了并发读的阻塞;busy_timeout 负责等待。

SQL 标准描述了三种异常:脏读(读取到未提交的数据)、不可重复读(重新读取同一行却看到不同的值),以及幻读(重放一个范围查询,看到有新行出现)。由于 SQLite 把写者串行化,它实际上表现为 SERIALIZABLE:这些异常不会出现(在使用了 read_uncommitted 的共享缓存模式之外)。在其他数据库中,你要显式地选择隔离级别。

SQL
-- Atomic transfer: immediate write lock
BEGIN IMMEDIATE;
UPDATE accounts
SET balance = balance - 10
WHERE id = 1;
UPDATE accounts
SET balance = balance + 10
WHERE id = 2;
COMMIT;
BEGIN IMMEDIATE 在任何写入之前就预留了锁。

知识检测

确认你已牢记本课的重点内容。

  1. SQLite 在实践中提供哪种隔离级别?
    • READ UNCOMMITTED,默认情况下可能发生脏读
    • READ COMMITTED,和 PostgreSQL 一样
    • SERIALIZABLE,因为每次只有单个写者在操作
    • 没有,它没有事务
  2. BEGINBEGIN IMMEDIATE 之间有什么区别?
    • BEGIN IMMEDIATE 立刻取得写锁;BEGIN 则等到第一次写访问
    • BEGIN IMMEDIATE 禁用回滚日志
    • BEGIN IMMEDIATE 创建一个新数据库
    • 两者在行为上没有区别
  3. 什么是幻读?
    • 读取到另一个事务尚未提交的值
    • 重新读取同一行却得到不同的值
    • 重放一个范围查询,看到期间另一个事务插入的新行出现
    • 读取到一行刚刚被删除的数据