← 中文资源

简体中文版本

幻读

幻读指一个事务重跑查询时,发现另一事务自其首次执行以来插入了新行。

阅读英文原文

概念定义

幻读是一种并发异常:一个事务执行查询,另一事务插入或删除匹配该查询的行并提交,第一个事务再执行同一查询时得到不同的行集。它与不可重复读不同,后者关乎某一行发生变化;幻读关乎匹配某一谓词的新出现或消失的行。它是促使可串行化隔离与范围锁出现的异常。

为什么重要

应用规则常常作用于一个集合:不允许重叠预订、活跃用户名唯一、总额必须低于上限。在检查与动作之间,另一事务可以插入一条符合条件的行,于是两个事务都认为自己满足规则并双双提交,从而违反它。这正是可串行化隔离旨在消除幻读的原因,也是“先检查后插入”需要比行级保证更强的保护的原因。

工程实践

当决策依赖于一个集合时,让检查与动作在覆盖该范围的级别或锁下成为单一原子单元:可串行化隔离、显式的范围或谓词锁,或由数据库强制执行的唯一约束。尽可能用声明式约束替代应用层检查,因为引擎可以在无竞态的情况下强制执行。要意识到仅靠快照隔离仍可能允许写偏斜,因此在假设某个级别足够之前,先弄清引擎实际提供的保证。

代码示例

-- 乐观锁更新:版本匹配才写入,且不允许透支。
UPDATE accounts
SET balance = balance - :amount,
    version = version + 1
WHERE id = :account_id
  AND version = :expected_version
  AND balance >= :amount;

-- 应用侧检查受影响行数:
--   1  -> 更新成功
--   0  -> 并发冲突,或余额不足(需再读一次以区分)

-- 取回最新版本用于重试:
SELECT id, balance, version FROM accounts WHERE id = :account_id;
interface VersionedRow {
  readonly key: string;
  readonly value: string;
  readonly startTs: number;
  readonly endTs: number;
}

export class MvccStore {
  private versions: VersionedRow[] = [];
  private clock = 0;

  beginTransaction(): number {
    return ++this.clock;
  }

  write(key: string, value: string, ts: number): void {
    for (const row of this.versions) {
      if (row.key === key && row.endTs === Number.POSITIVE_INFINITY) row.endTs = ts;
    }
    this.versions.push({ key, value, startTs: ts, endTs: Number.POSITIVE_INFINITY });
  }

  read(key: string, snapshotTs: number): string | undefined {
    let found: VersionedRow | undefined;
    for (const row of this.versions) {
      if (row.key !== key) continue;
      if (row.startTs <= snapshotTs && snapshotTs < row.endTs) {
        if (!found || row.startTs > found.startTs) found = row;
      }
    }
    return found?.value;
  }
}