事务是用户定义的一个数据库操作序列,这些操作要么全做,要么全不做,是一个不可分割的工作单位。在关系数据库中,一个事务可以是一条SQL语句、一组SQL语句或整个程序。
事务的四种特性
- 原子性(Atomicity): 事务为一个整体,要么都执行,要么都不执行 ,如果事务多个操作的前半部分出错,必须回滚所有操作,让数据在逻辑上回到原先的状态。
- 一致性(Consistency): 事务完成时,所有数据保持在一致状态。
- 隔离性(Isolation): 事务查看数据时的状态, 要么是另一事务修改它之前或者之后的状态,事务不会查看中间状态的数据。
- 持久性(Durability): 事务完成后,对系统的影响是永久性的。
在PostgreSQL,可以使用多版本并发控制(MVCC)维护数据一致性。相比如锁定模型,MVCC对读数据的请求和写数据的请求不冲突,即读不会阻塞写,写不会阻塞读。
事务自动提交(AUTOCOMMIT)
PG默认开启了自动提交,通过\set将自动提交关闭,即:
postgres=# \echo :AUTOCOMMIT
on
postgres=# \set AUTOCOMMIT off
postgres=# \echo :AUTOCOMMIT
off
创建表,进行回滚操作:
postgres=# create table test(a int);
CREATE TABLE
postgres=# select * from test;
a
---
(0 rows)
postgres=# rollback ;
ROLLBACK
postgres=# select * from test;
ERROR: relation "test" does not exist
LINE 1: select * from test;
使用BEGIN开启一个事务,相当于关闭自动提交:
postgres=# \set AUTOCOMMIT on
postgres=# \echo :AUTOCOMMIT
on
postgres=# create table test(a int);
CREATE TABLE
postgres=# BEGIN;
BEGIN
postgres=# insert into test values(1);
INSERT 0 1
postgres=# select * from test;
a
---
1
(1 row)
postgres=# rollback ;
ROLLBACK
postgres=# select * from test;
a
---
(0 rows)
保存点(SAVEPOINT)
在一个事务中,执行部分语句成功后创建一个保存点,若后续语句出现错误,则只需回滚到此保存点,不用回滚整个事务。
postgres=# BEGIN;
BEGIN
postgres=# create table test(a int primary key);
CREATE TABLE
postgres=# insert into test values(1);
INSERT 0 1
postgres=# insert into test values(2);
INSERT 0 1
postgres=# savepoint test_savepoint;
SAVEPOINT
postgres=# insert into test values(1);
ERROR: duplicate key value violates unique constraint "test_pkey"
DETAIL: Key (a)=(1) already exists.
postgres=# rollback to savepoint test_savepoint;
ROLLBACK
postgres=# select * from test;
a
---
1
2
(2 rows)
postgres=# insert into test values(3);
INSERT 0 1
postgres=# commit ;
COMMIT
postgres=# select * from test;
a
---
1
2
3
(3 rows)
并发事务可能导致的问题
- 脏读: 一个事务读取了另一个未提交事务写入的数据。例如事务1 update了一条数据但未进行提交,事务2读取了这条数据并进行了使用,之后事务1 回滚,事务2仍然使用这条脏数据。
- 不可重复读:一个事务重新读取前面读取过的数据,发现该数据已经被另一个已经提交的事务修改了。
- 幻读:一个事务开始后,需要根据数据库中现有的数据做一些更新,于是重新执行一个查询,返回符合查询条件的行,这时发现这些行因为其它最近提交的事务而发生了改变,导致现有事务如果再进行下去可能发生逻辑上的错误。
不可重复读和幻读如何区分:
不可重复读重点在于数据的修改,同一个事务读取到的数据可能由于另一个已经提交的事务的修改导致不一样。而幻读重点在于新增或者删除,例如第一个事务对一个表中的所有行进行了update。同时,第二个事务插入了一行新数据。那么,就会发生操作第一个事务的用户发现表中还有没有修改的数据行的情况,就好象发生了幻觉一k样。
事务的隔离级别
- READ UNCOMMITED(读未提交):如果一个事务开始写某行数据,那么另一个事务不能同时对该行进行写操作,但其他事务读此行数据。应该该种隔离级别可以避免更新丢失,但可能出现事务A读取到事务B未提交的事务,即出现脏读。
- READ COMMITTED(读已提交):事务中的每次读取操作,读取到的都是数据库中其他事务已提交的最新的数据,该隔离级别避免了脏读,但可能出现不可重复读,即事务A事先读取了数据,事务B紧接着更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。
- REPEATABLE READ(重复读):一个事务操作中对于一个读取操作不管多少次,读取到的结果都是一样的。
- SERIALIZABLE(串行化):要求事务序列化执行,只能一个接着一个地执行。
--临时设置事务隔离级别:
set transaction isolation level REPEATABLE READ;
不同事务隔离级别下的行为
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 |
| 读已提交 | 不可能 | 可能 | 可能 |
| 重复读 | 不可能 | 不可能 | 可能 |
| 可串行化 | 不可能 | 不可能 | 不可能 |
PG中事务隔离级别
PG中默认的隔离级别为READ COMMITTED,PG中如果设置读未提交,实际上还是读已提交,在PG中一个事务不可能读到另一个事务中未提交的数据。另外,PG的重复读隔离级别下是不会出现幻读的,实际PG的隔离级别可能比设置的更加严格。
两阶段提交
在分布式系统中,事务通常包含多台数据库的操作,而多台数据库之间的原子性需要使用两阶段提交实现,两阶段提交时实现分布式事务的关键。
两阶段提交步骤:
- 应用程序调用各台数据库进行操作,但不提交事务。之后应用程序调用事务协调器(该协调器可能也是由应用自己实现)中的提交方法。
- 事务协调器联络事务中设计到的每台数据库,通知它们准备提交事务,这是第一阶段的开始。PG中一般是调用PREPARE TRANSACTION命令。
- 各台数据库接受到PREPARE TRANSACTION命令后,如果要返回成功,则数据库必须将自己置于如下状态:确保后续能在被要求提交事务的时候提交事务,或后续能在被要求回滚事务的时候回滚事务。所以PG会将准备好提交的信息写入持久存储区中。如果数据库无法完成此事务,它会直接返回失败给事务协调器。
- 事务协调器接受所有数据库的响应。
- 在第二阶段,如果任何一个数据库在第一阶段返回失败,则事务协调器将会发一个回滚命令"ROLLBACK PREPARED"给各台数据库。如果所有数据库的响应都是成功的,则向各台数据库发送COMMIT PREPARED命令,通知各台数据库事务成功。
示例:
--1、将max_prepared_transactions设置为大于0的数字并重启数据库
postgres=# show max_prepared_transactions ;
max_prepared_transactions
---------------------------
10
(1 row)
--2、创建表,启动事务插入数据,使用PREPARE TRANSACTION命令准备好事务提交(第一阶段)
postgres=# create table test (a int primary key, b text);
CREATE TABLE
postgres=# begin;
BEGIN
postgres=# insert into test values(1,now());
INSERT 0 1
postgres=# PREPARE TRANSACTION 'test_001';
PREPARE TRANSACTION
上述‘test_001’为两阶段提交中的全局事务ID,由事务协调器生成。PG一旦成功执行PREPARE TRANSACTION命令,会将事务持久化,即使此时数据库重启,此事务也不会回滚或丢失。此时查询视图pg_prepared_xacts,可以看到里面存在如下数据:
postgres=# select * from pg_prepared_xacts;
transaction | gid | prepared | owner | database
-------------+----------+-------------------------------+----------+----------
784 | test_001 | 2022-01-02 17:37:25.782111+08 | postgres | postgres
(1 row)
此时查询表test,数据为空
postgres=# select * from test ;
a | b
---+---
(0 rows)
退出数据库,查看data目录下的pg_twophase目录,存在文件:
[postgres@VM-0-11-centos bin]$ ls ./data/pg_twophase/00000310 -al
-rw------- 1 postgres postgres 220 Jan 2 17:39 ./data/pg_twophase/00000310
停止数据库,之后重新启动
[postgres@VM-0-11-centos bin]$ ./pg_ctl -D ./data stop
waiting for server to shut down.... done
server stopped
[postgres@VM-0-11-centos bin]$ ./pg_ctl -D ./data start
waiting for server to start....2022-01-02 17:44:38.182 CST [2733] LOG: starting PostgreSQL 12.2 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39), 64-bit
2022-01-02 17:44:38.183 CST [2733] LOG: listening on IPv4 address "127.0.0.1", port 5432
2022-01-02 17:44:38.192 CST [2733] LOG: listening on Unix socket "/tmp/.s.PGSQL.5432"
2022-01-02 17:44:38.219 CST [2733] LOG: redirecting log output to logging collector process
2022-01-02 17:44:38.219 CST [2733] HINT: Future log output will appear in directory "log".
done
server started
连接数据库,查询表test,表为空;查询视图pg_prepared_xacts,视图中有数据。
postgres=# select * from test;
a | b
---+---
(0 rows)
postgres=# select * from pg_prepared_xacts;
transaction | gid | prepared | owner | database
-------------+----------+-------------------------------+----------+----------
784 | test_001 | 2022-01-02 17:37:25.782111+08 | postgres | postgres
(1 row)
执行COMMIT PREPARED进行第二阶段提交,
postgres=# COMMIT PREPARED 'test_001';
COMMIT PREPARED
postgres=# select * from test;
a | b
---+-------------------------------
1 | 2022-01-02 17:36:03.533989+08
(1 row)
postgres=# select * from pg_prepared_xacts;
transaction | gid | prepared | owner | database
-------------+-----+----------+-------+----------
(0 rows)
此时可以查询到数据,且pg_twophase目录下变为空,再次重启数据库数据仍正常。由此看出,一旦执行PREPARE TRANSACTION命令,事务会持久化,即使数据库重启仍然可以提交事务。事务操作不会丢失。
参考《PostgreSQL修炼之道 从小工到专家 第2版》