PostgreSQL MVCC机制详解

前言:MVCC是PostgreSQL的“并发灵魂”

在高并发数据库场景中,“读写互斥”曾是制约性能的核心瓶颈:当一个事务写入数据时,所有读事务必须阻塞等待;反之,读事务持有锁时,写事务也只能排队。这种基于“严格二阶段锁(S2PL)”的并发控制,在OLTP(在线事务处理)系统中(读操作占比常超85%),会导致严重的性能损耗。

多版本并发控制(MVCC,Multi-version Concurrency Control)的出现,彻底打破了这一僵局——它通过为数据维护多个历史版本,让读事务访问“老版本”,写事务修改“新版本”,从而实现读写不互斥、写与写串行的高效并发模型。

PostgreSQL的MVCC实现堪称“独树一帜”:与Oracle依赖UNDO段、MySQL依赖undo日志不同,它直接在表的物理存储中维护数据的多个版本(即“元组”),通过事务ID(XID)和元组状态字段的精细管理,无需额外的UNDO空间即可实现一致性读。

一、MVCC基础:“为什么需要多版本”

在深入PostgreSQL细节前,我们先明确MVCC的核心目标和优势,避免陷入“知其然不知其所以然”的误区。

1.1 传统锁机制的痛点

传统数据库采用“读写锁”控制并发:

  • 读锁(S锁):多个事务可同时持有,阻止写锁获取;
  • 写锁(X锁):仅一个事务可持有,阻止其他读写锁获取。

这种机制的问题显而易见:

  • 读多写少场景下,大量读锁会阻塞写事务,导致写入延迟;
  • 长事务持有读锁时,会“卡住”所有后续写操作,引发性能雪崩。
1.2 MVCC的核心思路

MVCC的本质是“用空间换时间”:为每个数据修改操作保留历史版本,读事务访问历史版本,写事务生成新版本,二者互不干扰。具体来说,它解决了三个核心问题:

  1. 读不阻塞写:读事务无需等待写事务提交,直接读老版本;
  2. 写不阻塞读:写事务仅修改新版本,不影响老版本的读取;
  3. 支持多隔离级别:通过版本筛选,轻松实现READ COMMITTED、REPEATABLE READ等隔离级。
1.3 PostgreSQL MVCC的三大差异化特征

与Oracle、MySQL相比,PostgreSQL的MVCC有三个关键区别:

  1. 无UNDO日志:不依赖独立的UNDO段/undo日志,历史版本直接存储在表中;
  2. 元组级多版本:以“元组”(行)为单位维护版本,而非Oracle的“块级”;
  3. 依赖VACUUM:老版本元组不会自动删除,需通过VACUUM清理,否则会导致“高水位上升”和性能下降。

二、PostgreSQL MVCC的底层基石

要理解PostgreSQL的MVCC,必须先掌握其底层的三个核心组件:事务ID(XID)数据页(Page)结构元组(Tuple)字段。这三者共同构成了MVCC的“骨架”。

2.1 事务ID(XID):MVCC的“时间戳”

PostgreSQL为每个事务分配一个唯一的32位无符号整数,称为“事务ID(XID)”。XID不仅是事务的标识,更是判断元组版本“新旧”的核心依据。

2.1.1 XID的分配规则
  • 事务启动时,由PostgreSQL的“事务管理器”分配XID,从1开始递增;
  • 系统事务(如VACUUM、CHECKPOINT)的XID固定为0(InvalidTransactionId)或1(BootstrapTransactionId);
  • 只读事务默认不分配XID(PostgreSQL 9.4+特性),仅在执行写操作时才分配,减少XID消耗。
2.1.2 XID的“绕回”问题(Wraparound)

由于XID是32位整数,最大值为2^32-1(约42亿),当XID耗尽时会“绕回”到0,导致PostgreSQL无法区分“新事务”和“老事务”(例如XID=42亿的事务,绕回后XID=0,会被误认为是系统事务)。

为解决这一问题,PostgreSQL引入了“事务ID冻结(Freeze)”机制:

  • 当数据库中最老的未清理XID接近绕回阈值时(默认是2^31-1,约21亿),PostgreSQL会触发“Freeze”操作;
  • 将老元组的t_xmin(插入事务ID)替换为FrozenTransactionId(值为2),标记为“永久可见”,不再参与XID比较;
  • Freeze操作通常由自动VACUUM触发,无需手动干预。

示例:查看事务ID相关系统视图

-- 查看当前数据库的XID使用情况
SELECT 
  datname, 
  age(datfrozenxid) AS frozen_age,  -- 距离Freeze的XID差值
  age(datminmxid) AS minmxid_age    -- 最小多事务ID的年龄
FROM pg_database WHERE datname = 'postgres';

-- 查看当前活跃事务的XID
SELECT pid, txid_current() AS current_xid, query 
FROM pg_stat_activity WHERE state = 'active';
2.2 数据页(Page):元组的“存储容器”

PostgreSQL中,表的数据以“页(Page)”为单位存储,默认页大小为8KB(可通过shared_buffers等参数调整)。每个Page不仅存储元组,还包含管理MVCC的关键元数据。

2.2.1 Page的核心结构:PageHeaderData

每个Page的开头是固定大小的“页头(PageHeaderData)”,定义在src/include/storage/bufpage.h中,核心字段如下:

字段名 类型 作用
pd_lsn PageXLogRecPtr 该页最后一次修改的WAL日志序列号,用于崩溃恢复
pd_checksum uint16 页的校验和,防止数据损坏
pd_flags uint16 页状态标记(如是否包含已删除元组、是否需要Freeze等)
pd_lower LocationIndex 页内空闲空间的起始偏移量(低于此值为已使用空间)
pd_upper LocationIndex 页内空闲空间的结束偏移量(高于此值为已使用空间)
pd_special LocationIndex 特殊空间的起始偏移量(如索引页的B树结构数据)
pd_prune_xid TransactionId 该页中最老的可清理元组的事务ID,用于VACUUM判断是否需要清理此页
pd_linp ItemIdData[] 行指针数组,每个元素指向一个元组的位置和状态(是Page的“目录”)
2.2.2 行指针(ItemIdData):元组的“索引”

pd_linp是PageHeaderData中最关键的字段之一,它是一个动态数组,每个元素(ItemIdData)对应一个元组,结构如下:

  • lp_off:元组在Page中的偏移量(从Page开头到元组的字节数);
  • lp_len:元组的长度(字节数);
  • lp_flags:元组状态标记(如LP_UNUSED:未使用;LP_NORMAL:正常元组;LP_REDIRECT:重定向元组)。

形象比喻:Page就像一本书,PageHeaderData是“封面+版权页”,pd_linp是“目录”(记录每章的页码和长度),元组是“正文内容”。查询时,PostgreSQL先查“目录”(pd_linp),再根据偏移量找到对应的“正文”(元组)。

2.3 元组(Tuple):MVCC的“最小单元”

元组是PostgreSQL中数据的最小存储单元(对应表中的一行),除了用户定义的字段(如idinfo),每个元组还隐含了一组“系统字段”,其中HeapTupleFields是MVCC的核心。

2.3.1 HeapTupleFields:元组的“身份信息”

HeapTupleFields定义在src/include/access/htup_details.h中,包含4个关键字段,决定了元组的版本和可见性:

字段名 类型 作用
t_xmin TransactionId 插入该元组的事务ID(元组的“出生证明”)
t_xmax TransactionId 删除/更新该元组的事务ID(元组的“死亡证明”),未操作时为0
t_cid CommandId 事务内的命令ID(从0开始递增),标记该元组由事务中的第几个命令生成
t_ctid ItemPointerData 元组的物理位置((页号, 页内偏移)),更新时指向新元组(版本链指针)

关键规则

  • 一个元组的生命周期由t_xmint_xmax决定:t_xmin标识“谁创建了它”,t_xmax标识“谁销毁了它”;
  • t_ctid是版本链的核心:更新时,老元组的t_ctid指向新元组,新元组的t_ctid指向自身,形成“链表”;
  • t_cid用于区分同一事务内的多个命令(如一个事务执行两次UPDATE,生成的元组t_cid分别为0和1)。
2.3.2 元组的可见性状态

根据t_xmint_xmax的取值,元组可分为三种状态:

  1. 活跃状态t_xmax = 0,表示元组未被删除或更新,当前有效;
  2. 过期状态t_xmax ≠ 0t_xmax对应的事务已提交,表示元组已被删除/更新,仅历史版本可见;
  3. 未决状态t_xmax ≠ 0t_xmax对应的事务未提交,表示元组的删除/更新操作尚未完成,可见性待定。

三、PostgreSQL MVCC的核心流程:插入、更新、删除、查询

理解了XID、Page、Tuple的基础后,我们通过“插入-更新-删除-查询”的完整流程,拆解PostgreSQL MVCC的实现细节,结合实操示例让每个步骤可视化。

3.1 插入(INSERT):元组的“诞生”

当执行INSERT语句时,PostgreSQL会为新元组分配物理空间,并设置HeapTupleFields字段,核心步骤如下:

  1. 事务启动,分配XID(如XID=100);
  2. 在目标表的Page中找到空闲空间,创建新元组;
  3. 设置元组的 t_xmin = 100(插入事务ID)、t_xmax = 0(未被销毁)、t_cid = 0(事务内第一个命令)、t_ctid = (当前页号, 偏移量)(指向自身);
  4. 更新Page的pd_linp(行指针),添加新元组的位置信息;
  5. 事务提交后,元组的t_xmin标记为“已提交”,对其他事务可见。

实操示例:INSERT时的元组状态

-- 1. 创建测试表
CREATE TABLE t_mvcc (
  id INT PRIMARY KEY,
  info TEXT
);

-- 2. 开启事务1,插入数据(XID=100,假设当前事务ID为100)
BEGIN;
INSERT INTO t_mvcc VALUES (1, '初始版本');

-- 3. 在事务内查询元组的系统字段
SELECT 
  xmin,  -- 插入事务ID,即100
  xmax,  -- 未删除/更新,为0
  ctid,  -- 元组物理位置,如(0,1)
  id, 
  info 
FROM t_mvcc;

-- 结果如下:
 xmin | xmax | ctid  | id |   info   
------+------+-------+----+----------
  100 |    0 | (0,1) |  1 | 初始版本

-- 4. 提交事务
COMMIT;
3.2 更新(UPDATE):版本链的“形成”

PostgreSQL的UPDATE并非“原地修改”,而是“生成新元组+标记老元组”,核心步骤如下(假设事务XID=101更新上述元组):

  1. 事务启动,分配XID=101;
  2. 找到需要更新的老元组(id=1t_xmin=100t_xmax=0);
  3. 在Page中创建新元组,设置:
    • t_xmin=101(更新事务ID);
    • t_xmax=0(新元组未被销毁);
    • t_cid=0(事务内第一个命令);
    • t_ctid=(新页号, 新偏移量)(如(0,2),指向自身);
  4. 更新老元组的t_xmax=101(标记为“被XID=101销毁”),t_ctid=(0,2)(指向新元组);
  5. 事务提交后,新元组对其他事务可见,老元组变为历史版本。

关键特性:更新操作会导致表中存在多个版本的元组(老元组+新元组),这些元组通过t_ctid形成“版本链”,查询时会遍历链找到可见的版本。

实操示例:UPDATE后的版本链

-- 1. 开启事务2,更新数据(XID=101)
BEGIN;
UPDATE t_mvcc SET info = '第一次更新' WHERE id = 1;

-- 2. 在事务内查询元组(此时可见新元组)
SELECT 
  xmin,  -- 新元组的xmin=101,老元组的xmin=100
  xmax,  -- 新元组xmax=0,老元组xmax=101
  ctid,  -- 新元组(0,2),老元组(0,1)
  id, 
  info 
FROM t_mvcc;

-- 结果如下(默认只显示可见的新元组,需用系统函数查看所有元组):
 xmin | xmax | ctid  | id |     info     
------+------+-------+----+--------------
  101 |    0 | (0,2) |  1 | 第一次更新

-- 3. 用pg_stat_user_tables查看表的元组情况(需先安装pg_stat_statements扩展)
SELECT 
  relname, 
  n_live_tup AS 活跃元组数, 
  n_dead_tup AS 死亡元组数  -- 死亡元组即老版本元组
FROM pg_stat_user_tables WHERE relname = 't_mvcc';

-- 结果如下(此时死亡元组数=1,即老元组):
  relname  | 活跃元组数 | 死亡元组数 
-----------+------------+------------
 t_mvcc    |          1 |          1

-- 4. 提交事务
COMMIT;
3.3 删除(DELETE):元组的“标记”

PostgreSQL的DELETE并非“物理删除”,而是“标记元组为过期”,核心步骤如下(事务XID=102删除元组):

  1. 事务启动,分配XID=102;
  2. 找到目标元组(id=1t_xmin=101t_xmax=0);
  3. 更新元组的t_xmax=102(标记为“被XID=102销毁”);
  4. 事务提交后,元组变为“死亡元组”,仅对未提交的读事务可见;
  5. 后续VACUUM操作会将“死亡元组”的行指针(pd_linp)标记为LP_UNUSED,释放空间供新元组使用(但物理空间不会缩小,除非VACUUM FULL)。

实操示例:DELETE后的元组状态

-- 1. 开启事务3,删除数据(XID=102)
BEGIN;
DELETE FROM t_mvcc WHERE id = 1;

-- 2. 查询元组(此时元组已被标记为死亡,事务内可见,其他事务不可见)
SELECT 
  xmin,  -- 101
  xmax,  -- 102(被删除事务标记)
  ctid,  -- (0,2)
  id, 
  info 
FROM t_mvcc;

-- 结果如下(事务内仍可见,显示死亡元组):
 xmin | xmax | ctid  | id |     info     
------+------+-------+----+--------------
  101 |  102 | (0,2) |  1 | 第一次更新

-- 3. 提交事务
COMMIT;

-- 4. 再次查询(此时元组对所有事务不可见)
SELECT * FROM t_mvcc;  -- 结果为空

-- 5. 查看死亡元组数(此时死亡元组数=2:UPDATE生成的老元组+DELETE标记的元组)
SELECT relname, n_live_tup, n_dead_tup FROM pg_stat_user_tables WHERE relname = 't_mvcc';
-- 结果:
  relname  | 活跃元组数 | 死亡元组数 
-----------+------------+------------
 t_mvcc    |          0 |          2
3.4 查询(SELECT):可见性的“判断逻辑”

查询是MVCC中最复杂的环节——PostgreSQL需要根据“当前事务的快照”,遍历元组的版本链,判断每个元组是否“可见”。核心步骤如下:

3.4.1 第一步:生成事务快照(Snapshot)

当事务执行第一个SELECT时,PostgreSQL会生成一个“快照(Snapshot)”,包含三个关键信息:

  • xmin:快照生成时,所有已提交事务的最大XID(小于xmin的事务均已提交);
  • xmax:快照生成时,下一个待分配的XID(大于等于xmax的事务尚未启动);
  • active_xids:快照生成时,正在运行的活跃事务ID列表。

快照的作用:定义了事务“能看到什么”——仅能看到xmin之前提交的事务生成的元组,无法看到xmax之后启动的事务或active_xids中的活跃事务生成的元组。

3.4.2 第二步:遍历版本链

对于每个查询的元组,PostgreSQL会从最新的元组开始,沿t_ctid遍历版本链,直到找到“可见的元组”或遍历结束(无可见元组则返回空)。

3.4.3 第三步:可见性判断规则

对每个元组,根据其t_xmint_xmax,结合当前快照的xminxmaxactive_xids,判断是否可见,核心规则如下:

  1. 判断元组是否“已出生”t_xmin对应的事务是否已提交;

    • t_xmin < 快照.xmin:已提交,元组“已出生”;
    • t_xmin >= 快照.xmax:未启动,元组“未出生”,不可见;
    • t_xminactive_xids中:活跃事务,元组“未出生”,不可见;
    • 其他情况:t_xmin对应的事务已提交,元组“已出生”。
  2. 判断元组是否“已死亡”t_xmax对应的事务是否已提交;

    • t_xmax = 0:未被销毁,元组“存活”,可见;
    • t_xmax < 快照.xmin:已提交,元组“已死亡”,不可见;
    • t_xmax >= 快照.xmax:未启动,元组“未死亡”,可见;
    • t_xmaxactive_xids中:活跃事务,元组“未死亡”,可见;
    • 其他情况:t_xmax对应的事务已提交,元组“已死亡”,不可见。

简化记忆:可见的元组需满足“已出生(t_xmin提交)且未死亡(t_xmax未提交或未启动)”。

实操示例:多事务并发下的可见性
假设存在三个事务,按以下顺序执行,验证可见性规则:

时间 事务1(XID=103,READ COMMITTED) 事务2(XID=104,UPDATE) 事务3(XID=105,REPEATABLE READ)
T1 BEGIN; SELECT * FROM t_mvcc; (空) - -
T2 - BEGIN; INSERT INTO t_mvcc VALUES (1, ‘并发测试’); -
T3 SELECT * FROM t_mvcc; (空,事务2未提交) - BEGIN; SELECT * FROM t_mvcc; (空)
T4 - COMMIT; -
T5 SELECT * FROM t_mvcc; (看到(1, ‘并发测试’),READ COMMITTED快照更新) - SELECT * FROM t_mvcc; (空,REPEATABLE READ快照复用)

结果分析

  • 事务1(READ COMMITTED):每次SELECT生成新快照,T5时事务2已提交,故可见新元组;
  • 事务3(REPEATABLE READ):仅在第一次SELECT生成快照,T5时快照未更新,故仍看不到新元组;
  • 这就是MVCC如何实现不同隔离级别的核心——通过控制快照的生成时机。

四、PostgreSQL MVCC的“副作用”与解决方案

PostgreSQL的MVCC虽然实现了高效并发,但“元组多版本”也带来了两个核心问题:高水位上升元组/索引膨胀,若不处理会导致性能急剧下降。

4.1 高水位(High Water Mark,HWM)问题

高水位是表的“逻辑边界”,标记了表中已使用过的最大Page位置。PostgreSQL的查询会扫描“高水位以下的所有Page”,即使Page中只有死亡元组。

问题根源

  • UPDATE/DELETE仅标记元组为“死亡”,不会删除Page;
  • 高水位只会上升,不会下降(除非执行VACUUM FULL);
  • 当表中死亡元组占比高时,查询会扫描大量空Page,导致IO性能下降。

示例:高水位导致的性能问题

-- 1. 创建一个包含10万行数据的表
CREATE TABLE t_hwm AS SELECT generate_series(1, 100000) AS id, 'test' AS info;

-- 2. 删除所有数据(仅标记死亡元组,高水位不变)
DELETE FROM t_hwm;

-- 3. 查看表的高水位(通过pg_class的relpages字段,单位:Page数)
SELECT relname, relpages, reltuples 
FROM pg_class WHERE relname = 't_hwm';

-- 结果:relpages=300(假设),reltuples=0(活跃元组为0)
-- 说明:高水位仍在300个Page,查询时会扫描这300个Page,但无数据返回

-- 4. 执行普通VACUUM(清理死亡元组,但不降低高水位)
VACUUM t_hwm;

-- 再次查询relpages:仍为300(高水位未降)

-- 5. 执行VACUUM FULL(降低高水位)
VACUUM FULL t_hwm;

-- 再次查询relpages:降至1(高水位重置)
4.2 元组膨胀与索引膨胀
  • 元组膨胀:表中死亡元组占比过高(通常超过20%),导致表体积增大,查询需遍历更多元组;
  • 索引膨胀:索引会指向所有元组(包括死亡元组),死亡元组未清理时,索引体积增大,查询索引时IO增多。

膨胀的危害

  • 存储占用增加:死亡元组浪费磁盘空间;
  • 查询性能下降:扫描更多元组/索引页,IO延迟增加;
  • 备份恢复变慢:备份数据量增大,恢复时间延长。
4.3 解决方案:VACUUM的工作原理

VACUUM是PostgreSQL清理死亡元组、解决膨胀的核心工具,分为“普通VACUUM”和“VACUUM FULL”两种模式。

4.3.1 普通VACUUM(自动/手动)

核心作用

  1. 清理死亡元组:将死亡元组的行指针(pd_linp)标记为LP_UNUSED,释放空间供新元组复用;
  2. 冻结老元组:将接近绕回阈值的元组t_xmin冻结为FrozenTransactionId,避免XID绕回;
  3. 更新统计信息:更新pg_stat_user_tables等系统视图,为查询优化器提供准确的元组数量。

特点

  • 非阻塞:执行时不锁表,可与读写事务并发;
  • 不降低高水位:仅复用Page内的空闲空间,不缩小表的物理体积;
  • 自动触发:PostgreSQL默认开启autovacuum(自动VACUUM),当死亡元组占比超过autovacuum_vacuum_scale_factor(默认20%)时触发。
4.3.2 VACUUM FULL

核心作用

  1. 彻底清理死亡元组:将活跃元组迁移到表的前端Page,删除后端的空Page;
  2. 降低高水位:重置表的高水位,缩小表的物理体积;
  3. 修复索引膨胀:重建索引,删除指向死亡元组的索引项。

特点

  • 阻塞:执行时会锁表(ACCESS EXCLUSIVE锁),禁止所有读写操作;
  • 耗时较长:需迁移所有活跃元组,大表执行时间可能很长;
  • 慎用场景:生产环境需在业务低峰期执行,避免影响服务。
4.3.3 自动VACUUM配置优化

默认的autovacuum参数可能无法满足高并发场景,需根据业务调整:

-- 查看autovacuum相关参数
SELECT name, setting, unit, short_desc 
FROM pg_settings WHERE name LIKE 'autovacuum%';

-- 常用优化参数(在postgresql.conf中配置)
autovacuum = on  -- 开启自动VACUUM
autovacuum_vacuum_scale_factor = 0.1  -- 死亡元组占比超过10%触发VACUUM
autovacuum_vacuum_threshold = 5000  -- 最少死亡元组数量(避免小表频繁触发)
autovacuum_max_workers = 4  -- 最大自动VACUUM工作线程数
autovacuum_naptime = 1min  -- 自动VACUUM检查间隔(缩短间隔,及时清理)

五、PostgreSQL MVCC的优缺点与实践优化

5.1 优点
  1. 读写并发性能优异:读事务不阻塞写,写事务不阻塞读,适合读多写少的OLTP场景;
  2. 隔离级别实现灵活:通过快照机制,轻松支持READ COMMITTED、REPEATABLE READ、SERIALIZABLE;
  3. 崩溃恢复简单:无需维护UNDO日志,崩溃后仅需通过WAL日志恢复,减少恢复时间。
5.2 缺点
  1. 存储开销大:维护多个元组版本,导致表体积增大,需更多磁盘空间;
  2. 依赖VACUUM:若VACUUM不及时,会导致膨胀和性能下降;
  3. 不适合频繁大量更新场景:如秒杀系统、高频交易系统,频繁更新会导致版本链过长,查询性能下降。
5.3 实践优化建议
5.3.1 表设计层面
  1. 避免大字段频繁更新:将大字段(如TEXT、JSONB)拆分到子表,主表仅存储关键字段,减少更新时的元组体积;
  2. 使用分区表:对大表按时间或业务维度分区,每个分区独立进行VACUUM,降低单表膨胀风险;
  3. 选择合适的主键:使用自增序列(SERIAL/BIGSERIAL)而非UUID,避免主键更新导致的索引膨胀。
5.3.2 事务与SQL层面
  1. 避免长事务:长事务会导致快照长期有效,死亡元组无法清理,建议事务时长控制在秒级;
  2. 批量操作替代循环更新:用UPDATE ... FROM批量更新,减少事务数量和版本链长度;
  3. 合理使用DELETE+INSERT替代UPDATE:对频繁更新的表,可先删除老数据再插入新数据,减少版本链(需注意主键冲突)。
5.3.3 系统参数层面
  1. 调整Page大小:对大表(如超过100GB),将shared_buffers的Page大小从8KB调整为16KB或32KB,减少Page数量和IO;
  2. 优化VACUUM参数:如前文所述,降低autovacuum_vacuum_scale_factor,增加autovacuum_max_workers
  3. 开启并行查询:设置max_parallel_workers_per_gather(默认4),加速大表查询,抵消膨胀带来的性能损耗。

六、总结

PostgreSQL的MVCC是其并发性能的核心支柱,通过“元组多版本+事务ID+快照机制”,在无UNDO日志的情况下实现了高效的读写并行。其核心逻辑可概括为:

  • 写入时生成新元组:INSERT/UPDATE/DELETE不修改老元组,仅生成新元组或标记老元组;
  • 查询时筛选可见元组:基于事务快照,遍历版本链找到“已出生且未死亡”的元组;
  • 清理时依赖VACUUM:通过VACUUM复用空间、冻结XID,避免膨胀和XID绕回。

掌握PostgreSQL的MVCC机制,不仅能帮助你解决生产环境中的性能问题(如膨胀、高水位),更能理解其设计哲学——用“空间换时间”的思路,在并发控制和性能之间找到最佳平衡。在实际应用中,需结合业务场景合理配置VACUUM、优化表设计,才能让MVCC的优势最大化。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐