Adivsory lock
PG的锁控制–PCC
1 PostgreSQL中的多版本并发控制-MVCC
MVCC , Multi - Version Concurrency Control , 多版本控制并发
数据库在并发操作下,如果数据正在写,而用户又在读,可能会出现数据不一致的问题,比如一行数据只写入了前半部分,后半部分还没有写入,而此时用户读取这行数据时就会出现前半部分是新数据,后半部分是旧数据的现象,造成前后数据不一致问题,解决这个问题最好的方法就是读写加锁,写的时候不允许读,读的时候不允许写,不过这样就降低了数据库的并发性能,因此便引入了MVCC的概念,它的目的便是实现读写事务相互不阻塞,从而提高数据库的并发性能。
实现MVCC的机制有两种:
1、写入数据时,把旧版本数据移到其他地方,如回滚等操作,在回滚中把数据读出来。
2、写入数据库时,保留旧版本的数据,并插入新数据
像oracle数据库使用的是第一种方式,postgresql使用的是第二种方式。
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事务对其他事务的可见性
在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亿
每一行数据,称为一行元祖,一个tupe
tuple中的隐藏字段,代表tuple的物理位置
tuple 中的隐藏字段,在创建一个tuple时,记录此值为当前的事务ID
tuple 中的隐藏字段,默认为0,在删除时,记录此值为当前的事务的ID
tuple中的隐藏字段,表示同一个事务中多个语句的顺序,从0开始
Postgresql中的MVCC就是通过以上几个隐藏字段协作同实现的,下面举几个例子来看下工作机制:
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按顺序增长
修改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=#
删除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、数据文件中同一逻辑行存在多个版本
2、每个版本通过隐藏字段记录着它的创建事务的ID,删除事务ID等信息
3、通过一定的逻辑保证每个事务能够看到一个特定的版本
读写事务工作在不同的版本上,以保证读写不冲突。
快照,顾名思义表示某个时间点的状态.在日常生活中,我们使用相机拍摄,拍下来的画面可以理解为拍摄对象的快照;与此类似,连接数据库执行操作(如查询)时数据库的状态也可以视为快照.
为了方便讨论,假定数据库事务隔离级别为READ COMMITTED,考虑以下场景:
假设在时间点T4执行操作OP,在T4获取数据库状态快照:
其中,TR1/TR4为已完结事务,TR2/TR3/TR5是正在进行中的事务,TR6是在T4后发生的事务.显然,TR1和TR4事务对数据库的修改对操作OP可见;TR6是未来发生的事务,对操作OP不可见;TR2/TR3/TR5正在进行中,同样也不可见.
就此场景而言,理论上我们只需要知道”拍摄”快照的时间T4,就可以控制数据库变化的可见性了,简单而言就是:在此时间前完结的事务,可见,否则不可见.
按上一小节的介绍,可以看到快照天生具有时间属性,但在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,∞),未来的事务,对此快照均不可见;