PG的并发控制

PG的并发控制


MVCC并发控制
MVCC
S2PL
OCC
PCC
基于锁定的并发控制
X锁
Exlusive Locks排他锁
事务A锁定了T表,可以进行访问和修改
事务B不能对T表加锁,也不能进行访问和修改
S锁
Share locks共享锁
事务A获取了T表共享锁,事务A可以对对象T进行访问
事务B获取T表的共享锁

​ Adivsory lock

PG的锁控制–PCC

1 PostgreSQL中的多版本并发控制-MVCC

MVCC , Multi - Version Concurrency Control , 多版本控制并发

1.1 为什么需要MVCC

数据库在并发操作下,如果数据正在写,而用户又在读,可能会出现数据不一致的问题,比如一行数据只写入了前半部分,后半部分还没有写入,而此时用户读取这行数据时就会出现前半部分是新数据,后半部分是旧数据的现象,造成前后数据不一致问题,解决这个问题最好的方法就是读写加锁,写的时候不允许读,读的时候不允许写,不过这样就降低了数据库的并发性能,因此便引入了MVCC的概念,它的目的便是实现读写事务相互不阻塞,从而提高数据库的并发性能。

1.2 不同的MVCC机制

实现MVCC的机制有两种:

1、写入数据时,把旧版本数据移到其他地方,如回滚等操作,在回滚中把数据读出来。

2、写入数据库时,保留旧版本的数据,并插入新数据

像oracle数据库使用的是第一种方式,postgresql使用的是第二种方式。

1.3 MVCC 设计的几个概念

HeapTupleHeaderData布局

类型 长度 描述
t_xmin TransactionId 4 bytes 插入XID标志
t_xmax TransactionId 4 bytes 删除XID标志
t_cid CommandId 4 bytes 插入和/或删除CID标 志(覆盖t_xvac)
t_xvac TransactionId 4 bytes VACUUM操作移动一个 行版本的XID
类型 长度 描述
t_ctid ItemPointerData 6 bytes 当前版本的TID或者指 向更新的行版本
t_infomask2 uint16 2 bytes 一些属性,加上多个 标志位
t_infomask uint16 2 bytes 多个标志位
t_hoff uint8 1 byte 到用户数据的偏移量

cmin和cmax插入和删除元组的事务序列标识
Xmin和xmax事务对其他事务的可见性

1、事务ID

在postgresql中,每个事务都存在一个唯一的ID,也称为xid,可通过txid_current()函数获取当前的事务ID,取值大小为42亿,逻辑上无限
txd视为一个环形状标识
tid不会在BEGN;开始分配,只有在一个BEGN中,执行第一条命令的时候,事务管理器才会分配id,并启动一个事务

txid保留的三个tid
0标识无效的tid
1标识启动的tid( initdb)
2标识冻结的xid( freeze)
对于td=1382大于1382的tid,属于未来,小于td=1382,是属于过去的
属于未来的d对于wd=1382的事务是不可见的
属于过去的d对于tid=1382的事务是可见的
txid逻辑上无限
4B42亿

2、tupe

每一行数据,称为一行元祖,一个tupe

3、ctid

tuple中的隐藏字段,代表tuple的物理位置

4、xmin

tuple 中的隐藏字段,在创建一个tuple时,记录此值为当前的事务ID

5、xmax

tuple 中的隐藏字段,默认为0,在删除时,记录此值为当前的事务的ID

6、cmin/cmax

tuple中的隐藏字段,表示同一个事务中多个语句的顺序,从0开始

1.4 MVCC的工作机制

Postgresql中的MVCC就是通过以上几个隐藏字段协作同实现的,下面举几个例子来看下工作机制:

1.4.1 插入数据实例

1、首先我们开启事务插入一条数据,其中ctid代表数据的物理位置,xmin为当前事务ID,xmax为0

postgre=# create table test(id int,name varchar(50));
CREATE TABLE
postgre=#  begin transaction;
BEGIN
postgre=# select txid_current();
 txid_current
--------------
          587
(1 row)

postgre=#  insert into test(id,name) values(1,'a');
INSERT 0 1
postgre=#  insert into test(id,name) values(2,'b');
INSERT 0 1
postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,1) |  587 |    0 |    0 |    0 |  1 | a
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
(2 rows)

postgre=# commit
postgre-# ;
COMMIT
postgre=#
postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,1) |  587 |    0 |    0 |    0 |  1 | a
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
(2 rows)

postgre=#

继续在上一个事务中再插入一条数据,因为在同一个事务中,可以看到cmin,cmax按顺序增长

1.4.2 修改数据实例

修改ID为1的数据name为d,此时ID为1的ctid变为了(0,4),同时开启另外一个窗口,可以看到ID为1的xmax标识为修改数据时的事务ID,既代表词条tuple已删除。

– 第一个窗口

postgre=#  insert into test(id,name) values(3,'c');
INSERT 0 1
postgre=# begin transaction;
BEGIN
postgre=# select txid_current();
 txid_current
--------------
          589
(1 row)

postgre=#  update test set name = 'd' where id ='1';
UPDATE 1
postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
 (0,3) |  588 |    0 |    0 |    0 |  3 | c
 (0,4) |  589 |    0 |    0 |    0 |  1 | d
(3 rows)

– 第二个窗口

postgre=# begin transaction;
BEGIN
postgre=# select txid_current();
 txid_current
--------------
          590
(1 row)

postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,1) |  587 |  589 |    0 |    0 |  1 | a
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
 (0,3) |  588 |    0 |    0 |    0 |  3 | c
(3 rows)

postgre=#

在commit之前,看到的ID1的值还是a,虽然当前590>587,589;但是589没有提交,只是在xmax上变成了589说明修改或者delete过了,

第一个窗口commit后在第二个窗口查询显示,提交之后就可以看到了过去的变化情况了。

postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
 (0,3) |  588 |    0 |    0 |    0 |  3 | c
 (0,4) |  589 |    0 |    0 |    0 |  1 | d
(3 rows)

postgre=#

1.4.3 删除数据实例

删除ID为1的数据,另开启一个窗口,可以看到ID为1的xmax为删除操作的事务ID,代表此条tuple删除。

– 第一个窗口操作如下

postgre=# begin transaction;
BEGIN
postgre=#  select txid_current();
 txid_current
--------------
          591
(1 row)

postgre=# delete from test where id = 1;
DELETE 1
postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
 (0,3) |  588 |    0 |    0 |    0 |  3 | c
(2 rows)

– 第二个窗口操作如下

postgre=#  begin transaction;
BEGIN
postgre=# select txid_current();
 txid_current
--------------
          592
(1 row)

postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
 (0,3) |  588 |    0 |    0 |    0 |  3 | c
 (0,4) |  589 |  591 |    0 |    0 |  1 | d
(3 rows)

postgre=#

– 第一个窗口提交事务,第二个不提交事务,查看第二个窗口的数据信息
– 第一个窗口操作
postgres=# commit;
COMMIT
– 查看第二个窗口信息

postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
 (0,3) |  588 |    0 |    0 |    0 |  3 | c
(2 rows)

postgre=#

1.4.4 数据操作总结来说

1、数据文件中同一逻辑行存在多个版本

2、每个版本通过隐藏字段记录着它的创建事务的ID,删除事务ID等信息

3、通过一定的逻辑保证每个事务能够看到一个特定的版本

读写事务工作在不同的版本上,以保证读写不冲突。

1.5 快照

15.1、快照模型

快照,顾名思义表示某个时间点的状态.在日常生活中,我们使用相机拍摄,拍下来的画面可以理解为拍摄对象的快照;与此类似,连接数据库执行操作(如查询)时数据库的状态也可以视为快照.
为了方便讨论,假定数据库事务隔离级别为READ COMMITTED,考虑以下场景:
在这里插入图片描述

假设在时间点T4执行操作OP,在T4获取数据库状态快照:
其中,TR1/TR4为已完结事务,TR2/TR3/TR5是正在进行中的事务,TR6是在T4后发生的事务.显然,TR1和TR4事务对数据库的修改对操作OP可见;TR6是未来发生的事务,对操作OP不可见;TR2/TR3/TR5正在进行中,同样也不可见.
就此场景而言,理论上我们只需要知道”拍摄”快照的时间T4,就可以控制数据库变化的可见性了,简单而言就是:在此时间前完结的事务,可见,否则不可见.

二、PostgreSQL工程实践

按上一小节的介绍,可以看到快照天生具有时间属性,但在PG中,事务号并没有使用时间戳等信息而是使用自然数列(1,2,…,n,…).
考虑以下场景(简单起见,假设无论是只读还是修改事务,均分配事务号):
在这里插入图片描述

XID=315的事务执行操作OP,获取数据库快照,与上一个场景类型类似,TR1和TR4事务执行的数据库修改对操作OP可见;TR6是未来发生的事务,对操作OP不可见;TR2/TR3/TR5正在进行中,同样也不可见.理论上来说,我们只需要知道该查询的事务号(如315)就可以控制可见性了.但在工程实践上,PostgreSQL并没有简单的使用事务号作为快照,而是使用了最后一个已结束的事务号+1(即snapshot中的xmax)作为历史和未来的分界:
1、大于等于xmax的,属于未来事务,不可见;
2、小于xmax的,已结束事务可见,进行中事务不可见。
 出于性能的考虑,为了进一步区分小于xmax的事务,引入了xmin和xip_list:
 1)小于xmin的事务,视为已结束事务,可见;
 2)大于等于xmin小于xmax,进行中的事务,不可见,否则可见。
仍以上述场景为例(详见下图):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GijhiuVE-1589646242705)(D:\SYBASE&informix&DB2\2 Postgresql\3 markdown笔记\体系架构\晟数体系架构7天之旅\5 并发控制.assets\b00f39ad0162aaea.png)]

最后已结束事务为200,最早仍活跃事务为125,则:
xmax = 200 + 1 = 201
xmin = 125
snapshot = 125 : 201 : 140

获取快照
通过函数txid_current_snapshot()可获取当前的快照信息:

11:05:16 (xdb@[local]:5432)testdb=# select txid_current_snapshot(); txid_current_snapshot ----------------------- 2404:2404:(1 row)11:24:11 (xdb@[local]:5432)testdb=# 

输出格式为xmin : xmax : xip_list
其中:
xmin:最早仍活跃的事务ID(以下简称XID),早于此XID的事务要么被提交并可见,要么回滚要么丢弃。
xmax:最后已完结事务(COMMITTED/ABORTED)的事务ID + 1。
xip_list:在”拍摄”快照时仍进行中的事务ID。该列表包含xmin和xmax之间的活动事务ID。
总结一下,简单来说,对于给定的XID:
XID ∈ [1,xmin),过去的事务,对此快照均可见;
XID ∈ [xmin,xmax),不考虑子事务的情况,仍处于IN_PROGRESS状态的,不可见;COMMITED状态,可见;ABORTED状态,不可见;
XID ∈ [xmax,∞),未来的事务,对此快照均不可见;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章