Postgresql杂谈 15—Postgresql中的事务

       本文主要学习下Postgresql中事务。事务是数据库中非常重要的一个逻辑结构,可以说是数据库数据安全和准确的一个最重要的保证。同时,它也大大提高了数据库的易用性,因为通过数据库事务,我们在操作数据库进行增删改查时不必再考虑并发引起的问题,而只要专注地实现自己的功能。与其它关系型数据库一样,Postgresql的 事务也具有ACID(原子性、一致性、隔离性和持久性)四大特性,通过这四大特性,Postgresql才有了上述功能特点。但除了这些通用的特性之外,Postgresql的事务也有自己的特点,比如DDL事务、两阶段提交等等。

       下面,笔者就Postgresql事务的这些特点进行深入的学习和介绍。

一、普通的事务操作

1.1 打开/关闭自动提交

       我们先来学习下Postgresql中普通事务的开启方法。我们在使用psql等一些客户端的工具时,事务的自动提交功能是默认打开的,所以我们每次执行一条SQL语句都会自动提交。在psql中手动的打开自动提交的方法是执行以下命令:

\set AUTOCOMMIT on

       psql中手动关闭自动提交:

\set AUTOCOMMIT off

       查看AUTOCOMMIT当前的值:

stock_analysis_data=# \echo :AUTOCOMMIT 
off

1.2 手动开启事务

       Postgresql中可以使用BEGIN命令来手动开启事务,手动开启事务之后也就相当于关闭了事务自动提交的功能。如果我们要手动控制事务,建议使用这种方式,因为:

(1)使用BEGIN可以更见显式的提醒我们当前是在手动事务中,需要手动commit或者rollback;

(2)\set AUTOCOMMIT off这种方式,只适合在psql中使用,因为AUTOCOMMIT是psql的一个变量。

       下面的例子,就是使用BEGIN开启一个事务:

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# insert into t1 (id,name) values (1,'李四');
INSERT 0 1
stock_analysis_data=# insert into t1 (id,name) values (2,'张三');
INSERT 0 1

       此时,还没有提交,我们可以开启另外一个会话查看t1表,发现表中还是空的:

stock_analysis_data=# select * from t1;
 id | name 
----+------
(0 rows)

       提交的方式有两种,第一种是显示的使用commit命令:

stock_analysis_data=# commit; 
COMMIT

       使用这种方式提交并不会关闭当前的事务,要想提交时同时结束事务,需要使用end命令:

stock_analysis_data=# end; 
COMMIT

       不管使用哪种方式,提交之后,我们在另外的session中都会查看到t1表中新增的数据:

stock_analysis_data=# select * from t1;
 id | name 
----+------
  2 | 张三
  1 | 李四
(2 rows)

       我们在手动开启事务时,也可以使用检查点savepoint。下面的语句,就是在插入第一条数据之后保存了一个检查点,然后继续insert,最后回滚到保存的检查点再进行提交,最终的效果是只有第一条数据插入有效:

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# insert into t1 (id,name) values (1,'李四');
INSERT 0 1
stock_analysis_data=# savepoint mypoint;
SAVEPOINT
stock_analysis_data=# insert into t1 (id,name) values (2,'张三');
INSERT 0 1
stock_analysis_data=# rollback to savepoint mypoint;
ROLLBACK
stock_analysis_data=# end;
COMMIT

       查询以上语句执行的结果:

stock_analysis_data=# select * from t1;
 id | name 
----+------
  1 | 李四
(1 row)

二、DDL事务

       DDL事务是Postgresql很有特色的一个功能,其它的关系型数据库很多是不支持DDL事务的。所谓DDL事务就是在执行create table、alter table等这些DDL语句时,支持事务的回滚或提交。

       DDL事务创建的方式其实和普通事务一致,都是使用BEGIN命令开启一个事务,也可以设置savepoint,然后进行commit或者rollback。下面的例子就是开启了一个DDL事务,创建了t3、t4然后设置savepiont,再创建t5,最后rollback到保存的检查点并进行提交。最终的效果就是只创建了t3和t4没有创建t5。

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# 
stock_analysis_data=# create table t3(id int);
CREATE TABLE
stock_analysis_data=# create table t4(id int);
CREATE TABLE
stock_analysis_data=# savepoint mypoint;
SAVEPOINT
stock_analysis_data=# create table t5(id int);
CREATE TABLE
stock_analysis_data=# rollback to savepoint mypoint;
ROLLBACK
stock_analysis_data=# end;
COMMIT

       我们可以查看创建的结果,以验证符合我们的预期(t1和t2是之前创建好的表):

stock_analysis_data=# \dt t*
         List of relations
 Schema | Name  | Type  |  Owner   
--------+-------+-------+----------
 public | t1    | table | postgres
 public | t2    | table | postgres
 public | t3    | table | postgres
 public | t4    | table | postgres

三、分布式事务

       接下来,我们学习下Postgresql对分布式事务的支持。目前分布式架构系统的的势头愈演愈烈,在分布式的架构中一个回避不了的问题就是怎么把不同服务或者不同数据块实例间的操作放到同一个事务,也就是支持分布式事务?目前比较成熟的方案有两阶段提交、三阶段提交、以及一些比较成熟的第三方框架:阿里的seata、txlcn等等。Postgresql为了支持分布式的事务,实现了两阶段提交的事务方式。

       所谓的两阶段提交,就是将事务的提交分成了两个过程:

(1)在执行完成DML语句(update、insert、delete)之后,先进行事务的预提交。预提交的过程不会真的提交数据,但是数据库可以保证只要进行了预提交,数据就不会再丢失,即使数据库发生了重启、宕机。Postgresql中使用PREPARE TRANSACTION命令进行预提交。

(2)完成了预提交之后,就可以真正的提交事务了,Postgresql中使用COMMIT PREPARED命令进行数据的最终提交。

       接下来,我们使用一个实例来介绍了Postgresql中两阶段提交的分布式事务的支持。例子还是向t1表中插入两条数据,然后用分布式事务的方式进行提交。不过在此之前,我们还需要修改下postgresql.conf中max_prepared_transactions的参数,这个参数默认是0,表示不支持分布式事务,我们需要改成一个大于0的数字,然后重启数据库。

max_prepared_transactions = 10

       主要注意一下,max_prepared_transactions只能通过修改postgresql.conf完成,如果通过set命令修改会报错:

postgres=# set max_prepared_transactions=10;
ERROR:  parameter "max_prepared_transactions" cannot be changed without restarting the server

       开启事务并执行insert语句:

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# insert into t1 values(1,'tom');
INSERT 0 1
stock_analysis_data=# insert into t1 values(2,'jerry');
INSERT 0 1

       使用PREPARE TRANSACTION进行预提交:

stock_analysis_data=# PREPARE TRANSACTION 'transaction_001';
PREPARE TRANSACTION

       transaction_001是我们为分布式事务定义的一个事务ID,通过这个事务ID可以保证所有的数据库实例的事务属于同一个分布式的事务,它需要保证在全局的共享和唯一。

       完成了上述步骤之后,就完成了事务的预提交过程,此时如果我们重启了数据库,预提交的数据也不会丢失。

       执行数据库的重启命令:

systemctl restart postgresql-11

       使用COMMIT PREPARED进行最终提交:

       重启了数据库之后,我们先查看下t1表里面有没有我们插入的数据,以验证预提交阶段是不会实际插入数据的:

stock_analysis_data=# select * from t1;
 id | name 
----+------
(0 rows)

       查询的数据为空,接下来再执行最终提交的命令:

stock_analysis_data=# commit prepared 'transaction_001';
COMMIT PREPARED

       再去查询t1表,发现数据被成功插入进去。

stock_analysis_data=# select * from t1;
 id | name  
----+-------
  1 | tom
  2 | jerry
(2 rows)

四、事务的隔离级别

       在介绍事务的隔离级别之前,我们先了解下数据库中常见的不一致问题:

  • 脏读

       在一个事务中读取到了另外一个事务还没有提交的数据。比如一个事务A在读student表某行的score字段,另一个事务B在修改该行的score字段,结果事务B中修改的数据还未提交,事务A却读到了B中修改后的数据。

  • 不可重复度

       在同一个事务中,读了两次某数据,结果读的数据不一致。比如事务A开启后读某账户余额为100,事务B修改了账户余额为90并提交,结果事务A又去读了账户的余额发现和以前不一样了,变成了90。

  • 幻读

       在一个事务里面发现了未被操作的数据。比如事务A修改Account全表的six字段,要把该字段设置成false,结果事务B又向Account表插入了一条新的数据,且six为true,然后事务A提交数据,发现了这条新的数据自己没有修改到。

       针对上述不一致的问题,数据库中提出了隔离级别的概念。所谓隔离级别,实际上就是数据库支持的并发数据安全级别。主要有以下四种:

  • READ UNCOMMITED:读未提交
  • READ COMMITED:读已提交
  • REPEATABLE READ:可重复读
  • SERIALIZABLE:串行化

       接下来,我们分别设置Postgresql不同的隔离级别,并分别进行验证:

4.1 READ UNCOMMITED和READ COMMITED

       Postgresql中默认的隔离级别是READ COMMITED,虽然我们可以设置隔离级别为READ UNCOMMITED,但是实际上还是READ COMMITED。也就是说在postgresql中一个事务中不能读到其它事务中未提交的数据。

       首先,查看下当前的隔离级别:

stock_analysis_data=# select name,setting from pg_settings where name like 'transaction%';
          name          |    setting     
------------------------+----------------
 transaction_deferrable | off
 transaction_isolation  | read committed
 transaction_read_only  | off

       可以看到,默认的隔离级别为read committed,接下来,我们在一个事务中设置隔离级别为read uncommitted

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# set transaction isolation level read uncommitted;
SET
stock_analysis_data=# select name,setting from pg_settings where name like 'transaction%';
          name          |     setting      
------------------------+------------------
 transaction_deferrable | off
 transaction_isolation  | read uncommitted
 transaction_read_only  | off
(3 rows)

       设置完成之后,我们用实例验证下当前数据库的隔离级别的有效性。首先在当前事务A中查询t1表:

stock_analysis_data=# select * from t1;
 id | name  
----+-------
  1 | tom
  2 | jerry
(2 rows)

       另启一个事务B,将id为2的name改成Jack:

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# update t1 set name='jack' where id=2;
UPDATE 1

       再在事务A中查询:

stock_analysis_data=# select * from t1;
 id | name  
----+-------
  1 | tom
  2 | jerry
(2 rows)

       发现id为2的数据name的确没有改变,所以说即使我们将Postgresql的隔离级别设置成了read uncommitted,实际上它的级别还是read committed。

       接下来,提交事务B,继续在事务A中查询,发现数据已经得到了修改:

stock_analysis_data=# select * from t1;
 id | name 
----+------
  1 | tom
  2 | jack
(2 rows)

4.2 REPEATABLE READ和SERIALIZABLE

       比READ UNCOMMITED和READ COMMITED更加严格的是REPEATABLE READ和SERIALIZABLE。其实后面两种隔离级别在设置时与前者也类似,当我们设置隔离级别为REPEATABLE READ时,它会默认支持到SERIALIZABLE。Postgresql总是执行更加严格的隔离级别,关于这点我们不再验证,但是验证下设置隔离级别为SERIALIZABLE是会发生什么现象。

       接着上面的例子,我们设置事务A的隔离级别为SERIALIZABLE。

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# set transaction isolation level serializable;
SET

       然后查询t1表:

stock_analysis_data=# select * from t1;
 id | name 
----+------
  1 | tom
  2 | jack
(2 rows)

       在事务B中,我们更新id为2的数据,同时插入新的数据,并提交。

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# update t1 set name='jack' where id=2;
UPDATE 1
stock_analysis_data=# commit;
COMMIT
stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# update t1 set name='json' where id=2;
UPDATE 1
stock_analysis_data=# insert into t1 values(3,'hyman');
INSERT 0 1
stock_analysis_data=# commit;
COMMIT

       在事务A中再次执行查询:

stock_analysis_data=# select * from t1;
 id | name 
----+------
  1 | tom
  2 | jack
(2 rows)

       发现这次事务B中的更新即使提交了也没有同步到事务A中,当我们尝试改动t1的数据时会提示下面的错误:

stock_analysis_data=# delete from t1;
ERROR:  could not serialize access due to concurrent update

       也就是说,一旦设置了串行化的隔离级别,在事务A中便不能再次修改相关表。

五、总结

       本文,我们主要学习了Postgresql事务相关的内容,现总结如下:

(1)在plsql中默认设置自动提交,我们如果想要手动开启事务,可以执行以下命令:

\set AUTOCOMMIT off

或者使用

BEGIN; 
... 
END;

(2)Postgresql支持DDL事务,使用的方法和普通事务类似,都是用Begin开启事务,可以支持DDL语句的回滚和提交。

(3)Postgresql支持二阶段提交事务,所谓二阶段就是事务分为了预提交和最终提交两个阶段,预提交的数据提交后也做了持久化,即使数据库宕机也不会丢失。

       预提交的命令是:

PREPARE TRANSACTION 'transaction_001';

       最终提交的命令是:

commit prepared 'transaction_001';

(4)Postgresql默认的隔离级别是READ COMMITED,在这个隔离级别下,事务A可以读到事务B的已经提交的数据,可以设置隔离级别为REPEATABLE READ和SERIALIZABLE,此时事务A中无法再读到事务B提交的数据,但是同时也无法修改事务B操作的表。

你可能感兴趣的:(Postgresql原理与实战,Postgresql,事务,预提交,隔离级别)