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

PostgreSQL中repeatable read事务隔离级别为什么不会出现幻读

御数临风 2019-11-05
5179

在查看PostgreSQL-11.5官方文档的时候发现,pg中repeatable read隔离级别下是不会出现幻读的。如下图标红处所示:

PostgreSQL事务隔离级别

什么是幻读? 下面是官方的解析:

phantom read

A transaction re-executes a query returning a set of rows that satisfy a search condition and finds that the set of rows satisfying the condition has changed due to another recently-committed transaction.

大概意思指在一个事务中相同的SQL查询条件前后读取到的结果不一致,原因是后者读取到了其他事务中新提交的数据。

数据读取,实际上读取的是一种状态数据;数据库中所说的“事务”,可以理解为数据在某个时间的一致性状态,每当产生一个新的事务,数据将从上一个事务的状态进入到新事务中的状态。“事务快照”本质上代表的是某个时刻数据状态的一个定格。

一、事务快照介绍

事务快照transaction snapshot,表示某个时刻事务的状态。既然是状态,我在这里对事务快照的理解为三个阶段:一个transaction snapshot将事务划分为过去的、当前的、未来的三个范围区域。

PostgreSQL中事务快照的状态信息包括如下内容:

  • xmin,当前处于active状态的最小事务编号;

  • xmax,未来产生的事务中,第一个被分配的事务编号;

  • xip_list,当前处于active 状态的事务列表。

如下,查看当前时刻事务快照:

    (postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
    txid_current_snapshot
    -----------------------
    639:642:639,640 <<<事务快照格式:xmin:xmax:xip_list
    1. xmin=639,表示当前快照时刻最小的事务是639这个事务。小于该编号的事务都已经终止(提交、回滚或异常终止),这些事务属于“过去的”范围区域。

    2. xmax=642,表示将来新事务产生时分配到的第一个事务编号txid,大于等于642的事务未产生,属于“将来的”范围区域。

    3. xip_list=(639,640),表示该快照时刻639和640这两个事务正处于active状态,属于“当前的”范围区域。


    下面画个图表示:

    transaction snapshot examples

    二、事务快照有什么用?

    一个重要作用就是对某个行记录tuple版本是否可见的重要判断依据。

    pg默认的事务隔离级别transaction isolation为read committed,

    在read committed级别下,session中同一事务的每一条SQL语句执行的时候都会自动读取当前时刻事务快照;而在repeatable read级别下,session中同一事务只会在事务开始的第一个SQL获取一次事务快照。

    这是read committed事务级别下可能产生幻读的原因,也是repeatable read可以实现重复读的原因。

    因为read committed级别下,同一事务中不同时刻的SQL获取的快照可能不一样,因此读到的数据可能会不一样;

    而repeatable read在整个事务周期只获取一次事务快照,所以同一事务内所有SQL读取的快照信息都是一致的,因此可以实现重复读,规避了幻读的产生。

    关于PostgreSQL的事务隔离级别,可查看文章:

    从Oracle到PG-PostgreSQL的事务隔离模式

    三、提交读、可重复读模式下事务快照对比测试

    下面针对read committed和repeatable read两种主要的事务隔离模式下的事务快照进行对比测试,例子如下:


    1.T1时间段:

    • session 1开启事务txid=666,并insert插入tuple 2,获取到的快照为 666:666:,并且在查询结果中能看到tuple2。

    • session 2在read committed隔离模式下开启事务txid=674;

    • session 3在可重复读repeatable read隔离模式下开启事务txid=675;

    • session 4开启事务txid=676。

    1)事务开始前table01中只有一行记录:tuple 1

      (postgres@[local]:5432)[akendb01]#select * from table01;
      id | name
      ----+--------
      1 | aken01
      (1 row)
      (postgres@[local]:5432)[akendb01]#


      2)session 1在默认提交读模式下开启事务txid=666,并insert插入tuple 2

        (postgres@[local]:5432)[akendb01]#begin;
        BEGIN
        (postgres@[local]:5432)[akendb01]#show default_transaction_isolation;
        default_transaction_isolation
        -------------------------------
        read committed
        (1 row)
        (postgres@[local]:5432)[akendb01]#
        (postgres@[local]:5432)[akendb01]#select txid_current();
        txid_current
        --------------
        666
        (1 row)
        (postgres@[local]:5432)[akendb01]#
        (postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
        txid_current_snapshot
        -----------------------
        666:666:
        (1 row)
        (postgres@[local]:5432)[akendb01]#
        (postgres@[local]:5432)[akendb01]#insert into table01 values(2,'aken02');
        INSERT 0 1
        (postgres@[local]:5432)[akendb01]#
        (postgres@[local]:5432)[akendb01]#select id,name, ctid,xmin,xmax from public.table01;
        id | name | ctid | xmin | xmax
        ----+--------+-------+------+------
        1 | aken01 | (0,1) | 663 | 0
        2 | aken02 | (0,2) | 666 | 0
        (2 rows)
        (postgres@[local]:5432)[akendb01]#


        3)session 2:在提交读隔离级别下开启事务txid=674

          (postgres@[local]:5432)[akendb01]#start transaction isolation level read committed;
          START TRANSACTION
          (postgres@[local]:5432)[akendb01]#select txid_current();
          txid_current
          --------------
          674
          (1 row)


          4)session 3:在可重复读隔离级别下开启事务txid=675

            (postgres@[local]:5432)[akendb01]#start transaction isolation level repeatable read;
            START TRANSACTION
            (postgres@[local]:5432)[akendb01]#select txid_current();
            txid_current
            --------------
            675
            (1 row)


            5)session 4:开启事务txid=676

              (postgres@[local]:5432)[akendb01]#begin;
              BEGIN
              (postgres@[local]:5432)[akendb01]#select txid_current();
              txid_current
              --------------
              676
              (1 row)


              2.T2时间段,session 1、2、3对table01执行select语句。

              1)session 1:

              session 1在事务txid=666中获取的事务快照为'666:676:674,675',查看结果中可以看到自己新插入的tuple 2。

                (postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
                txid_current_snapshot
                -----------------------
                666:676:674,675 <<< 实际上txid=676在session 4已经产生,这个和官网将xmax解析为将来产生的第一个事务有矛盾,我的理解是pg获取事务快照时最后一个txid会滞后
                (1 row)
                (postgres@[local]:5432)[akendb01]#select * from table01;
                id | name
                ----+--------
                1 | aken01
                2 | aken02
                (2 rows)
                (postgres@[local]:5432)[akendb01]#


                下图是官网对事务快照函数txid_current_snapshot的原文解析:

                Table 9.75. Snapshot Components

                详细介绍见:https://www.postgresql.org/docs/current/functions-info.html

                2)session 2:

                session 2在事务txid=674中获取的事务快照为'666:676:666,675',查看结果中看不到事务txid=666新插入的tuple 2。

                  (postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
                  txid_current_snapshot
                  -----------------------
                  666:676:666,675
                  (1 row)
                  (postgres@[local]:5432)[akendb01]#
                  (postgres@[local]:5432)[akendb01]#select * from table01;
                  id | name
                  ----+--------
                  1 | aken01
                  (1 rows)
                  (postgres@[local]:5432)[akendb01]#


                  3)session 3:

                  session 3在事务txid=675中获取的事务快照为'666:676:666,674',查看结果中看不到事务txid=666新插入的tuple 2。

                    (postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
                    txid_current_snapshot
                    -----------------------
                    666:676:666,674
                    (1 row)
                    (postgres@[local]:5432)[akendb01]#select * from table01;
                    id | name
                    ----+--------
                    1 | aken01
                    (1 rows)
                    (postgres@[local]:5432)[akendb01]#


                    3.T3时间段:

                    session 1提交,session 1、session 2、session 3对表table01执行select语句。

                    1)session 1:

                      (postgres@[local]:5432)[akendb01]#commit;
                      COMMITTED
                      (postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
                      txid_current_snapshot
                      -----------------------
                      674:676:674,675
                      (1 row)
                      (postgres@[local]:5432)[akendb01]#select * from table01;
                      id | name
                      ----+--------
                      1 | aken01
                      2 | aken02
                      (2 rows)
                      (postgres@[local]:5432)[akendb01]#


                      2)session 2:

                      因为是read committed隔离模式,txid=666提交后从xip_list中消失,事务快照为 674:676:675,查询结果可以看到tuple2,和事务开始时结果不一致,出现幻读。

                        (postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
                        txid_current_snapshot
                        -----------------------
                        674:676:675
                        (1 row)
                        (postgres@[local]:5432)[akendb01]#select * from table01;
                        id | name
                        ----+--------
                        1 | aken01
                        2 | aken02
                        (2 rows)
                        (postgres@[local]:5432)[akendb01]#


                        3)session 3:

                        因为是repeatable read隔离模式,txid=666仍然当成active状态看待,事务快照依旧为 666:674:666,674,看不到tuple2,查询结果和事务开始时保持一致,无幻读现象发生。

                          (postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
                          txid_current_snapshot
                          -----------------------
                          666:676:666,674 <<<session 1的事务666即使已经成功提交,但在repeatable read隔离模式的快照中依旧为active状态,整个事务快照不变。
                          (1 row)
                          (postgres@[local]:5432)[akendb01]#select * from table01;
                          id | name
                          ----+--------
                          1 | aken01
                          (1 row)
                          (postgres@[local]:5432)[akendb01]#


                          4.T4时间段

                          session 2、session 3事务结束,session 1、2、3读取到的事务快照都为“676:676:”,且查询结果相同。

                            (postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
                            txid_current_snapshot
                            -----------------------
                            676:676: <<<xip_list为空,xmin=xmax,表示当前快照无活跃事务,未来产生的第一个事务为676.(实际上txid=676在session 4已经产生,我的理解是pg获取事务快照时最后一个txid会滞后)
                            (1 row)
                            (postgres@[local]:5432)[akendb01]#
                            (postgres@[local]:5432)[akendb01]#select * from table01;
                            id | name
                            ----+--------
                            1 | aken01
                            2 | aken02
                            (2 rows)


                            查看Aken-甘植恳更过相关文章:

                            https://www.toutiao.com/c/user/54536888148/#mid=1610143870006285


                            最后修改时间:2019-11-24 19:06:24
                            文章转载自御数临风,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                            评论