概念定义
幻读是一种并发异常:一个事务执行查询,另一事务插入或删除匹配该查询的行并提交,第一个事务再执行同一查询时得到不同的行集。它与不可重复读不同,后者关乎某一行发生变化;幻读关乎匹配某一谓词的新出现或消失的行。它是促使可串行化隔离与范围锁出现的异常。
为什么重要
应用规则常常作用于一个集合:不允许重叠预订、活跃用户名唯一、总额必须低于上限。在检查与动作之间,另一事务可以插入一条符合条件的行,于是两个事务都认为自己满足规则并双双提交,从而违反它。这正是可串行化隔离旨在消除幻读的原因,也是“先检查后插入”需要比行级保证更强的保护的原因。
工程实践
当决策依赖于一个集合时,让检查与动作在覆盖该范围的级别或锁下成为单一原子单元:可串行化隔离、显式的范围或谓词锁,或由数据库强制执行的唯一约束。尽可能用声明式约束替代应用层检查,因为引擎可以在无竞态的情况下强制执行。要意识到仅靠快照隔离仍可能允许写偏斜,因此在假设某个级别足够之前,先弄清引擎实际提供的保证。
代码示例
-- 乐观锁更新:版本匹配才写入,且不允许透支。
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;
}
}