暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

再论PostgreSQL和Oracle之间RC隔离级别事务操作对比

前言
写这篇文章的想法主要是出于pgfans群里关于PostgreSQL和Oracle之间RC隔离级别的事务对比问题的思考,很感谢liuaustin3静心老师已经对该问题做了解答,详见:POSTGRESQL RC事务处理与ORACLE MYSQL 的区别 --对PGFANS 群里面的问题的分解。
觉得Jimmy小何老师发现的问题比较有意思,所以顺着两位老师的思路,自己也做了一次测试,顺便谈谈个人的看法。
文章目录
一、现象描述
二、问题解读及相关知识准备
三、测试演绎及分析
四、归纳总结

一、现象描述

以下问题现象源自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多版本的可见性判断

                                    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产学研用发展。



                                        欢迎投稿

                                        做你的舞台,show出自己的才华 。

                                        投稿邮箱:partner@postgresqlchina.com

                                                                       

                                                                         ——愿能安放你不羁的灵魂


                                        技术文章精彩回顾




                                        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初级认证考试圆满结束

                                        复制
                                        最后修改时间:2020-05-22 09:17:49
                                        文章转载自开源软件联盟PostgreSQL分会,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                        评论