MVCC PostgreSQL实现事务和多版本并发控制的精华
原创文章,同步发自作者个人博客,http://www.jasongj.com/sql/mvcc/
PostgreSQL针对ACID的实现机制
事务的实现原理可以解读为RDBMS采取何种技术确保事务的ACID特性。PostgreSQL针对ACID的实现技术如下表所示。
ACID | 实现技术 |
---|---|
原子性(Atomicity) | MVCC |
一致性(Consistency) | 约束(主键、外键等) |
隔离性 | MVCC |
持久性 | WAL |
从上表可以看到,PostgreSQL主要使用MVCC和WAL两项技术实现ACID特性。实际上,MVCC和WAL这两项技术都比较成熟,主流关系型数据库中都有相应的实现,但每个数据库中具体的实现方式往往存在较大的差异。本文将介绍PostgreSQL中的MVCC实现原理。
PostgreSQL中的MVCC原理
事务ID
在PostgreSQL中,每个事务都有一个唯一的事务ID,被称为XID。注意:除了被BEGIN - COMMIT/ROLLBACK包裹的一组语句会被当作一个事务对待外,不显示指定BEGIN - COMMIT/ROLLBACK的单条语句也是一个事务。
数据库中的事务ID递增。可通过txid_current()
函数获取当前事务的ID。
隐藏多版本标记字段
PostgreSQL中,对于每一行数据(称为一个tuple),包含有4个隐藏字段。这四个字段是隐藏的,但可直接访问。
- xmin 在创建(insert)记录(tuple)时,记录此值为插入tuple的事务ID
- xmax 默认值为0.在删除tuple时,记录此值
- cmin和cmax 标识在同一个事务中多个语句命令的序列值,从0开始,用于同一个事务中实现版本可见性判断
下面通过实验具体看看这些标记如何工作。在此之前,先创建测试表
CREATE TABLE test
(
id INTEGER,
value TEXT
);
开启一个事务,查询当前事务ID(值为3277),并插入一条数据,xmin为3277,与当前事务ID相等。符合上文所述——插入tuple时记录xmin,记录未被删除时xmax为0
postgres=> BEGIN;
BEGIN
postgres=> SELECT TXID_CURRENT();
txid_current
--------------
3277
(1 row)
postgres=> INSERT INTO test VALUES(1, 'a');
INSERT 0 1
postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
1 | a | 3277 | 0 | 0 | 0
(1 row)
继续通过一条语句插入2条记录,xmin仍然为当前事务ID,即3277,xmax仍然为0,同时cmin和cmax为1,符合上文所述cmin/cmax在事务内随着所执行的语句递增。虽然此步骤插入了两条数据,但因为是在同一条语句中插入,故其cmin/cmax都为1,在上一条语句的基础上加一。
INSERT INTO test VALUES(2, 'b'), (3, 'c');
INSERT 0 2
postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
1 | a | 3277 | 0 | 0 | 0
2 | b | 3277 | 0 | 1 | 1
3 | c | 3277 | 0 | 1 | 1
(3 rows)
将id为1的记录的value字段更新为'd',其xmin和xmax均未变,而cmin和cmax变为2,在上一条语句的基础之上增加一。此时提交事务。
UPDATE test SET value = 'd' WHERE id = 1;
UPDATE 1
postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
2 | b | 3277 | 0 | 1 | 1
3 | c | 3277 | 0 | 1 | 1
1 | d | 3277 | 0 | 2 | 2
(3 rows)
postgres=> COMMIT;
COMMIT
开启一个新事务,通过2条语句分别插入2条id为4和5的tuple。
BEGIN;
BEGIN
postgres=> INSERT INTO test VALUES (4, 'x');
INSERT 0 1
postgres=> INSERT INTO test VALUES (5, 'y');
INSERT 0 1
postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
2 | b | 3277 | 0 | 1 | 1
3 | c | 3277 | 0 | 1 | 1
1 | d | 3277 | 0 | 2 | 2
4 | x | 3278 | 0 | 0 | 0
5 | y | 3278 | 0 | 1 | 1
(5 rows)
此时,将id为2的tuple的value更新为'e',其对应的cmin/cmax被设置为2,且其xmin被设置为当前事务ID,即3278
UPDATE test SET value = 'e' WHERE id = 2;
UPDATE 1
postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
3 | c | 3277 | 0 | 1 | 1
1 | d | 3277 | 0 | 2 | 2
4 | x | 3278 | 0 | 0 | 0
5 | y | 3278 | 0 | 1 | 1
2 | e | 3278 | 0 | 2 | 2
在另外一个窗口中开启一个事务,可以发现id为2的tuple,xin仍然为3277,但其xmax被设置为3278,而cmin和cmax均为2。符合上文所述——若tuple被删除,则xmax被设置为删除tuple的事务的ID。
BEGIN;
BEGIN
postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
2 | b | 3277 | 3278 | 2 | 2
3 | c | 3277 | 0 | 1 | 1
1 | d | 3277 | 0 | 2 | 2
(3 rows)
这里有几点要注意
- 新旧窗口中id为2的tuple对应的value和xmin、xmax、cmin/cmax均不相同,实际上它们是该tuple的2个不同版本
- 在旧窗口中,更新之前,数据的顺序是2,3,1,4,5,更新后变为3,1,4,5,2。因为在PostgreSQL中更新实际上是将旧tuple标记为删除,并插入更新后的新数据,所以更新后id为2的tuple从原来最前面变成了最后面
- 在新窗口中,id为2的tuple仍然如旧窗口中更新之前一样,排在最前面。这是因为旧窗口中的事务未提交,更新对新窗口不可见,新窗口看到的仍然是旧版本的数据
提交旧窗口中的事务后,新旧窗口中看到数据完全一致——id为2的tuple排在了最后,xmin变为3278,xmax为0,cmin/cmax为2。前文定义中,xmin是tuple创建时的事务ID,并没有提及更新的事务ID,但因为PostgreSQL的更新操作并非真正更新数据,而是将旧数据标记为删除,并插入新数据,所以“更新的事务ID”也就是“创建记录的事务ID”。
SELECT *, xmin, xmax, cmin, cmax FROM test;
id | value | xmin | xmax | cmin | cmax
----+-------+------+------+------+------
3 | c | 3277 | 0 | 1 | 1
1 | d | 3277 | 0 | 2 | 2
4 | x | 3278 | 0 | 0 | 0
5 | y | 3278 | 0 | 1 | 1
2 | e | 3278 | 0 | 2 | 2
(5 rows)
MVCC保证原子性
原子性(Atomicity)指得是一个事务是一个不可分割的工作单位,事务中包括的所有操作要么都做,要么都不做。
对于插入操作,PostgreSQL会将当前事务ID存于xmin中。对于删除操作,其事务ID会存于xmax中。对于更新操作,PostgreSQL会将当前事务ID存于旧数据的xmax中,并存于新数据的xin中。换句话说,事务对增、删和改所操作的数据上都留有其事务ID,可以很方便的提交该批操作或者完全撤销操作,从而实现了事务的原子性。
MVCC保证事物的隔离性
隔离性(Isolation)指一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
标准SQL的事务隔离级别分为如下四个级别
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读(read uncommitted) | 可能 | 可能 | 可能 |
提交读(read committed) | 不可能 | 可能 | 可能 |
可重复读(repeatable read) | 不可能 | 不可能 | 可能 |
串行读(serializable) | 不可能 | 不可能 | 不可能 |
从上表中可以看出,从未提交读到串行读,要求越来越严格。
注意,SQL标准规定,具体数据库实现时,对于标准规定不允许发生的,绝不可发生;对于可能发生的,并要不求一定能发生。换句话说,具体数据库实现时,对应的隔离级别只可更严格,不可更宽松。
事实中,PostgreSQL可实现了三种隔离级别——未提交读和提交读实际上都被实现为提交读。
下面将讨论提交读和可重复读的实现方式
MVCC提交读
提交读只可读取其它已提交事务的结果。PostgreSQL中通过pg_clog来记录哪些事务已经被提交,哪些未被提交。具体实现方式将在下一篇文章《SQL优化(七) WAL PostgreSQL实现事务和高并发的重要技术》中讲述。
MVCC可重复读
相对于提交读,重复读要求在同一事务中,前后两次带条件查询所得到的结果集相同。实际中,PostgreSQL的实现更严格,不紧要求可重复读,还不允许出现幻读。它是通过只读取在当前事务开启之前已经提交的数据实现的。结合上文的四个隐藏系统字段来讲,PostgreSQL的可重复读是通过只读取xmin小于当前事务ID且已提交的事务的结果来实现的。
PostgreSQL中的MVCC优势
- 使用MVCC,读操作不会阻塞写,写操作也不会阻塞读,提高了并发访问下的性能
- 事务的回滚可立即完成,无论事务进行了多少操作
- 数据可以进行大量更新,不段像MySQL和Innodb引擎和Oracle那样需要保证回滚段不会被耗尽
PostgreSQL中的MVCC缺点
事务ID个数有限制
事务ID由32位数保存,而事务ID递增,当事务ID用完时,会出现wraparound问题。
PostgreSQL通过VACUUM机制来解决该问题。对于事务ID,PostgreSQL有三个事务ID有特殊意义:
- 0代表invalid事务号
- 1代表bootstrap事务号
- 2代表frozon事务。frozon transaction id比任何事务都要老
可用的有效最小事务ID为3。VACUUM时将所有已提交的事务ID均设置为2,即frozon。之后所有的事务都比frozon事务新,因此VACUUM之前的所有已提交的数据都对之后的事务可见。PostgreSQL通过这种方式实现了事务ID的循环利用。
大量过期数据占用磁盘并降低查询性能
由于上文提到的,PostgreSQL更新数据并非真正更改记录值,而是通过将旧数据标记为删除,再插入新的数据来实现。对于更新或删除频繁的表,会累积大量过期数据,占用大量磁盘,并且由于需要扫描更多数据,使得查询性能降低。
PostgreSQL解决该问题的方式也是VACUUM机制。从释放磁盘的角度,VACUUM分为两种
- VACUUM 该操作并不要求获得排它锁,因此它可以和其它的读写表操作并行进行。同时它只是简单的将dead tuple对应的磁盘空间标记为可用状态,新的数据可以重用这部分磁盘空间。但是这部分磁盘并不会被真正释放,也即不会被交还给操作系统,因此不能被系统中其它程序所使用,并且可能会产生磁盘碎片。
- VACUUM FULL 需要获得排它锁,它通过“标记-复制”的方式将所有有效数据(非dead tuple)复制到新的磁盘文件中,并将原数据文件全部删除,并将未使用的磁盘空间还给操作系统,因此系统中其它进程可使用该空间,并且不会因此产生磁盘碎片。
SQL优化系列
- SQL优化(一) Merge Join vs. Hash Join vs. Nested Loop
- SQL优化(二) 快速计算Distinct Count
- SQL优化(三) PostgreSQL Table Partitioning
- SQL优化(四) Postgre Sql存储过程
- SQL优化(五) PostgreSQL (递归)CTE 通用表表达式
- SQL优化(六) MVCC PostgreSQL实现事务和多版本并发控制的精华
MVCC PostgreSQL实现事务和多版本并发控制的精华的更多相关文章
- 《高性能MySQL》读书笔记之 MySQL锁、事务、多版本并发控制的基础知识
1.2 并发控制 1.2.1 读写锁 在处理并发读或写时,通过实现一个由两种类型的锁组成的锁系统来解决问题.这两种类型的锁通常被称为 共享锁(shared lock) 和 排它锁(exclusive ...
- Mysql 的InnoDB事务方面的 多版本并发控制如何实现 MVCC
Mysql的MVCC不能解决幻读的问题,但是Mysql还有间隙锁功能,Mysql的间隙锁工作在Repeatable Read隔离级别下面,可以防止幻读, 参考:Mysql 间隙锁原理,以及Repeat ...
- [MySQL] MVCC 多版本并发控制实现的事务
1.没有一个统一的实现标准,实现了非阻塞的读操作,写操作也只锁定必要的行2.通过保存数据在某个时间点的快照实现的3.典型的有乐观并发控制和悲观并发控制4.innodb的mvcc是每次事务都有递增的版本 ...
- 数据库原理-事务隔离与多版本并发控制(MVCC)
刚来美团实习,正好是星期天,不得不说,其内部的资料很丰富,看了部分文档后,对数据库事务这块更理解了.数据库事务的ACID,大家都知道,为了维护这些性质,主要是隔离性和一致性,一般使用加锁这种方式.同时 ...
- SQL事务的四种隔离级别和MySQL多版本并发控制
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的那些改变时可见的,那些是不可见的.低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销. ReadUncommitted( ...
- 转: 多版本并发控制(MVCC)在分布式系统中的应用 (from coolshell)
from: http://coolshell.cn/articles/6790.html 问题 最近项目中遇到了一个分布式系统的并发控制问题.该问题可以抽象为:某分布式系统由一个数据中心D和若干业务 ...
- MySQL多版本并发控制(MVCC)
MVCC是行级锁的一个变种,但是它在很多的情况下避免了加锁操作,因此开销更低.MySQL,包括Oracle.PostgreSQL都实现了MVCC,虽然每个关系数据库实现不一样,但大都是实现了非阻塞的读 ...
- MySQL MVCC(多版本并发控制)
概述 为了提高并发MySQL加入了多版本并发控制,它把旧版本记录保存在了共享表空间(undolog),当事务提交之后将重做日志写入磁盘(前提innodb_flush_log_at_trx_commit ...
- mysql的mvcc(多版本并发控制)
mysql的mvcc(多版本并发控制) 我们知道,mysql的innodb采用的是行锁,而且采用了多版本并发控制来提高读操作的性能. 什么是多版本并发控制呢 ?其实就是在每一行记录的后面增加两个隐藏列 ...
随机推荐
- c语言基础表达式, 关系运算符, 逻辑运算符, 位运算符, 数据的取值范围, 分支结构(if...else, switch...case)
1.表达式: 表达式的判断是有无结果(值), 最简单的表达式是一个常量或变量, 如:12, a, 3 + 1, a + b, a + 5 都是表达式 2.BOOL(布尔)数据类型: c语言中除了基本数 ...
- 计算机网络(11)-----TCP连接的建立和释放
TCP连接的建立和释放 概述 TCP运输连接的建立和释放是每一次面向连接的通信中必不可少的过程,运输连接有三个阶段:连接建立,数据传送和连接释放. TCP连接的建立 如图所示,假定A主机是客户端程序, ...
- DIV的不能包住子集解决办法
在div的样式中加上:overflow:hidden,或者float:left或right
- JavaScript 对象的创建
Object类型是JavaScript中使用最多的一种类型.创建Object实例的方式有多种,接下来一一列举. 1. Object构造函数 person1的friends属性修改影响了person2的 ...
- C#按行读取文本并存放再数组内
我只想说真的是日了狗的麻烦,代码就那么几行,但是根本看不懂在搞些什么东西,我现在还是一点都不知道getline函数到底是怎么用的,但是事实就是他确实能用. 期间在那该死的第一个char根本不知道为什么 ...
- JavaScript贷款计算器
今天花了两个小时模仿书上代码用JS制作了JavaScript贷款计算器,时间有些长,但相比以前,自己细心了不少,每天进步一点点,量的积累达到质的飞跃 <!doctype html>< ...
- MFC-01-Chapter01:Hello,MFC---1.3 第一个MFC程序(05)
1.3.4 绘制窗口 如何在屏幕上随心所欲的进行绘制?应用程序通过响应来自Windows的WM_PAINT消息进行绘制的,此消息通知它更新窗口. WM_PAINT消息如何发生:窗口位置改变:窗口大小改 ...
- WPF学习笔记1---初接触
刚刚接触WPF,微软的一套东西.WPF最大的特点就是UI设计与代码逻辑的完全剥离.这样美工和程序员的分工就变得非常清楚.因为界面和程序的耦合度很低,也增加的代码的灵活性和可重用性. 微软为WPF的UI ...
- Three.js入门
一.前段时候花了些功夫研究了下WebGL,了解了基本实体的实现原理和实现方法,现在回忆就只记得如果要我画个圆形,怀疑都要了我的命(那得画多少个三角形...).功夫不负有心人,今天学习Three.js得 ...
- readelf与动态库
使用arm-linux-gcc编译的可执行文件可能会无法在开发板上执行,并提示:-/bin/sh xxx not found 解决办法: 在主机上使用readelf -d xxx 来查看该程序所需要的 ...