功能说明
该示例演示 MVCC 的核心:每次写入不覆盖旧值,而是追加一个带生效区间的新版本。事务开启时拿一个时间戳,之后所有读取都只看在该快照时刻可见的版本。由于读不阻塞写、写不阻塞读,多个事务可以并行,同时各自看到一致的数据视图。
关键代码
每个版本记录起始时间戳与结束时间戳,活跃版本的结束时间为正无穷。写入时先给同 key 的活跃版本封上结束时间戳,即令其对该时间点之后的快照不可见,再追加新版本。读取遍历所有版本,只接受起始不晚于快照且快照早于其结束的版本,并在可能重叠时取起始最新的那一个。时间戳由单调递增的时钟产生。
使用注意
这是用于讲解的简化模型,未处理写写冲突与写偏斜:两个事务读取同一快照再写不同 key 时可能都提交,从而违反业务不变量,真实系统需以冲突检测或可串行化隔离补充。读取为线性扫描,生产实现用索引与可见性映射来定位版本。快照隔离保证读一致却不保证串行等价,选型前应明确所用引擎实际提供的保证。
代码示例
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;
}
}