mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2213 字
6 分钟
为什么 PostgreSQL 使用 MVCC
2024-03-30

PostgreSQL 是世界上最先进的开源关系型数据库之一,其高并发性能的核心秘密在于 MVCC(Multi-Version Concurrency Control,多版本并发控制)。这种设计让 PostgreSQL 在高并发场景下实现了”读不阻塞写,写不阻塞读”的特性。本文将深入探讨 PostgreSQL 选择 MVCC 的设计哲学。

一、并发控制的挑战#

在多用户并发访问数据库时,如果不加以控制,会产生一系列数据一致性问题。

1.1 经典的并发问题#

数据库并发操作会带来三种经典问题:

flowchart TB subgraph 脏读问题 T1A[事务 A 修改数据] --> T1B[事务 B 读取未提交数据] T1B --> T1C[事务 A 回滚] T1C --> T1D[事务 B 读到脏数据] end subgraph 不可重复读问题 T2A[事务 A 读取数据] --> T2B[事务 B 修改并提交] T2B --> T2C[事务 A 再次读取] T2C --> T2D[同一事务内数据不一致] end subgraph 幻读问题 T3A[事务 A 查询范围] --> T3B[事务 B 插入新行] T3B --> T3C[事务 A 再次查询] T3C --> T3D[发现"幻影"数据] end style T1D fill:#f99 style T2D fill:#f99 style T3D fill:#f99

三种问题的对比

问题类型描述影响范围
脏读读到其他事务未提交的数据单行
不可重复读同一事务内两次读取同一数据结果不同单行
幻读同一事务内两次查询返回的行数不同多行范围

1.2 ANSI SQL 隔离级别#

为了解决这些问题,ANSI SQL 定义了四种事务隔离级别:

flowchart LR subgraph 隔离级别 RU[读未提交<br/>Read Uncommitted] RC[读已提交<br/>Read Committed] RR[可重复读<br/>Repeatable Read] SE[可串行化<br/>Serializable] end RU -->|解决| D1[脏读] RC -->|解决| D2[不可重复读] RR -->|部分解决| D3[幻读] SE -->|完全解决| D4[所有问题] style RU fill:#fcc style RC fill:#ffc style RR fill:#cfc style SE fill:#ccf

隔离级别与问题的关系

隔离级别脏读不可重复读幻读
读未提交(RU)
读已提交(RC)
可重复读(RR)
可串行化(SER)

注意:PostgreSQL 的可重复读隔离级别实际上也能防止幻读,这是通过 MVCC 的快照隔离机制实现的。

二、锁机制的局限性#

在 MVCC 出现之前,数据库主要依赖锁机制来实现并发控制。

2.1 传统锁方案#

flowchart TB subgraph 读锁-共享锁 R1[读请求] --> S1[获取 S 锁] S1 --> S2[允许其他读] S1 --> S3[阻塞其他写] end subgraph 写锁-排他锁 W1[写请求] --> X1[获取 X 锁] X1 --> X2[阻塞其他读] X1 --> X3[阻塞其他写] end style S3 fill:#fcc style X2 fill:#fcc style X3 fill:#fcc

锁模式的兼容性矩阵

S 锁(读)X 锁(写)
S 锁兼容冲突
X 锁冲突冲突

2.2 锁机制的问题#

传统锁机制在高并发场景下存在严重问题:

-- 场景:大量读操作阻塞写操作
-- 事务 A:长时间读取
BEGIN;
SELECT * FROM orders WHERE status = 'pending'; -- 获取 S 锁
-- 此时事务 B 想要更新数据
-- 事务 B 被阻塞,直到事务 A 提交
-- 事务 B:等待更新
UPDATE orders SET status = 'processing' WHERE id = 1; -- 等待 X 锁

锁机制的主要问题

问题描述影响
读写冲突读操作阻塞写操作吞吐量下降
写读冲突写操作阻塞读操作查询延迟增加
锁粒度行锁开销大,表锁并发度低性能权衡
死锁风险多个事务互相等待需要超时机制
锁管理开销维护锁表需要内存和 CPU资源消耗

2.3 为什么需要更好的方案#

flowchart LR subgraph 理想特性 G1[读不阻塞写] G2[写不阻塞读] G3[保证一致性] G4[高性能] end subgraph 锁机制现状 B1[读阻塞写] B2[写阻塞读] B3[保证一致性] B4[性能受限] end G1 -.->|无法实现| B1 G2 -.->|无法实现| B2 style G1 fill:#cfc style G2 fill:#cfc style B1 fill:#fcc style B2 fill:#fcc

传统锁机制无法同时满足”高并发”和”数据一致性”的需求,这正是 MVCC 要解决的核心问题。

三、MVCC 核心思想#

3.1 什么是 MVCC?#

MVCC(Multi-Version Concurrency Control)的核心思想是:为每个数据修改创建新版本,而不是直接覆盖旧数据

flowchart TB subgraph 传统方式-原地更新 V1[数据值: 100] --> U1[UPDATE] U1 --> V2[数据值: 200] V2 -.->|旧值丢失| X1[] end subgraph MVCC-多版本 V3[版本 1: 100<br/>xmin=100] --> U2[UPDATE] U2 --> V4[版本 2: 200<br/>xmin=101] V3 -.->|旧值保留| X2[] V4 -.->|可读| X3[] end style X1 fill:#fcc style X2 fill:#cfc style X3 fill:#cfc

3.2 MVCC 的核心优势#

flowchart LR subgraph 读事务 R[SELECT] --> S[读取快照版本] S --> R1[无需等待锁] end subgraph 写事务 W[UPDATE] --> N[创建新版本] N --> W1[无需等待读锁] end R -.->|互不阻塞| W style R1 fill:#cfc style W1 fill:#cfc

MVCC 的核心优势

特性描述收益
读写不冲突读操作读取历史版本,写操作创建新版本高并发性能
无锁读取读操作不需要获取任何锁读性能优异
快照隔离每个事务看到一致的数据快照可重复读
非阻塞回滚回滚只需标记版本无效回滚快速

3.3 MVCC 的代价#

MVCC 并非完美无缺,它也有自身的代价:

mindmap root((MVCC 代价)) 存储开销 多版本数据 版本链表 额外空间占用 清理机制 Vacuum 进程 死元组清理 空间回收 写放大 更新产生新行 索引更新 WAL 日志增加 事务 ID 管理 事务 ID 回卷 冻结机制 运维复杂度

四、PostgreSQL 的 xmin/xmax 实现#

PostgreSQL 的 MVCC 实现非常精妙,主要通过隐藏的系统列来管理版本。

4.1 隐藏的系统列#

每张表都有四个隐藏的系统列:

-- 查看隐藏列
SELECT xmin, xmax, ctid, cmin, cmask, *
FROM users
LIMIT 5;
列名含义说明
xmin插入事务 ID创建该行版本的事务
xmax删除事务 ID标记该行版本删除/更新的事务
ctid物理位置 (页号, 行号)行的物理地址
cmin命令序号(插入)同一事务内的命令顺序
cmax命令序号(删除)同一事务内的命令顺序

4.2 版本链示意图#

flowchart TB subgraph 数据行版本链 R1[行版本 1<br/>id=1, name='Alice'<br/>xmin=100, xmax=102<br/>状态: 旧版本] R2[行版本 2<br/>id=1, name='Bob'<br/>xmin=102, xmax=105<br/>状态: 旧版本] R3[行版本 3<br/>id=1, name='Charlie'<br/>xmin=105, xmax=0<br/>状态: 最新版本] end R1 -.->|被更新| R2 R2 -.->|被更新| R3 subgraph 事务时间线 T100[事务 100: INSERT] T102[事务 102: UPDATE] T105[事务 105: UPDATE] end T100 --> R1 T102 --> R2 T105 --> R3 style R3 fill:#cfc style R1 fill:#fcc style R2 fill:#ffc

4.3 版本可见性规则#

PostgreSQL 使用复杂的可见性规则判断哪个版本对当前事务可见:

flowchart TD START[读取行版本] --> CHECK1{xmin 已提交?} CHECK1 -->|否| INVISIBLE1[不可见] CHECK1 -->|是| CHECK2{xmax = 0?} CHECK2 -->|是| VISIBLE[可见] CHECK2 -->|否| CHECK3{xmax 已提交?} CHECK3 -->|否| VISIBLE CHECK3 -->|是| CHECK4{xmax > 当前快照?} CHECK4 -->|是| VISIBLE CHECK4 -->|否| INVISIBLE2[不可见] style VISIBLE fill:#cfc style INVISIBLE1 fill:#fcc style INVISIBLE2 fill:#fcc

可见性判断的伪代码

// 简化的可见性判断逻辑
bool IsVisible(Row row, Snapshot snap) {
// 1. 插入事务必须在快照之前提交
if (!TransactionIdDidCommit(row.xmin) ||
row.xmin > snap.xmax) {
return false;
}
// 2. 如果 xmax = 0 或未提交,则可见
if (row.xmax == 0 ||
!TransactionIdDidCommit(row.xmax)) {
return true;
}
// 3. 如果删除事务在快照之后,则可见
if (row.xmax > snap.xmax) {
return true;
}
return false;
}

4.4 实际案例演示#

-- 事务 100: 插入数据
BEGIN;
INSERT INTO users (id, name) VALUES (1, 'Alice');
-- xmin=100, xmax=0
COMMIT;
-- 事务 101: 查询数据(看到 Alice)
BEGIN;
SELECT * FROM users WHERE id = 1;
-- 结果: Alice(xmin=100, xmax=0 可见)
COMMIT;
-- 事务 102: 更新数据
BEGIN;
UPDATE users SET name = 'Bob' WHERE id = 1;
-- 旧行: xmin=100, xmax=102(被标记删除)
-- 新行: xmin=102, xmax=0
COMMIT;
-- 事务 103: 查询数据(看到 Bob)
BEGIN;
SELECT * FROM users WHERE id = 1;
-- 结果: Bob(新行可见,旧行已被 xmax=102 标记)
COMMIT;

五、快照隔离级别#

5.1 PostgreSQL 的快照机制#

PostgreSQL 在事务开始时创建一个快照,记录当前活跃的事务 ID:

flowchart LR subgraph 快照内容 S1[xmin: 最小活跃事务 ID] S2[xmax: 下一个分配的事务 ID] S3[xip: 活跃事务 ID 列表] end subgraph 事务状态 T1[事务 100<br/>已提交] T2[事务 101<br/>进行中] T3[事务 102<br/>已提交] T4[事务 103<br/>进行中] end S2 --> |快照时刻| T4 style S2 fill:#cfc

快照数据结构

// PostgreSQL 快照结构(简化)
typedef struct SnapshotData {
TransactionId xmin; // 最小活跃事务 ID
TransactionId xmax; // 下一个要分配的事务 ID
// 活跃事务 ID 数组
TransactionId *xip;
uint32 xcnt;
// 时间戳
TimestampTz snapshottime;
} SnapshotData;

5.2 隔离级别实现#

PostgreSQL 实现了三种隔离级别,底层都基于 MVCC:

flowchart TB subgraph 读已提交-RC RC1[每条 SQL 创建新快照] RC2[能看到已提交的修改] RC3[不可重复读] end subgraph 可重复读-RR RR1[事务开始时创建快照] RR2[整个事务使用同一快照] RR3[可重复读,防止幻读] end subgraph 可串行化-SER SE1[使用 SSI 检测] SE2[检测到冲突时回滚] SE3[完全可串行化] end style RC1 fill:#ffc style RR1 fill:#cfc style SE1 fill:#ccf

隔离级别对比

隔离级别快照时机特点性能
读已提交(RC)每条 SQL能看到最新已提交数据
可重复读(RR)事务开始整个事务看到一致的数据视图
可串行化(SER)事务开始 + SSI串行化冲突检测

5.3 快照隔离示例#

-- 会话 A:使用可重复读
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 快照创建,假设此时 xmax = 100
SELECT balance FROM accounts WHERE id = 1;
-- 结果: 1000
-- 会话 B:更新数据并提交
BEGIN;
UPDATE accounts SET balance = 2000 WHERE id = 1;
COMMIT;
-- 会话 A:再次查询
SELECT balance FROM accounts WHERE id = 1;
-- 结果: 1000(仍然是快照中的值,不是 2000)
COMMIT;
sequenceDiagram participant A as 会话 A (RR) participant DB as 数据库 participant B as 会话 B A->>DB: BEGIN (快照: xmax=100) A->>DB: SELECT balance WHERE id=1 DB-->>A: 1000 (版本: xmin=50) B->>DB: BEGIN B->>DB: UPDATE accounts SET balance=2000 Note over DB: 新版本: xmin=101, xmax=0 B->>DB: COMMIT A->>DB: SELECT balance WHERE id=1 Note over DB: 快照 xmax=100<br/>版本 xmin=101 > 100<br/>不可见,返回旧版本 DB-->>A: 1000 (同一快照,结果一致) A->>DB: COMMIT

六、Vacuum 清理机制#

MVCC 的一个关键问题是如何清理不再需要的旧版本数据。

6.1 为什么需要 Vacuum?#

flowchart TB subgraph 更新操作前 T1[表空间: 100MB<br/>有效数据: 100MB] end subgraph 大量更新后 T2[表空间: 500MB<br/>有效数据: 100MB<br/>死元组: 400MB] end subgraph Vacuum 后 T3[表空间: 500MB<br/>有效数据: 100MB<br/>可用空间: 400MB] end subgraph Vacuum Full 后 T4[表空间: 100MB<br/>有效数据: 100MB] end T1 -->|UPDATE 操作| T2 T2 -->|VACUUM| T3 T3 -->|VACUUM FULL| T4 style T2 fill:#fcc style T3 fill:#ffc style T4 fill:#cfc

死元组的产生场景

操作死元组产生原因
DELETE被删除的行变成死元组
UPDATE旧行版本变成死元组
ROLLBACK未提交事务产生的所有行变成死元组

6.2 Vacuum 的工作原理#

flowchart LR subgraph Vacuum 流程 S1[扫描表] --> S2[识别死元组] S2 --> S3[更新索引] S3 --> S4[更新空闲空间映射] S4 --> S5[更新可见性映射] S5 --> S6[标记空间可重用] end subgraph Autovacuum 触发条件 C1[死元组超过阈值] C2[插入行数超过阈值] C1 --> T[自动启动] C2 --> T end style S6 fill:#cfc

Vacuum 的主要任务

-- 手动执行 Vacuum
VACUUM users; -- 清理死元组,标记空间可重用
VACUUM FULL users; -- 重建表,回收空间(会锁表)
VACUUM ANALYZE users; -- 清理 + 更新统计信息
-- 查看表的死元组数量
SELECT relname, n_live_tup, n_dead_tup
FROM pg_stat_user_tables
WHERE relname = 'users';

6.3 Autovacuum 自动清理#

PostgreSQL 提供了自动 Vacuum 机制:

flowchart TB subgraph 监控阶段 M1[监控表的修改统计] --> M2{是否达到阈值?} end subgraph 触发条件 M2 -->|是| T1[死元组触发:<br/>n_dead_tup > threshold] M2 -->|是| T2[插入触发:<br/>n_ins_tup > threshold] end subgraph 执行阶段 T1 --> E1[启动 Worker 进程] T2 --> E1 E1 --> E2[扫描并清理] E2 --> E3[更新统计信息] end E3 --> M1 style M1 fill:#ccf style E2 fill:#cfc

Autovacuum 配置参数

# postgresql.conf 配置
autovacuum = on # 启用自动清理
autovacuum_vacuum_threshold = 50 # 基础阈值
autovacuum_vacuum_scale_factor = 0.2 # 死元组比例阈值
# 触发条件计算公式:
# 触发阈值 = autovacuum_vacuum_threshold +
# autovacuum_vacuum_scale_factor * 表行数
# 例如:100 万行的表
# 触发阈值 = 50 + 0.2 * 1000000 = 200050 个死元组

6.4 可见性映射与 Freeze#

flowchart TB subgraph 可见性映射-VM VM1[标记页中所有元组对所有事务可见] VM2[加速 Vacuum 扫描] VM3[跳过无需清理的页] end subgraph 事务 ID 冻结 F1[防止事务 ID 回卷] F2[将 xmin 标记为 FrozenXID] F3[2 字节存储,节省空间] end subgraph 问题 P1[事务 ID 32 位限制] P2[ID 环绕: 2^31] P3[回卷导致数据丢失] end P1 --> P2 --> P3 --> F1 style P3 fill:#fcc style F1 fill:#cfc

事务 ID 回卷问题

-- 查看事务 ID 状态
SELECT relname, age(relfrozenxid) as xid_age,
pg_size_pretty(pg_total_relation_size(oid)) as size
FROM pg_class
WHERE relkind = 'r'
ORDER BY age(relfrozenxid) DESC
LIMIT 10;
-- 年龄接近 20 亿时,需要手动冻结
VACUUM FREEZE tablename;

七、与 MySQL MVCC 对比#

MySQL(InnoDB)也实现了 MVCC,但实现方式与 PostgreSQL 有显著差异。

7.1 实现方式对比#

flowchart TB subgraph PostgreSQL P1[主存储上的新版本] P2[xmin/xmax 标记] P3[版本链在堆表中] P4[Vacuum 清理] end subgraph MySQL InnoDB M1[Undo Log 中的旧版本] M2[事务 ID + 回滚指针] M3[版本链在 Undo Log] M4[Purge 线程清理] end style P1 fill:#cfc style M1 fill:#ccf

核心差异对比表

特性PostgreSQLMySQL InnoDB
新版本存储位置表空间(新元组)Undo Log(旧版本)
版本链方向新→旧(通过 xmax)旧→新(通过回滚指针)
读操作直接读可见版本构造 ReadView + 回滚
更新操作INSERT 新元组原地更新 + Undo Log
清理机制Vacuum(需要手动/自动)Purge(自动后台线程)
空间回收需要 VACUUM FULL自动回收 Undo 空间
二级索引不存储事务信息,需回表不存储事务信息,需回表

7.2 更新操作的实现差异#

flowchart LR subgraph PostgreSQL 更新 PG1[UPDATE 前<br/>id=1, name='Alice'<br/>xmin=100, xmax=0] PG2[UPDATE 后<br/>id=1, name='Bob'<br/>xmin=102, xmax=0] PG3[旧版本保留<br/>id=1, name='Alice'<br/>xmin=100, xmax=102] PG1 -->|UPDATE| PG2 PG1 -->|标记删除| PG3 end subgraph MySQL 更新 MY1[UPDATE 前<br/>id=1, name='Alice'<br/>TRX_ID=100] MY2[UPDATE 后<br/>id=1, name='Bob'<br/>TRX_ID=102] MY3[Undo Log<br/>旧值: 'Alice'<br/>TRX_ID=100] MY1 -->|原地更新| MY2 MY1 -->|写入 Undo| MY3 end style PG2 fill:#cfc style MY2 fill:#ccf

7.3 读操作的实现差异#

PostgreSQL 读操作

-- PostgreSQL: 直接扫描堆表,找到可见版本
SELECT * FROM users WHERE id = 1;
-- 扫描过程:
-- 1. 找到 id=1 的所有版本(可能有多个)
-- 2. 根据快照判断哪个版本可见
-- 3. 返回可见版本

MySQL 读操作

-- MySQL: 通过 Undo Log 回滚到可见版本
SELECT * FROM users WHERE id = 1;
-- 读取过程:
-- 1. 读取最新版本
-- 2. 检查 TRX_ID 是否在 ReadView 中可见
-- 3. 若不可见,通过回滚指针找 Undo Log
-- 4. 回滚到可见版本

7.4 性能特点对比#

场景PostgreSQL 表现MySQL 表现
大量更新表膨胀,需要 VacuumUndo Log 增长,Purge 回收
长事务阻塞 Vacuum,表膨胀风险高Undo Log 无法清理,空间膨胀
只读查询优秀,无需回滚较好,可能需要回滚
二级索引查询需要回表检查可见性需要回表检查可见性
空间管理需要定期维护相对自动化

八、优缺点权衡分析#

8.1 MVCC 的优势#

mindmap root((MVCC 优势)) 并发性能 读不阻塞写 写不阻塞读 无锁读取 高吞吐量 一致性保证 快照隔离 可重复读 避免脏读 时间旅行查询 实现简洁 无需复杂锁管理 天然支持回滚 MVCC 即事务日志 查询灵活性 历史数据查询 时间点恢复 闪回查询

详细优势分析

优势说明
读性能优异读操作不需要获取锁,不会被写操作阻塞
一致性读每个事务看到一致的数据快照,无需额外锁机制
非阻塞回滚回滚只需标记 xmax,无需恢复操作
时间旅行可以查询历史版本(需要保留旧版本)

8.2 MVCC 的劣势#

flowchart TB subgraph 存储问题 S1[表膨胀] --> S2[需要 VACUUM] S2 --> S3[运维复杂度] end subgraph 写放大问题 W1[UPDATE = INSERT + DELETE] --> W2[索引更新开销] W2 --> W3[WAL 日志增加] end subgraph 事务 ID 问题 T1[32 位事务 ID] --> T2[回卷风险] T2 --> T3[Freeze 操作] T3 --> T4[性能抖动] end subgraph 长事务问题 L1[长事务持有快照] --> L2[阻塞 Vacuum] L2 --> L3[表持续膨胀] L3 --> L4[查询性能下降] end style S1 fill:#fcc style W1 fill:#fcc style T2 fill:#fcc style L1 fill:#fcc

8.3 最佳实践建议#

PostgreSQL MVCC 优化建议

-- 1. 配置合理的 Autovacuum 参数
ALTER SYSTEM SET autovacuum_vacuum_scale_factor = 0.1;
ALTER SYSTEM SET autovacuum_vacuum_threshold = 100;
-- 2. 对高更新表单独配置
ALTER TABLE hot_table SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_vacuum_cost_delay = 2
);
-- 3. 监控表膨胀
SELECT schemaname, relname,
n_live_tup, n_dead_tup,
last_vacuum, last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC;
-- 4. 避免长事务
SELECT pid, now() - pg_stat_activity.query_start AS duration,
query, state
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '5 minutes';

应用层建议

建议原因
避免长事务长事务会阻塞 Vacuum,导致表膨胀
批量操作分批提交减少单次事务产生的大量死元组
定期监控表膨胀及时发现并处理膨胀问题
合理设置填充因子预留空间给 UPDATE,减少页分裂
使用 HOT 更新符合条件时,Heap-Only Tuple 更新更高效

8.4 适用场景分析#

flowchart TB subgraph 适合 MVCC 的场景 A1[读多写少] A2[短事务为主] A3[需要一致性读] A4[高并发查询] end subgraph 需要注意的场景 B1[大量更新操作] B2[长事务频繁] B3[存储空间敏感] B4[延迟敏感型应用] end style A1 fill:#cfc style B1 fill:#ffc

场景适用性评估

场景类型适用度说明
OLTP 读密集型读不阻塞写,性能优异
OLTP 写密集型需要合理配置 Vacuum
OLAP 分析型快照隔离适合分析查询
长事务场景需要特别关注 Vacuum 和膨胀问题

九、总结#

PostgreSQL 选择 MVCC 作为并发控制机制,是基于高并发场景下读写性能的综合考量。

flowchart LR subgraph 问题 P1[读写冲突] P2[锁开销大] P3[性能瓶颈] end subgraph 解决方案 S1[MVCC] S2[多版本] S3[快照隔离] end subgraph 收益 R1[读不阻塞写] R2[无锁读取] R3[高并发性能] end subgraph 代价 C1[存储开销] C2[Vacuum 维护] C3[事务 ID 管理] end P1 --> S1 P2 --> S1 P3 --> S1 S1 --> S2 S1 --> S3 S2 --> R1 S3 --> R2 R1 --> R3 R2 --> R3 S1 --> C1 S1 --> C2 S1 --> C3 style S1 fill:#ccf style R3 fill:#cfc

设计决策总结

因素分析
核心目标实现高并发下的读写不冲突
技术选择多版本 + 快照隔离 + Vacuum 清理
性能收益读性能优异,写不被读阻塞
运维成本需要 Vacuum 维护,关注表膨胀和事务 ID 回卷
设计哲学以空间换时间,用额外存储换取并发性能

PostgreSQL 的 MVCC 设计体现了经典的工程权衡:没有完美的解决方案,只有最适合特定场景的选择。理解 MVCC 的原理和权衡,才能更好地使用和优化 PostgreSQL 数据库。

参考引用#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为什么 PostgreSQL 使用 MVCC
https://blog.souloss.com/posts/why-the-design/why-postgresql-uses-mvcc/
作者
Souloss
发布于
2024-03-30
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时