Kodokon kodokon.com

事务:BEGIN、COMMIT、ROLLBACK 与 ACID

用事务让你的写操作具备原子性,并理解 ACID 保证在日常实践中究竟意味着什么。

8 分钟 · 3 题

在 Kodokon 中打开本课

一次银行转账就是两条 UPDATE:从一个账户扣款,给另一个账户入账。如果第二条失败,数据库就在说谎。事务让这个代码块变得不可分割,并带来 ACID 保证:Atomicity 原子性(要么全部完成,要么全都不做)、Consistency 一致性(约束始终保持有效)、Isolation 隔离性(并发的事务永远看不到彼此做到一半的状态)、Durability 持久性(COMMIT 之后即便崩溃也能幸存)。本节课请优先使用真正的 sqlite3 客户端:某些网页沙箱会对每条查询自动提交。

SQL
CREATE TABLE accounts (
  id INTEGER PRIMARY KEY,
  owner TEXT NOT NULL,
  balance REAL NOT NULL CHECK (balance >= 0)
);

INSERT INTO accounts (owner, balance)
VALUES
  ('alice', 500.0),
  ('bruno', 120.0);
CHECK 约束禁止任何负数余额。

正常情况:BEGIN 开启事务,写操作一条条累积起来,COMMIT 把它们一次性地全部定型。在这中间,没有任何其他连接能看到中间状态(alice 已扣款,而 bruno 尚未入账)。

SQL
BEGIN;

UPDATE accounts
SET balance = balance - 200
WHERE owner = 'alice';

UPDATE accounts
SET balance = balance + 200
WHERE owner = 'bruno';

COMMIT;

SELECT owner, balance FROM accounts;
原子转账:alice 变为 300,bruno 变为 320。

现在来看失败的情况。给 bruno 扣款 400 会违反 CHECK (balance >= 0):这条语句失败了。SQLite 有个微妙之处:默认情况下,错误只会取消出错的那条语句,但会让事务保持开启。接下来怎么做取决于你的应用代码:ROLLBACK 撤销一切(理智的本能反应),或者在错误可恢复时继续下去。

SQL
BEGIN;

UPDATE accounts
SET balance = balance - 400
WHERE owner = 'bruno';
-- Error: CHECK (balance >= 0) rejects the row.

ROLLBACK;

SELECT owner, balance FROM accounts;
ROLLBACK 之后,上一次 COMMIT 时的余额完好无损。

知识检测

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

  1. 原子性,也就是 ACID 中的 A,保证的是什么?
    • 事务里的查询运行得更快
    • 事务要么完整生效,要么完全不生效
    • 两个事务永远不能读取同一张表
  2. ROLLBACK 之后,数据库处于什么状态?
    • 上一次 COMMIT 时的状态,就好像那个 BEGIN 从未发生过
    • 错误之前已经成功的那些 UPDATE 会被保留
    • 数据库会一直锁定到下一个 BEGIN
  3. 为什么 10,000 条 INSERT 放在一个 SQLite 事务里会快得多?
    • SQLite 会成批地压缩插入的数据
    • 在 COMMIT 时只做一次磁盘同步,而不是每条语句一次
    • 事务期间索引被禁用