什么是 MVCC
MVCC,全称 Multi-Version Concurrency Control,即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。PostgreSQL 引入了 MVCC 机制来保证事务的隔离性,实现数据库的隔离级别。
MVCC 的关键特点
-
多版本存储:
-
数据库中同一个数据的不同版本会存储在不同的时间点上,每个版本对应一次修改。
-
每次写操作不会直接覆盖原来的数据,而是生成一个新的版本,旧版本仍然可用。
-
-
快照读取:
-
每个事务在开始时获取一个“时间快照”,基于这个快照读取数据。
-
即使其他事务在进行修改,也不会影响当前事务的读取视图。
-
-
版本控制:
-
每个数据版本带有两个元信息:创建时间戳和删除时间戳。
-
事务在读取数据时,会通过比较时间戳判断当前事务是否能看到某个版本的数据。
-
PostgreSQL 中 MVCC 的具体实现
Tuple 数据的存储结构
struct HeapTupleHeaderData
{
union
{
HeapTupleFields t_heap;
DatumTupleFields t_datum;
} t_choice;
ItemPointerData t_ctid; /* current TID of this or newer tuple (or a
* speculative insertion token) */
/* Fields below here must match MinimalTupleData! */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK2 2
uint16 t_infomask2; /* number of attributes + various flags */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK 3
uint16 t_infomask; /* various flag bits, see below */
#define FIELDNO_HEAPTUPLEHEADERDATA_HOFF 4
uint8 t_hoff; /* sizeof header incl. bitmap, padding */
/* ^ - 23 bytes - ^ */
#define FIELDNO_HEAPTUPLEHEADERDATA_BITS 5
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */
/* MORE DATA FOLLOWS AT END OF STRUCT */
};
typedef struct HeapTupleFields
{
TransactionId t_xmin; /* inserting xact ID */
TransactionId t_xmax; /* deleting or locking xact ID */
union
{
CommandId t_cid; /* inserting or deleting command ID, or both */
TransactionId t_xvac; /* old-style VACUUM FULL xact ID */
} t_field3;
} HeapTupleFields;
从中可看出 tuple 的存储结构:
t_xmin
t_xmax
t_ctid
其它字段
具体数据
其中 MVCC 主要需要用到的字段有 t_xmin, t_xmax, t_ctid。
-
t_xmin 存储的是产生这个元组的事务 ID,可能是 insert 或者 update 语句。
-
t_xmax 存储的是删除或者锁定这个元组的事务 ID
-
t_ctid 存储用来记录当前元组或新元组的物理位置
-
由块号和块内偏移组成
-
如果这个元组被更新,则该字段指向更新后的新元组
-
这个字段指向自己,且后面 t_heap 中的 xmax 字段为空,就说明该元组为最新版本。
-
MVCC 的工作流程
数据库中目前有 A 一条数据:
当事务 T1BEGIN 时事务调度器会分配一个时间戳
当事务 T1 读取 A 时发现 t_xmax=0,t_xmin<TS(T1),于是读取 A1。
当事务 T2BEGIN 是分配时间戳 TS2。
当事务 T2 写入 A 时会新建一个版本 A2,并将 A2 的 t_xmin 和 A1 的 t_xmax 设为 TS2,并让 A1 的 t_ctid 指针指向新版本。
当 T1 进行到 R(A)时查看 A1 发现 t_xmin < TS1 < t_xmax,符合要求,即读取 A 的值为 123。
当 T2 要读取 A 时会先查看 A1,发现 TS2 不在 t_xmin 和 t_xmax 范围中。于是会沿着 t_ctid 指针查看下一版本 A2,发现 A2 的 t_xmax=0,且 A2 的 t_xmin<=TS2 可,于是读取 A2 的值 256。
总结
从上面例子中可看出使用 MVCC 可以解决不可重复读、脏读的问题。此外 PostGreSQL 引入 MVCC 还可以保证事务的隔离性,实现数据库的不同隔离级别,以及提高读写并发性能,并在保证事务隔离性的同时最大化系统吞吐量。这使得 MVCC 成为许多现代数据库的核心机制之一。