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

PostgreSQL:初识(三)

DB备战室 2019-08-11
750
与ORACLE和MySQL相似,在学习PostgreSQL过程中,事务与锁都是绕不开的,它们都是知识体系中较为重要的部分。
一、事务
在关系型数据库操作中,一组相关的操作通常要么全部成功,要么全部失败,这组操作就是所谓的“事务”。而事务必定要满足四个特性:原子性(Automicity),一致性(Consistency),隔离性(Isolation),持久性(Durability),简称“ACID”。
与MySQL相似的是,在默认配置下,SQL语句都是自动提交的。而与ORACLE或MySQL不同的是,PostgreSQL中绝大部分的DDL不会隐式的提交,也就是说在一个事务中,PostgreSQL的大部分DDL都是事务性的,都支持回滚操作。

1.开始与结束事务

    (1)开始事务
    postgres=# \h begin
    Command: BEGIN
    Description: start a transaction block
    Syntax:
    BEGIN [ WORK | TRANSACTION ] [ transaction_mode [, ...] ]


    where transaction_mode is one of:


    ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED }
    READ WRITE | READ ONLY
    [ NOT ] DEFERRABLE


    (2)结束事务
    # end与commit效果相同,用于提交完成当前事务;rollback用于停止并回滚当前事务
    postgres=# \end
    Invalid command \end. Try \? for help.
    postgres=# \h end
    Command: END
    Description: commit the current transaction
    Syntax:
    END [ WORK | TRANSACTION ]


    postgres=# \h commit;
    Command: COMMIT
    Description: commit the current transaction
    Syntax:
    COMMIT [ WORK | TRANSACTION ]
    postgres=# \h rollback
    Command: ROLLBACK
    Description: abort the current transaction
    Syntax:
    ROLLBACK [ WORK | TRANSACTION ]

    2.保存点

    (1)事务处理

      postgres=# begin;
      BEGIN
      postgres=# select version();
      version
      -----------------------------------------------------------------------------------------------------------
      PostgreSQL 9.6.14 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-16), 64-bit
      (1 row)


      postgres=# commit;
      COMMIT
      postgres=# begin;
      BEGIN
      postgres=# select version();
      version
      -----------------------------------------------------------------------------------------------------------
      PostgreSQL 9.6.14 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-16), 64-bit
      (1 row)


      postgres=# rollback;
      ROLLBACK


      postgres=# begin;
      BEGIN
      postgres=# select version;
      ERROR: column "version" does not exist
      LINE 1: select version;
      ^
      postgres=# select version();
      ERROR: current transaction is aborted, commands ignored until end of transaction block
      postgres=# commit;
      ROLLBACK
      正常情况下,我们可以对事务任意进行提交或者终止,但当事务中有SQL执行出现问题时,其后面的正确SQL也将无法继续执行,而且此时执行commit操作在PostgreSQL会转为rollback而非commit。

      3.DDL事务

        # 开启事务并创建t2表及插入数据
        test2=> begin;
        BEGIN
        test2=> create table t2(id int);
        CREATE TABLE
        test2=> insert into t2 values (1),(2),(3),(4),(5);
        INSERT 0 5
        # 查看数据
        test2=> select * from t2;
        id
        ----
        1
        2
        3
        4
        5
        (5 rows)


        # 执行rollback后再次查看t2表数据,可以看到此时表已经不存在
        test2=> rollback;
        ROLLBACK
        test2=> select * from t2;
        ERROR: relation "t2" does not exist
        LINE 1: select * from t2;

        4.保存点(SAVEPOINT)

        在一个长事务中,往往很难确保其运行完全正常,此时可以使用保存点将该长事务分隔成几部分,每部分执行成功后生成一个savepoint,后面的事务执行失败则只需要回滚到前面一个savepoint即可,不需要回滚整个事务。
          test2=> begin;
          BEGIN
          test2=> select * from t1;
          id
          ----
          1
          2
          3
          (3 rows)


          # 插入新数据并创建savepoint
          test2=> insert into t1 values(4);
          INSERT 0 1
          test2=> savepoint a;
          SAVEPOINT
          test2=> insert into t1 values(5);
          INSERT 0 1
          test2=> savepoint b;
          SAVEPOINT


          # 故意写错SQL创造错误
          test2=> insert into t1 values(6; 
          test2(> );
          ERROR: syntax error at or near ";"
          LINE 1: insert into t1 values(6;
          ^
          # 此时如果没有savepoint就只能rollback整个事务,否则无法继续进行下去
          test2=> insert into t1 values(6);
          ERROR: current transaction is aborted, commands ignored until end of transaction block
          test2=> rollback to savepoint b;
          ROLLBACK


          # rollback到保存点b,继续进行插入数据成功
          test2=> insert into t1 values(6);
          INSERT 0 1
          test2=> select * from t1;
          id
          ----
          1
          2
          3
          4
          5
          6
          (6 rows)


          test2=> commit;
          COMMIT


          # 注意:rollback到保存点,只能在事务块中实现,也就是说随着事务提交/结束,该事务中的保存点寿命也将被终止
          test2=> rollback to savepoint b;
          ERROR: ROLLBACK TO SAVEPOINT can only be used in transaction blocks

          5.事务的隔离级别

          一般来说,数据库有以下4种隔离级别:

          隔离级别脏读不可重复读幻读
          读未提交YYY
          读已提交NYY
          可重复读NNY
          串行化NNN

          注:Y代表可能,N代表不可能

          在PostgreSQL中,可以通过命令将事务隔离级别设置为四种隔离级别中的任意一种。但实际上,在PostgreSQL目前版本,其内部只有3种隔离级别(没有读未提交,使用读未提交,PostgreSQL会自动将其映射为读提交)。在9.1版本之前甚至只有2种(没有可重复读),读已提交是PostgreSQL的事务默认隔离级别。
          另外,在PostgreSQL的重复读隔离级别下,幻读是不可能的。因为标准SQL隔离级别是通过锁数据实现,而PostgreSQL中是使用“MVCC”设计来实现的。

          二、MVCC的实现

          多版本并发控制(Multi-Version Concurrency Control),简称MVCC,在大多数关系型数据库中都支持MVCC特性。MVCC主要用来提高并发性,实现读写相互不阻塞的目标。

          MVCC的实现方法,下面引用《PostgreSQL修炼之道》来说明:

            MVCC的实现方法有两种:

            1.写新数据时,把旧数据移到一个单独的地方,如回滚段中,其他人读数据时,从回滚段中把旧的数据读出来;

            2.写数据时,旧数据不删除,而是把新数据插入。
            PostgreSQL数据库使用第二种方法,而ORACLE数据库和MySQL中的innodb引擎使用的是第一种方法。
            与ORACLE数据库和MySQL中的innodb引擎相比较,PostgreSQL的MVCC实现方式的优缺点如下。
            优点:
            1.事务回滚可以立即完成,无论事务进行了多少操作;
            2.数据可以进行很多更新,不必像ORACLE和MySQL的Innodb引擎那样需要经常保证回滚段不会被用完,也不会像oracle数据库那样经常遇到“ORA-1555”错误的困扰;
            缺点:
            1.旧版本数据需要清理。PostgreSQL清理旧版本的命令成为Vacuum;
            2.旧版本的数据会导致查询更慢一些,因为旧版本的数据存在于数据文件中,查询时需要扫描更多的数据块。   

            关于MVCC实现,下面有个小实验可以侧面证明

              # 创建表及测试数据,
              test2=> create table t2 (id int);
              CREATE TABLE
              test2=> insert into t2 select * from generate_series(1,10000);
              INSERT 0 10000


              # 查看表大小
              test2=> select pg_size_pretty(pg_relation_size('t2'));
              pg_size_pretty
              ----------------
              360 kB
              (1 row)


              # 修改全表数据,再次查看大小
              test2=> update t2 set id=id *10;
              UPDATE 10000
              test2=> select pg_size_pretty(pg_relation_size('t2'));
              pg_size_pretty
              ----------------
              712 kB
              (1 row)


              # 可以看到update这种更新/修改数据的操作,在PostgreSQL中是不清除旧数据,而是复制行数据进行修改实现的。

              三、锁

              在PostgreSQL中,存在4种锁机制:表级锁,行级锁,页级锁和advisory锁。

              1.表级锁

              PostgreSQL中一共提供了8种表级锁,它们之间的冲突关系如下图。

              下面是这8种锁模式的简单说明:

              (1)ACCESS SHARE:select命令可在查询中引用的表上加这个类型的锁。一般情况下,是所有的操作中只有读表操作才加上这个类型的锁。

              (2)ROW SHARE:select for update/select for share命令会使得目标表上获得该锁。

              (3)ROW EXCLUSIVE:update/insert/delete命令会使被修改的目标表加上这类型的锁,一般来说,表数据修改都是使用这个类型的锁。

              (4)SHARE UPDATE EXCLUSIVE:CREATE INDEX CONCURRENTLY、ANALYZE、VACUUM(不含FULL)和VALIDATE等这些命令获得此类锁。

              (5)SHARE:CREATE INDEX命令可以在表上获得该锁。

              (6)SHARE ROW EXCLUSIVE:不能被任何PostgreSQL命令自动获取。

              (7)EXCLUSIVE:不能被任何PostgreSQL命令自动获取。

              (8)ACCESS EXCLUSIVE:ALTER TABLE、DROP TABLE、TRUNCATE、REINDEX、CLUSTER和VACUUM FULL命令在获得该锁。它是lock table命令的默认锁模式。

              2.行级锁

              在9.3之前的版本,PostgreSQL行级锁只有select...for update和select...for share两种,从9.3版本开始,引入了for key share和for key update。下表是这4类锁之间的冲突情况。

              FOR KEY 

              SHARE

              FOR SHARE

              FOR NO

              KEY  UPDATE

              FOR UPDATE

              FOR  KEY 

              SHARE

              FOR  SHARE

              FOR  NO 

              KEY UPDATE

              FOR UPDATE

              (1)FOR UPDATE:该操作将导致SELECT语句检索的行被锁定。这可以防止它们被其他事务锁定,修改或删除,直到当前事务结束。

              (2)FOR NO KEY UPDATE:该锁行为类似于FOR UPDATE,只是获得的锁比较弱:

              (3)FOR SHARE:该锁行为类似于FOR NO KEY UPDATE,只是它在每个行上获得一个共享锁,而不是排它锁。

              (4)FOR KEY SHARE:该锁行为类似于FOR SHARE,只是获得的锁比较弱。

              3.页级锁

              除了表和行锁之外,页级共享/独占锁用于控制对共享缓冲池中表页的读/写访问。获取或更新数据行后立即释放这些锁。应用程序开发人员通常不需要关注页级锁,这里暂不讨论。

              4.advisory锁

              PostgreSQL允许创建由应用自定义其含义的锁。这种锁被称为advisory, 系统并不强制使用而是由应用来保证它们被恰当的使用,比如:它可以用于锁定一个数字。它们不会像普通的锁一样在事务提交或回滚后被释放,而是需要使用特殊的函数显式进行释放。另外获取advisory锁也需要使用特殊的函数进行显式加锁。它的加锁和解锁可以从下表看出来:
              时刻会话1会话2
              1begin;
              1select  pg_advisory_lock(6); begin;
              2
              select  pg_advisory_lock(6); 
              3其他操作等待
              4commit;等待
              5select  pg_advisory_unlock(6); 等待
              6
              获得锁

              5.查看锁

              与ORACLE和MySQL一样,PostgreSQL数据库也提供了查看锁的视图,那就是:pg_locks

                # 会话1:
                test2=> select pg_backend_pid();
                pg_backend_pid
                ----------------
                2447
                (1 row)
                test2=> begin;
                BEGIN
                test2=> update t1 set id=id+1 where id=6;
                UPDATE 1
                # 会话2:
                test2=> select pg_backend_pid();
                pg_backend_pid
                ----------------
                4434
                (1 row)
                test2=> begin;
                BEGIN
                test2=> update t1 set id=id+1 where id=6;
                --此时update处于等待状态


                # 会话3:
                test2=# select locktype, relation::regclass as rel,page||','||tuple as ctid,virtualxid as vxid,transactionid as xid,virtualtransaction as vxid2, pid,mode,granted from pg_locks where pid in (2447,4434) order by pid;
                locktype | rel | ctid | vxid | xid | vxid2 | pid | mode | granted
                ---------------+-----+------+--------+------+--------+------+------------------+---------
                relation | t1 | | | | 3/1414 | 2447 | RowExclusiveLock | t
                virtualxid | | | 3/1414 | | 3/1414 | 2447 | ExclusiveLock | t
                transactionid | | | | 1841 | 3/1414 | 2447 | ExclusiveLock | t
                virtualxid | | | 2/4141 | | 2/4141 | 4434 | ExclusiveLock | t
                transactionid | | | | 1841 | 2/4141 | 4434 | ShareLock | f
                transactionid | | | | 1842 | 2/4141 | 4434 | ExclusiveLock | t
                tuple | t1 | 0,10 | | | 2/4141 | 4434 | ExclusiveLock | t
                relation | t1 | | | | 2/4141 | 4434 | RowExclusiveLock | t
                (8 rows)


                # 定位锁
                test2=# select * from t1 where ctid='(0,10)';
                id
                ----
                6
                (1 row)


                locktype:锁对象的类型,如relation(表),tuple(行数据),virtualxid,transactionid等
                rel:对象为表则显式表名,否则为空
                ctid:表的ctid字段
                mode:锁模式名称
                granted:持有锁为“t”(ture),否则w诶“f”(false)


                进程2447即会话1获取了t1表上的RowExclusive Lock
                进程4434即会话2实际上此时是被阻塞的状态,这是因为行锁会在数据行上加上自己的xid,当另一个会话读到这一行时,
                如果发现其上有行锁,此时会在该事务的xid上申请一个share锁,而由于此时上面已经有exclusive,故会话被阻塞。


                文章转载自DB备战室,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                评论