一、现象描述
以下问题现象源自Pgfans讨论群老师们的发现,原文如下
pg和oracle事务对比测试:
(一)pg:
--初始化:
create table orasup_test (a int, b varchar(200));
insert into orasup_test values(1,'a'),(2,'b'),(3,'c'),(4,'d');
复制
--session 1:
begin;
delete from orasup_test where a=1;
insert into orasup_test values (1,'aa');
delete from orasup_test where a=2;
insert into orasup_test values (2,'bb');
delete from orasup_test where a=3;
insert into orasup_test values (3,'cc');
复制
--session 2:
begin;
update orasup_test set b='zzz' where a in (1,2,3);
-- hang
复制
--再到session 1的窗口:
commit;
复制
--发现session 2的窗口:
update orasup_test set b='zzz' where a in (1,2,3);
UPDATE 0
复制
(二)Oracle:
--初始化:
create table orasup_test (a number, b varchar(200));
insert into orasup_test values(1,'a');
insert into orasup_test values(2,'b');
insert into orasup_test values(3,'c');
insert into orasup_test values(4,'d');
commit;
复制
--session 1:
delete from orasup_test where a=1;
insert into orasup_test values (1,'aa');
delete from orasup_test where a=2;
insert into orasup_test values (2,'bb');
delete from orasup_test where a=3;
insert into orasup_test values (3,'cc');
复制
--session 2:
update orasup_test set b='zzz' where a in (1,2,3);
-- hang
复制
--再到session 1的窗口:
commit;
复制
--发现session 2的窗口:
update orasup_test set b='zzz' where a in (1,2,3);
3 rows updated
复制
(三)问题:为什么同样的事务隔离级别(都是RC),同样的操作,为什么pg的session2 只更新0行,而oracle的session2 可以更新3行?
二、问题解读及相关知识准备
问题解读
要解答Jimmy小何老师上面的问题,其实就是要解答下面这个问题:
事务A要update更新数据,刚好另外一个事务B拥有了数据的锁资源,事务A进入等待状态。
问题的重点是:
当事务A等到自己获取到对应的锁资源要更新数据的时候,此时事务A读到的值是什么呢?
问题可以简单的概括为:此时update语句使用的事务快照是什么?
什么是事务快照?
快照可以简单总结为事务在某个时刻的归档,作用是用来定义某个事务在执行的过程中“我能看到哪些数据行”。
关于事务快照,详细可以参考文章:
PostgreSQL中同样是别人提交的数据,为什么你能看到,我却看不到?
相关知识准备
下面这些知识,之前的文章也学习整理过一些,直接放些简要上来。
1.Oracle和PostgreSQL的MVCC对比
简单概括来说,PostgreSQL和Oracle在MVCC的实现上存在以下主要区别:
Oracle:基于SCN,块级别,循环undo segment实现,支持闪回功能,存在大事务回滚、快照过旧ORA-01555问题。
PostgreSQL:基于事务编号txid,行级别,无需undo,不支持闪回,大事务可瞬间回滚,存在数据块(data page)空间回收及性能消耗问题。
注意:以上概括基于当前Oracle 19C和PostgreSQL -12.0版本。
更多信息,可阅读文章:
从Oracle到PG系列-PostgreSQL多版本控制MVCC简介
2.一致性读和当前读
一致性读(consistent read):即常规的select SQL语句。
当前读(current read):即insert、update、delete等dml语句,还有就是select···for update、lock in share mode这类读语句。
值得一提的是,当前读的SQL每次都要获取数据当前的最新值,因此可能会被阻塞!一致性读则要满足MVC多版本,通过undo或其他机制实现,数据版本中通过pointer指针指向每个事务所对应的值。
这里顺便题外话一下:
引入了多版本,也就引入了“活跃事务”这个概念,尤其是在Oracle和MySQL的undo机制下,由于旧版本相关的事务还没结束,所以默认情况下,该undo只能在没有事务引用的时候才能被删除。而不再有事务使用,是指当前活跃事务中没有比回滚段相关联事务编号的更小。研究过ORA-01555的同学应该比较熟悉一些。
三、测试演绎及分析
初步测试
明白了上述的这些相关知识,前面的问题就容易理解多了。
我这里只测试两条数据,效果是一样的。
--session 1:开始事务删除并插入
begin;
delete from aken01 where id=1;
insert into aken01 values (1,'new');
delete from aken01 where id=2;
insert into aken01 values (2,'new');
复制
--session 2:发起更新,被挂起。
begin;
update aken01 set name='test' where id in (1,2);
-- hang
复制
--再到session 1的窗口:提交事务。
commit;
复制
--发现session 2的窗口:
(postgres@[local]:5436)[akendb]#update aken01 set name='old' where id in (1,2);
UPDATE 0 <<<这里成功更新0行
(postgres@[local]:5436)[akendb]#update aken01 set name='test' where id in (1,2);
UPDATE 2 <<< “只因在人群中多看了你一眼·······”
(postgres@[local]:5436)[akendb]#commit;
COMMIT
(postgres@[local]:5436)[akendb]#
复制
这里的测试只在session 2中比pgfans群里原测试场景多做了一点点:
就是在session 2的事务里面update 0之后再执行一次update。
看到这里,你是否体会到了什么新的东西哈?
测试的具体过程及分析
--测试数据初始化:
(postgres@[local]:5436)[akendb]#select xmin,xmax,cmin,cmax,* from aken01;
xmin | xmax | cmin | cmax | id | name
------+------+------+------+----+------
530 | 0 | 0 | 0 | 1 | old
530 | 0 | 0 | 0 | 2 | old
(2 rows)
复制
--T1时刻-session 1发起事务txid=535,删除初始数据,并插入新数据。
(postgres@[local]:5436)[akendb]#begin;
BEGIN
(postgres@[local]:5436)[akendb]#select clock_timestamp();
clock_timestamp
------------------------------
2020-05-14 16:27:25.34762+08
(1 row)
(postgres@[local]:5436)[akendb]#select txid_current_if_assigned(); --当前未分配txid
txid_current_if_assigned
--------------------------
(1 row)
(postgres@[local]:5436)[akendb]#select txid_current(); --给当前事务分配一个事务id
txid_current
--------------
535
(1 row)
(postgres@[local]:5436)[akendb]#select txid_current_if_assigned(); --已分配txid
txid_current_if_assigned
--------------------------
535
(1 row)
(postgres@[local]:5436)[akendb]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
535:535:
(1 row)
(postgres@[local]:5436)[akendb]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
535:535:
(1 row)
(postgres@[local]:5436)[akendb]#select xmin,xmax,cmin,cmax,* from aken01;
xmin | xmax | cmin | cmax | id | name
------+------+------+------+----+------
534 | 0 | 0 | 0 | 1 | old
534 | 0 | 0 | 0 | 2 | old
(2 rows)
(postgres@[local]:5436)[akendb]#select clock_timestamp();
clock_timestamp
-------------------------------
2020-05-14 16:28:46.724072+08
(1 row)
(postgres@[local]:5436)[akendb]#delete from aken01 where id=1;
DELETE 1
(postgres@[local]:5436)[akendb]#insert into aken01 values (1,'new');
INSERT 0 1
(postgres@[local]:5436)[akendb]#delete from aken01 where id=2;
DELETE 1
(postgres@[local]:5436)[akendb]#insert into aken01 values (2,'new');
INSERT 0 1
(postgres@[local]:5436)[akendb]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
535:535:
(1 row)
(postgres@[local]:5436)[akendb]#select xmin,xmax,cmin,cmax,* from aken01;
xmin | xmax | cmin | cmax | id | name
------+------+------+------+----+------
535 | 0 | 1 | 1 | 1 | new
535 | 0 | 3 | 3 | 2 | new
(2 rows)
(postgres@[local]:5436)[akendb]#select clock_timestamp();
clock_timestamp
-------------------------------
2020-05-14 16:29:27.177072+08
(1 row)
复制
--T2时刻-session 2:发起事务txid=536,尝试update字段值name='new'更新为name='test',语句挂起。
(postgres@[local]:5436)[akendb]#begin;
BEGIN
(postgres@[local]:5436)[akendb]#select clock_timestamp();
clock_timestamp
-------------------------------
2020-05-14 16:30:28.365332+08
(1 row)
(postgres@[local]:5436)[akendb]#select txid_current_if_assigned();
txid_current_if_assigned
--------------------------
(1 row)
(postgres@[local]:5436)[akendb]#select txid_current();
txid_current
--------------
536
(1 row)
(postgres@[local]:5436)[akendb]#select txid_current_if_assigned();
txid_current_if_assigned
--------------------------
536
(1 row)
(postgres@[local]:5436)[akendb]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
535:535:
(1 row)
(postgres@[local]:5436)[akendb]#select xmin,xmax,cmin,cmax,* from aken01;
xmin | xmax | cmin | cmax | id | name
------+------+------+------+----+------
534 | 535 | 0 | 0 | 1 | old
534 | 535 | 2 | 2 | 2 | old
(2 rows)
(postgres@[local]:5436)[akendb]#update aken01 set name='test' where id in (1,2);
--hang
复制
注意到:
1.此时session2在快照“535:535:”中查看到的是session 1中txid=535已经删除(未提交)的tuple,xmin=534,xmax=535说明tuple对事务txid=535可见但属于invalid无效的数据行;
2.另外,由于txid=535中insert进来的name='new'的这个tuple版本,xmin=535 and t_xmin_status = 'IN_PROGRESS' and (t_xmin(535) <> current_txid(536)),所以,该name='new'版本对于session 2中txid=536来说,是invisible不可见的。
更多可见性判断规则:
个人公众号文章:
Postgresql技术内幕:
http://www.interdb.jp/pg/pgsql08.html#_8.2.
--T3时刻-session 1 提交事务
(postgres@[local]:5436)[akendb]#select clock_timestamp();
clock_timestamp
-------------------------------
2020-05-14 16:32:00.038643+08
(1 row)
(postgres@[local]:5436)[akendb]#commit;
COMMIT
(postgres@[local]:5436)[akendb]#
复制
--T4时刻-session 2 执行update,更新0行,在原事务继续发起第二次update,成功update更新2行数据。
(postgres@[local]:5436)[akendb]#update aken01 set name='test' where id in (1,2);
UPDATE 0
(postgres@[local]:5436)[akendb]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
536:536: <<< 此时txid=536的快照得到更新,变为“536:536:”,该快照可成功获取session 1中txid=535新插入的数据。
(1 row)
(postgres@[local]:5436)[akendb]#select xmin,xmax,cmin,cmax,* from aken01;
xmin | xmax | cmin | cmax | id | name
------+------+------+------+----+------
535 | 0 | 1 | 1 | 1 | new
535 | 0 | 3 | 3 | 2 | new
(2 rows)
(postgres@[local]:5436)[akendb]#select clock_timestamp();
clock_timestamp
-------------------------------
2020-05-14 16:33:06.741677+08
(1 row)
(postgres@[local]:5436)[akendb]#select txid_current_if_assigned();
txid_current_if_assigned
--------------------------
536 <<< session2还在txid=536中
(1 row)
(postgres@[local]:5436)[akendb]#update aken01 set name='test' where id in (1,2);
UPDATE 2 <<< 在当前快照“536:536:”中成功update更新name='new'为name='test'
(postgres@[local]:5436)[akendb]#
复制
细心的同学可能已经看出:事务txid=536前后两次update执行结果之所以不同的原因。
第一次update更新0行,是因为session1中事务txid=535不是马上提交的,在session2的txid=536开启之后,txid=535依旧处于active状态,在txid=535提交前,事务txid=536的更新语句就已经先发起了。
这里需要注意的是,虽然事务txid=535还没提交,但是 (1,new) 这个版本也已经生成了,并且是当前最新版本。那么,txid=536更新语句会怎么处理呢?
答案很简单:
由于txid=535中insert进来的(1,new),(2,new)这两个版本中,tuple对应的xmin=535 and t_xmin_status = 'IN_PROGRESS' and (t_xmin(535) <> current_txid(536)),所以版本对于session 2中txid=536当前“535:535:”快照来说,是invisible不可见的。
另外,旧版本(1,old),(2,old)在session1的txid=535已经删除但未提交,xmin=534,xmax=535,t_xmin_status = 'IN_PROGRESS',说明tuple对事务txid=535可见,但属于invalid无效的数据行。
因此,在当前事务快照“535:535:”中,无有效的tuple版本可供update使用,update为0.这应该也算得上是PostgreSQL避免更新丢失的一个手法。
而第二次更新,由于PostgreSQL在read committed级别下,会在每个SQL开始时候自动获取一次事务快照信息,此时等同于事务txid=535已经提交后的第一次获取快照,自然是可以成功更新到最新的数据的。
好了,问题来了,那么为什么Oracle可以直接更新2两行数据呢?
这时候,我们前面提到的当前读就要上场了。在Oracle中,session 2中的update事务属于当前读,必须要读最新版本,而且必须加锁,因此就被锁住了,必须等到session 1事务释放这个锁,才能继续它的当前读。
当session 2的事务它要去更新数据的时候,就不能再在undo的历史版本上更新了,否则session1中事务的更新就丢失了。
因此,Oracle中session 2事务此时的update是在name='new'的基础上进行的操作。这里涉及这样的一个规则:update操作其实在更新数据的时候都是先读后写的,而这个读,只能读当前最新的版本,称为“当前读”(current read)。
因此,Oracle在更新的时候,当前读拿到的数据是 (1,new),(2,new)的数据版本,update更新后生成了新版本的数据 (1,test),(2,test)。
四、归纳总结
在read committed事务隔离级别下:
1.PostgreSQL会在事务中每个SQL开始的时候获取基于行的快照,如果本次快照中tuple的版本对当前是invalid无效,则update的结果为0,因为没有有效的tuple可供更新。
2.Oracle同样在事务中每个statement开始的时候获取当前语句scn,并在语句执行过程中通过比对基于block的scn来判断当前block的数据版本对当前statement是否可取,当block的scn比statement发起时的scn较新,由于update属于当前读,直接更新当前block里面对应的row值即可。
3.如果是select语句,Oracle则需要到undo数据镜像中寻找合适的数据版本(倒数第一个scn比statement scn小的block)以实现consistent read,典型的问题ORA-01555现象。
4. MySQL中RC级别一致性视图read-view的检索过程和Oracle检索scn的过程大概相同,上面的实验在MySQL中和Oracle保持一致。
5.抛一个问题:PostgreSQL就没有current read这么一说吗?
最近小编较忙,有一段时间没学习更新了,欢迎多多批评~~~~~~
I Love PG 复制
关于我们
中国开源软件推进联盟PostgreSQL分会(简称:中国PG分会)于2017年成立,由国内多家PostgreSQL生态企业所共同发起,业务上接受工信部中国电子信息产业发展研究院指导。中国PG分会是一个非盈利行业协会组织。我们致力于在中国构建PostgreSQL产业生态,推动PostgreSQL产学研用发展。
技术文章精彩回顾 PostgreSQL学习的九层宝塔 PostgreSQL职业发展与学习攻略 搞懂PostgreSQL数据库透明数据加密之加密算法介绍 一文读懂PostgreSQL-12分区表 一文搞懂PostgreSQL物化视图 PostgreSQL源码学习之:RegularLock Postgresql源码学习之词法和语法分析 2019,年度数据库舍 PostgreSQL 其谁? Postgres是最好的开源软件 PostgreSQL是世界上最好的数据库 从Oracle迁移到PostgreSQL的十大理由 从“非主流”到“潮流”,开源早已值得拥有 PG活动精彩回顾 创建PG全球生态!PostgresConf.CN2019大会盛大召开 首站起航!2019“让PG‘象’前行”上海站成功举行 走进蓉城丨2019“让PG‘象’前行”成都站成功举行 中国PG象牙塔计划发布,首批合作高校授牌仪式在天津举行 群英论道聚北京,共话PostgreSQL 相聚巴厘岛| PG Conf.Asia 2019 DAY0、DAY1简报 相知巴厘岛| PG Conf.Asia 2019 DAY2简报 独家|硅谷Postgres大会简报 PostgreSQL线上沙龙第一期精彩回顾 PostgreSQL线上沙龙第二期精彩回顾 PostgreSQL线上沙龙第三期精彩回顾 PostgreSQL线上沙龙第四期精彩回顾 PostgreSQL线上沙龙第五期精彩回顾 PostgreSQL线上沙龙第六期精彩回顾 PG培训认证精彩回顾 中国首批PGCA认证考试圆满结束,203位考生成功获得认证! 中国第二批PGCA认证考试圆满结束,115位考生喜获认证! 重要通知:三方共建,中国PostgreSQL认证权威升级! 近500人参与!首次PGCE中级、第三批次PGCA初级认证考试落幕! 通知:PostgreSQL技术能力电子证书上线! 2020年首批 | 中国PostgreSQL初级认证考试圆满结束
复制