事务与并发控制

文章目录

  • 事务
    • 概念
    • 并发引发的现象
      • 脏读
      • 不可重复读
      • 幻读
    • 事务隔离级别
  • 并发控制
    • 基于锁的并发控制
      • 表级锁的冲突矩阵
      • 表级锁对应的操作
      • 锁的查看
        • 单个读
        • 同时读写
        • 同时写
        • 观察死锁

事务

概念

事务是数据库系统执行过程中最小的逻辑单位。
在PostgreSQL中,显示地指定BEGINEND/COMMIT/ROLLBACK包括的语句块为一个事务,未指定的单条语句也称为一个事务。

例如:

SELECT
    now(),
    now();

              now              |              now
-------------------------------+-------------------------------
 2020-02-29 07:21:55.100997+00 | 2020-02-29 07:21:55.100997+00
(1 row)
BEGINSELECT
    now();

              now
-------------------------------
 2020-02-29 07:15:06.975875+00
(1 row)


SELECT
    now();

              now
-------------------------------
 2020-02-29 07:15:06.975875+00
(1 row)


COMMIT;

事务有四个重要的特性(通常称为ACID):

  • 原子性(Atomicity):事务必须是原子工作单元,对于其数据修改,要么全都执行,要么全都不执行。

  • 一致性(Consistency):事务在完成时,必须使所有的数据都保持一致状态。

  • 隔离性(Isolation):由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据时数据所处的状态,要么是另一个并发事务修改它之前的状态,要么是修改它之后的状态。事务不会查看中间状态的数据。

  • 持久性(Durability):事务完成之后,它对于系统的影响是永久性的。即使出现系统故障也将一直保持。

其中,一致性由主键、外键这类约束保证,持久性由WAL日志和数据库管理系统的恢复子系统保证,原子性、隔离性由事务管理器和MVCC来控制。

并发引发的现象

数据库中数据的并发操作经常发生,而对数据的并发操作可能会带来脏读、幻读、不可重复读等现象。

创建测试数据,并举例说明。

CREATE TABLE t_test (
    id		int,
    name	text
);


INSERT INTO t_test (
    id,
    name
)
VALUES
    (
        1,
        'a'
    );

脏读

一个事务读取了另一个并行未提交事务写入的数据。

由于PostgreSQL内部将Read Uncommitted设计为和Read Committed一样,因此在PostgreSQL中,无法产生脏读。所以下面的例子是假想的。

事务1 事务2
BEGIN TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; BEGIN;
UPDATE t_test SET name = ‘b’ WHERE id = 1;
SELECT * FROM t_test;
看到 (1, a)
ROLLBACK

不可重复读

一个事务重新读取之前读取过的数据,发现该数据已经被另一个事务(在初始读之后提交)修改。

事务1 事务2
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT * FROM t_test WHERE id = 1;
看到 (1, a)
BEGIN;
UPDATE t_test SET name = ‘b’ WHERE id = 1;
COMMIT;
SELECT * FROM t_test WHERE id = 1;
看到 (1, b)

使用可重复读隔离级别,可以避免不可重复读:

事务1 事务2
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM t_test WHERE id = 1;
看到 (1, a)
BEGIN;
UPDATE t_test SET name = ‘b’ WHERE id = 1;
COMMIT;
SELECT * FROM t_test WHERE id = 1;
仍然看到 (1, a)

幻读

一个事务重新执行一个返回符合一个搜索条件的行集合的查询, 发现满足条件的行集合因为另一个最近提交的事务而发生了改变。

事务1 事务2
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT * FROM t_test WHERE id < 3;
看到 (1, a)
BEGIN;
INSERT INTO t_test (id, name) VALUES (2, ‘b’);
COMMIT;
SELECT * FROM t_test WHERE id < 3;
(1, a)
(2, b)

不可重复读和幻读很相似,它们之间的区别主要在于不可重复读主要受到其他事务对数据的UPDATE操作影响,而幻读主要受到其他事务的INSERTDELETE操作影响。

在PostgreSQL中,使用可重复读隔离级别,也可以避免幻读:

事务1 事务2
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM t_test WHERE id < 3;
看到 (1, a)
BEGIN;
INSERT INTO t_test (id, name) VALUES (2, ‘b’);
COMMIT;
SELECT * FROM t_test WHERE id < 3;
仍然看到 (1, a)

事务隔离级别

SQL标准允许更严格的行为:四种隔离级别只定义了哪种现象不能发生,但是没有定义哪种现象必须发生。

隔离级别 脏读 不可重复读 幻读 序列化异常
读未提交 可能,但不在PG中 可能 可能 可能
读已提交 不可能 可能 可能 可能
可重复读 不可能 不可能 可能,但不在PG中 可能
可序列化 不可能 不可能 不可能 不可能
  • Read Uncommitted(读未提交):在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。在多用户数据库中,脏读是非常危险的,在并发情况下,查询结果非常不可控。即使不考虑结果的严谨性只追求性能,它的性能也并不比其他事务隔离级别好多少,可以说脏读没有任何好处。所以读未提交这一事务隔离级别很少用于实际应用。

  • Read Committed(读已提交):PostgreSQL的默认隔离级别。当一个事务运行使用这个隔离级别时,一个查询(没有FOR UPDATE/SHARE子句)只能看到查询开始之前已经被提交的数据, 而无法看到未提交的数据或在查询执行期间其它事务提交的数据。实际上,SELECT查询看到的是一个在查询开始运行的瞬间该数据库的一个快照。不过SELECT可以看见在它自身事务中之前执行的更新的效果,即使它们还没有被提交。还要注意的是,即使在同一个事务里两个相邻的SELECT命令可能会看到不同的数据, 因为其它事务可能会在第一个SELECT开始和第二个SELECT开始之间提交。

  • Repeatable Read(可重复读):可重复读隔离级别只看到在事务开始之前被提交的数据,它从来看不到未提交的数据或者并行事务在本事务执行期间提交的修改。在PostgreSQL中,这个隔离级别阻止了除序列化异常之外的所有现象,这是比SQL标准对此隔离级别所要求的更强的保证。如上面所提到的,这是SQL标准特别允许的。

  • Serializable(可序列化):可序列化隔离级别提供了最严格的事务隔离。这个级别为所有已提交事务模拟序列事务执行,就好像事务被按照序列一个接着另一个被执行,而不是并行地被执行。但是,使用这个级别的应用必须准备好因为序列化失败而重试事务。事实上,这个隔离级别完全像可重复读一样地工作,除了它会监视一些条件,这些条件可能导致一个可序列化事务的并发集合的执行产生的行为与这些事务所有可能的序列化(一次一个)执行不一致。这种监控不会引入超出可重复读之外的阻塞,但是监控会产生一些负荷,并且对那些可能导致序列化异常的条件的检测将触发一次序列化失败。

并发控制

基于锁的并发控制

PostgreSQL为锁定一个表提供了8种类型的锁。一个锁可以轻如ACCESS SHARE锁或者重如ACCESS EXCLUSIVE锁。

锁模式 解释
Access Share 只与Access Exclusive锁模式冲突。
SELECT将会在它查询的表上获取Access Share锁,一般地,任何一个对表上的只读查询操作都将获取这种类型锁。
Row Share 与Exclusive和Access Exclusive锁模式冲突。
SELECT FOR UPDATE和SELECT FOR SHARE命令将获得这种类型锁。
Row Exclusive 与Share,Share Row Exclusive,Exclusive,Access Exclusive模式冲突。
UPDATE/DELETE/INSERT命令会在目标表上获得这种类型的锁,一般地,更改表数据的命令都将在这张表上获得Row Exclusive锁。
Share Update Exclusive Share Update Exclusive,Share,Share Row Exclusive,Exclusive,Access exclusive模式冲突,这种模式保护一张表不被并发的模式更改和VACUUM。
VACUUM(without FULL),ANALYZE和CREATE INDEX CONCURRENTLY命令会获得这种类型锁。
Share 与Row Exclusive,Share Update Exclusive,Share Row Exclusive,Exclusive,Access exclusive锁模式冲突,这种模式保护一张表数据不被并发的更改。
CREATE INDEX命令会获得这种锁模式。
Share Row Exclusive 与Row Exclusive,Share Update Exclusive,Shared,Shared Row Exclusive,Exclusive,Access Exclusive锁模式冲突。
任何PostgreSQL命令都不会自动请求这个锁模式。
Exclusive 与ROW Share, Row Exclusive, Share Update Exclusive, Share , Share Row Exclusive, Exclusive, Access Exclusive模式冲突,这种锁模式仅能与Access Share模式并发,换句话说,只有读操作可以和持有Exclusive锁的事务并行。
任何PostgreSQL命令都不会在用户表上自动请求这个锁模式。
Access Exclusive 与所有模式锁冲突(Access Share,Row Share,Row Exclusive,Share Update Exclusive,Share, Share Row Exclusive,Exclusive,Access Exclusive)。
这种模式保证了当前只有一个人访问这张表。ALTER TABLE,DROP TABLE,TRUNCATE,REINDEX,CLUSTER,VACUUM FULL命令会获得这种类型锁,在LOCK TABLE命令中,如果没有申明其它模式,它也是默认模式。

表级锁的冲突矩阵

请求的锁模式 当前的锁模式
Access Share Row Share Row Exclusive Share Update Exclusive Share Share Row Exclusive Exclusive Access Exclusive
Access Share X
Row Share X X
Row Exclusive X X X X
Share Update Exclusive X X X X X
Share X X X X X X
Share Row Exclusive X X X X X X
Exclusive X X X X X X X
Access Exclusive X X X X X X X X

表中“X”表示这两种表冲突,也就是不同的进程不能同时持有这两种锁。

最普通的是共享锁Share和排他锁Exclusive,它们分别是读、写锁的意思。加了Share锁,即读锁,表的内容就不能变化了,可以为多个事务加上此锁,只要任意一个事务不释放这个读锁,则其他事务就不能修改这个表。加上了Exclusive,相当于加了写锁,这时别的进程不能写也不能读这条数据。

后来数据库又加上了多版本的功能。修改一条语句的同时,允许了读数据,为了处理这种情况,又增加了两种锁Access Share和Access Excusive,锁中的关键字Access是与多版本读相关的。有了多版本的功能后,如果修改一行数据,实际并没有改原先那行数据,而是复制了一个新行,修改都在新行上,事务不提交,其他人是看不到修改的这条数据的。由于旧行数据没有变化,在修改过程中,读数据的人仍然可以读到旧的数据。

表级锁加锁对象是表,这使得加锁范围太大,导致并发并不高,于是人们提出了行级锁的概念,但行级锁与表级锁之间会产生冲突,这时需要一种机制来描述行级锁与表级锁之间的关系。方法就是当我们要修改表中的某一行数据时,需要先在表上加一种锁,如Row Share和Row Exclusive(对应MySQL中的共享意向锁和排他意向锁),表示即将在表的部分行上加共享锁或排他锁。

表级锁对应的操作

锁类型 对应的数据库操作
Access Share SELECT
Row Share SELECT FOR UPDATE,SELECT FOR SHARE
Row Exclusive UPDATE,DELETE,INSERT
Share Update Exclusive VACUUM (without FULL),ANALYZE,CREATE INDEX CONCURRENTLY
Share CREATE INDEX (without CONCURRENTLY)
Share Row Exclusive 任何PostgreSQL命令都不会自动请求这个锁模式
Exclusive 任何PostgreSQL命令都不会在用户表上自动请求这个锁模式
Access Exclusive ALTER TABLE,DROP TABLE,TRUNCATE,REINDEX,CLUSTER,VACUUM FULL

锁的查看

视图pg_locks提供了数据库服务器上活动进程中保持的锁的信息。

名称 类型 引用 描述
locktype text 可锁对象的类型: relation, extend, page, tuple, transactionid, virtualxid, object, userlock, or advisory
database oid pg_database.oid 锁目标存在的数据库的OID,如果目标是一个共享对象则为0,如果目标是一个事务ID则为空
relation oid pg_class.oid 作为锁目标的关系的OID,如果目标不是一个关系或者只是关系的一部分则此列为空
page integer 作为锁目标的页在关系中的页号,如果目标不是一个关系页或元组则此列为空
tuple smallint 作为锁目标的元组在页中的元组号,如果目标不是一个元组则此列为空
virtualxid text 作为锁目标的事务虚拟ID,如果目标不是一个虚拟事务ID则此列为空
transactionid xid 作为锁目标的事务ID,如果目标不是一个事务ID则此列为空ID
classid oid pg_class.oid 包含锁目标的系统目录的OID,如果目标不是一个普通数据库对象则此列为空
objid oid 任意OID列 锁目标在它的系统目录中的OID,如果目标不是一个普通数据库对象则为空
objsubid smallint 锁的目标列号(classid和objid指表本身),如果目标是某种其他普通数据库对象则此列为0,如果目标不是一个普通数据库对象则此列为空
virtualtransaction text 保持这个锁或者正在等待这个锁的事务的虚拟ID
pid integer 保持这个锁或者正在等待这个锁的服务器进程的PID,如果此锁被一个预备事务所持有则此列为空
mode text 此进程已持有或者希望持有的锁模式
granted boolean 如果锁已授予则为真,如果锁被等待则为假
fastpath boolean 如果锁通过快速路径获得则为真,通过主锁表获得则为假

单个读

事务1 事务2
SELECT pg_backend_pid();
10572
BEGIN;
SELECT * FROM t_test;
查看锁【注1】
查看表【注2】

【注1】

SELECT
    locktype, database, relation, virtualxid, transactionid, virtualtransaction, pid, mode, granted
FROM
    pg_locks
WHERE
    pid = 10572;
    
  locktype  | database | relation | virtualxid | transactionid | virtualtransaction |  pid  |      mode       | granted
------------+----------+----------+------------+---------------+--------------------+-------+-----------------+---------
 relation   |    13269 |    16398 |            |               | 5/7                | 10572 | AccessShareLock | t
 virtualxid |          |          | 5/7        |               | 5/7                | 10572 | ExclusiveLock   | t
(2 rows)

【注2】

SELECT relname FROM pg_class WHERE oid = 16398;

 relname
---------
 t_test
(1 row)

同时读写

事务1 事务2 事务3
SELECT pg_backend_pid(); SELECT pg_backend_pid();
4354 4366
BEGIN; BEGIN;
UPDATE t_test SET name = ‘c’ WHERE id = 1;
SELECT * FROM t_test WHERE id = 1;
看到 (1, a)
查看锁【注3】

【注3】

SELECT
    locktype, relation, transactionid, pid, mode, granted
FROM
    pg_locks
WHERE
    pid IN (4354, 4366) 
    AND (relation > 5000 OR relation IS NULL) 
ORDER BY pid;

   locktype    | relation | transactionid | pid  |       mode       | granted
---------------+----------+---------------+------+------------------+---------
 relation      |    16398 |               | 4354 | RowExclusiveLock | t
 virtualxid    |          |               | 4354 | ExclusiveLock    | t
 transactionid |          |          1706 | 4354 | ExclusiveLock    | t
 relation      |    16398 |               | 4366 | AccessShareLock  | t
 virtualxid    |          |               | 4366 | ExclusiveLock    | t
(5 rows)

同时写

事务1 事务2 事务3
SELECT pg_backend_pid(); SELECT pg_backend_pid();
9602 9617
BEGIN; BEGIN;
UPDATE t_test SET name = ‘c’ WHERE id = 1;
UPDATE t_test SET name = ‘d’ WHERE id = 1;
等待事务1
查看锁【注4】

【注4】

SELECT ctid, * FROM t_test;

 ctid  | id | name
-------+----+------
 (0,6) |  1 | a
 (0,8) |  2 | b
(2 rows)


SELECT
    locktype, database, relation, page, tuple, virtualxid, transactionid, virtualtransaction, pid, mode, granted, fastpath
FROM
    pg_locks
WHERE
    pid IN (9602, 9617)
    AND (relation > 5000 OR relation IS NULL)
ORDER BY pid;

   locktype    | database | relation | page | tuple | virtualxid | transactionid | virtualtransaction | pid  |       mode       | granted | fastpath
---------------+----------+----------+------+-------+------------+---------------+--------------------+------+------------------+---------+----------
 relation      |    13269 |    16398 |      |       |            |               | 2/32               | 9602 | RowExclusiveLock | t       | t
 virtualxid    |          |          |      |       | 2/32       |               | 2/32               | 9602 | ExclusiveLock    | t       | t
 transactionid |          |          |      |       |            |          1709 | 2/32               | 9602 | ExclusiveLock    | t       | f
 virtualxid    |          |          |      |       | 3/16       |               | 3/16               | 9617 | ExclusiveLock    | t       | t
 transactionid |          |          |      |       |            |          1709 | 3/16               | 9617 | ShareLock        | f       | f
 tuple         |    13269 |    16398 |    0 |     6 |            |               | 3/16               | 9617 | ExclusiveLock    | t       | f
 transactionid |          |          |      |       |            |          1710 | 3/16               | 9617 | ExclusiveLock    | t       | f
 relation      |    13269 |    16398 |      |       |            |               | 3/16               | 9617 | RowExclusiveLock | t       | t
(8 rows)


SELECT 
    pid, wait_event_type, wait_event, state, backend_xid, backend_xmin, query
FROM
    pg_stat_activity
WHERE
    pid IN (9602, 9617);
    
 pid  | wait_event_type |  wait_event   |        state        | backend_xid | backend_xmin |                   query                
------+-----------------+---------------+---------------------+-------------+--------------+--------------------------------------------
 9602 |                 |               | idle in transaction |        1709 |              | UPDATE t_test SET name = 'c' WHERE id = 1;
 9617 | Lock            | transactionid | active              |        1710 |         1709 | UPDATE t_test SET name = 'd' WHERE id = 1;
(2 rows)

同时日志里记录如下:

2020-05-09 00:40:46.178 CST,"postgres","postgres",9617,"127.0.0.1:40056",5eb58b0d.2591,1,"",2020-05-09 00:38:37 CST,3/16,1710,LOG,00000,"process 9617 still waiting for ShareLock on transaction 1709 after 1000.090 ms","Process holding the lock: 9602. Wait queue: 9617.",,,,"while updating tuple (0,6) in relation ""t_test""",,,"ProcSleep, proc.c:1425","psql"

如果事务1结束,则事务2获得锁,在日志里记录如下:

2020-05-09 00:52:28.462 CST,"postgres","postgres",9617,"127.0.0.1:40056",5eb58b0d.2591,2,"",2020-05-09 00:38:37 CST,3/16,1710,LOG,00000,"process 9617 acquired ShareLock on transaction 1709 after 703283.665 ms",,,,,"while updating tuple (0,6) in relation ""t_test""",,,"ProcSleep, proc.c:1432","psql"

观察死锁

死锁是指两个(或多个)事务相互持有对方期待的锁,例如,如果事务1在表A上获得一个排他锁,同时试图获取一个在表B上的排他锁, 而事务2已经持有表B的排他锁,同时却正在请求表A上的一个排他锁,那么两个事务就都不能进行下去。

PostgreSQL能够自动检测到死锁情况并且会通过中断其中一个事务从而允许其它事务完成来解决这个问题。

事务1 事务2
BEGIN; BEGIN;
SELECT * FROM t_test WHERE id = 1 FOR UPDATE;
SELECT * FROM t_test WHERE id = 2 FOR UPDATE;
SELECT * FROM t_test WHERE id = 2 FOR UPDATE;
等待事务2 SELECT * FROM t_test WHERE id = 1 FOR UPDATE;
等待事务2 等待事务1
1秒(deadlock_timeout)以后死锁解除

一旦检测到死锁,将会报错如下:

ERROR:  deadlock detected
DETAIL:  Process 9617 waits for ShareLock on transaction 1711; blocked by process 9602.
Process 9602 waits for ShareLock on transaction 1712; blocked by process 9617.
HINT:  See server log for query details.
CONTEXT:  while locking tuple (0,6) in relation "t_test"

日志:

2020-05-09 01:02:35.541 CST,"postgres","postgres",9602,"127.0.0.1:40043",5eb58b04.2582,1,"",2020-05-09 00:38:28 CST,2/33,1711,LOG,00000,"process 9602 still waiting for ShareLock on transaction 1712 after 1000.097 ms","Process holding the lock: 9617. Wait queue: 9602.",,,,"while locking tuple (0,8) in relation ""t_test""",,,"ProcSleep, proc.c:1425","psql"
2020-05-09 01:03:22.892 CST,"postgres","postgres",9617,"127.0.0.1:40056",5eb58b0d.2591,3,"",2020-05-09 00:38:37 CST,3/17,1712,LOG,00000,"process 9617 detected deadlock while waiting for ShareLock on transaction 1711 after 1000.063 ms","Process holding the lock: 9602. Wait queue: .",,,,"while locking tuple (0,6) in relation ""t_test""",,,"ProcSleep, proc.c:1416","psql"
2020-05-09 01:03:22.893 CST,"postgres","postgres",9617,"127.0.0.1:40056",5eb58b0d.2591,4,"",2020-05-09 00:38:37 CST,3/17,1712,ERROR,40P01,"deadlock detected","Process 9617 waits for ShareLock on transaction 1711; blocked by process 9602.
Process 9602 waits for ShareLock on transaction 1712; blocked by process 9617.
Process 9617: SELECT * FROM t_test WHERE id = 1 FOR UPDATE;
Process 9602: SELECT * FROM t_test WHERE id = 2 FOR UPDATE;","See server log for query details.",,,"while locking tuple (0,6) in relation ""t_test""",,,"DeadLockReport, deadlock.c:1135","psql"
2020-05-09 01:03:22.893 CST,"postgres","postgres",9602,"127.0.0.1:40043",5eb58b04.2582,2,"",2020-05-09 00:38:28 CST,2/33,1711,LOG,00000,"process 9602 acquired ShareLock on transaction 1712 after 48351.896 ms",,,,,"while locking tuple (0,8) in relation ""t_test""",,,"ProcSleep, proc.c:1432","psql"

你可能感兴趣的:(PostgreSQL)