PostgreSQL中的事务、隔离和锁

事务

ACID特性

ACID是事务的四大特性,想要成为事务,必须具备这四点。

Atomicity(原子性)

原子性体现在对于一个事务来讲,必须要以一个整体单元的形式进行工作。对于数据的修改,要么全部执行,要么全部不执行。出错必须回滚到先前的状态。

Consistency(一致性)

一致性表现为事务进行过后和执行前,数据保持一致状态。

Isolation(隔离性)

隔离性表示各个事务之间不会互相影响,数据库一般会提供多种级别的隔离。实际上多个事务是并发执行的,但是他们之间不会互相影响。

Durability(持久性)

持久性表示一旦一个事务成功了,那么他的改变是永久性的被记录和操作。

DDL事务

PostgreSQL与其他数据库最大的区别是,大多数DDL可以包含在一个事务中,而且也是可以回滚的。因为有这个功能,所以非常适合把PostgreSQL作为Sharding的分布式数据系统的底层数据库。比如,在Sharding中,常常需要在多个结点中建相同的表,这时可以考虑把建表语句放在同一个事务中,这样就可以在各个节点上先启动一个事务,然后执行建表语句。如果某个节点执行失败,也可以把前面已执行建表成功的操作回滚掉,这样就不会出现部分节点建表成功,部分节点失败的情况。

在psql的默认配置下,自动提交“AUTOCOMMIT”项是打开的,也就是说,每执行一条SQL语句,都会自动提交。可以通过设置\set AUTOCOMMIT off来关闭自动提交。
另一种方法,是显式使用“BEGIN”语句来启动一个事务,这也相当于关闭了自动提交。

保存点

PG数据库支持保存点(savepoint)功能,在比较大的事务中,可以把执行过程分为几个步骤,每个步骤执行完成后创建一个保存点,后续步骤执行失败时,可回滚到之前的保存点,而不必回滚整个事务。
相关命令有:

savepoint point_name;//创建一个保存点
rollback to  SAVEPOINT point_name;//回滚到一个保存点

事务的隔离级别

标准的事务隔离级别包括:

  1. 读未提交
  2. 读已提交
  3. 可重复读
  4. 序列化(串行化)
    在正式的postgresql 并不包括读未提交这个隔离级别,因为出于多版本并发控制的架构考虑,默认的事务级别是“读已提交”。
Read uncommitted(读未提交)

简单解释就是:一个session的事务即使没有commit,也对其他session可见。
这会造成脏读。
PostgreSQL中的事务、隔离和锁_第1张图片
事务T1:William Blake想向Riley Truden转10,000美元,在p5时刻撤销转账。
事务T2:因为事务隔离级别是Read uncommitted,所以在T2中能看到收到了10,000美元(脏读),然后撤销了。

Read committed(读已提交)

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

所以脏读现象将不会再发生。

还要注意的是,即使在同一个事务里两个相邻的SELECT命令可能看到不同的数据, 因为其它事务可能会在第一个SELECT开始和第二个SELECT开始之间提交。即会造成不可重复读。

Repeatable read(可重复读)

可重复读隔离级别只看到在事务开始之前被提交的数据;它从来看不到未提交的数据或者并行事务在本事务执行期间提交的修改(不过,查询能够看见在它的事务中之前执行的更新,即使它们还没有被提交)。
这个级别与读已提交不同之处在于,一个可重复读事务中的查询可以看见在事务中第一个非事务控制语句开始时的一个快照,而不是事务中当前语句开始时的快照。因此,在一个单一事务中的后续SELECT命令看到的是相同的数据,即它们看不到其他事务在本事务启动后提交的修改。

可重复读可能会导致幻读,同理,读已提交更有可能会造成幻读。
所谓幻读:一个事务开始后,需要根据数据库中现有的数据做一些更新,就重新执行一个查询,发现查询结果因为最近提交的事务而发生了改变(数据库已经修改了,但是该事务还不知道,因为该事务获取的只是其begin前的一个快照),从而导致现有的事务若再进行下去将会在逻辑上出现一些错误。

使用这个级别的应用必须准备好由于序列化失败而重试事务。

UPDATE、DELETE、SELECT FOR UPDATE和SELECT FOR SHARE命令在搜索目标行时的行为和SELECT一样: 它们将只找到在事务开始时已经被提交的行。 不过,在被找到时,这样的目标行可能已经被其它并发事务更新(或删除或锁住)。在这种情况下, 可重复读事务将等待第一个更新事务提交或者回滚(如果它还在进行中)。 如果第一个更新事务回滚,那么它的作用将被忽略并且可重复读事务可以继续更新最初发现的行。 但是如果第一个更新事务提交(并且实际更新或删除该行,而不是只锁住它),则可重复读事务将回滚并带有如下消息

ERROR:  could not serialize access due to concurrent update

因为一个可重复读事务无法修改或者锁住被其他在可重复读事务开始之后的事务改变的行。

当一个应用接收到这个错误消息,它应该中断当前事务并且从开头重试整个事务。在第二次执行中,该事务将见到作为其初始数据库视图一部分的之前提交的改变,这样在使用行的新版本作为新事务更新的起点时就不会有逻辑冲突。
注意只有更新事务可能需要被重试;只读事务将永远不会有序列化冲突。

Serializable(串行化)

这是最严格的隔离级别,事务串行化的执行。标准定义描述为:保证一组可串行化事务的任意并发执行的结果和以某种顺序执行的结果相同。
在这个隔离级别上,幻读是不可能的。但是,和可重复读级别相似,使用这个级别的应用必须准备好因为序列化失败而重试事务。事实上,这个隔离级别完全像可重复读一样地工作,除了它会监视一些条件,这些条件可能导致一个可序列化事务的并发集合的执行产生的行为与这些事务所有可能的序列化(一次一个)执行不一致。这种监控不会引入超出可重复读之外的阻塞,但是监控会产生一些负荷,并且对那些可能导致序列化异常的条件的检测将触发一次序列化失败。

事务隔离级别的行为如下:

事务隔离级别 脏读 不可重复读 幻读 序列化异常
读未提交 允许,但不在PG中 可能
不可重复读 可能
可重复读 允许,但不在PG中 可能
串行化 不可能

在PostgreSQL中,你可以请求四种标准事务隔离级别中的任意一种,但是内部只实现了三种不同的隔离级别,即 PostgreSQL 的读未提交模式的行为和读已提交相同。这是因为把标准隔离级别映射到 PostgreSQL 的多版本并发控制架构的唯一合理的方法。
该表格也显示 PostgreSQL 的可重复读实现不允许幻读,因为在PG中,可重复读已满足SQL标准的串行化。

要设置一个事务的隔离级别,使用SET TRANSACTION命令。

SET TRANSACTION transaction_mode [, ...]
SET TRANSACTION SNAPSHOT snapshot_id
SET SESSION CHARACTERISTICS AS TRANSACTION transaction_mode [, ...]

其中 transaction_mode 是下列之一:

    ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED }
    READ WRITE | READ ONLY
    [ NOT ] DEFERRABLE
两阶段提交

该小节参考:https://www.w3cschool.cn/architectroad/architectroad-two-phase-commit.html
两阶段提交2PC(Two phase Commit)是指,在分布式系统里,为了保证所有节点在进行事务提交时保持一致性的一种算法。

背景:在分布式系统里,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。当一个事务跨多个节点时,为了保持事务的原子性与一致性,需要引入一个协调者(Coordinator)来统一掌控所有参与者(Participant)的操作结果,并指示它们是否要把操作结果进行真正的提交(commit)或者回滚(rollback)。

思路:
(1)投票阶段(voting phase):参与者将操作结果通知协调者;
(2)提交阶段(commit phase):收到参与者的通知后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交还是回滚;

缺陷:
算法执行过程中,所有节点都处于阻塞状态,所有节点所持有的资源(例如数据库数据,本地文件等)都处于封锁状态。
典型场景为:
(1)某一个参与者发出通知之前,所有参与者以及协调者都处于阻塞状态;
(2)在协调者发出通知之前,所有参与者都处于阻塞状态;

另外,如有协调者或者某个参与者出现了崩溃,为了避免整个算法处于一个完全阻塞状态,往往需要借助超时机制来将算法继续向前推进,故此时算法的效率比较低。

总的来说,2PC是一种比较保守的算法。

在PostgreSQL中,有以下5个步骤:
1.应用程序先调用各台服务器做一些操作,但不提交事务;然后调用事务协调器中的提交方法。
2.事务协调器通过调用“PREPARE TRANSACTION”命令来通知各台数据库准备提交事务。
3.各台数据库返回成功或失败状态。所谓成功状态,需满足:确保后续能在被要求提交事务时提交事务,或者被要求回滚时能够回滚事务。
4.事务协调器收到各台数据库的响应。
5.若任一数据库返回失败,事务协调器发送回滚命令给各台数据库。若所有数据库返回成功,向各台服务器发送“COMMIT PREPARED”命令。

锁机制

PostgreSQL提供了多种锁模式用于控制对表中数据的并发访问。 这些模式可以用于在MVCC无法给出期望行为的情境中由应用控制的锁。

表级锁模式

以下这些锁模式都是表级锁,即使它们的名字包含"row"单词(这些名称是历史遗产)。 在一定程度上,这些名字反应了每种锁模式的典型用法 — 但是语意却都是一样的。 两种锁模式之间真正的区别是它们有着不同的冲突锁模式集合。两个事务在同一时刻不能在同一个表上持有属于相互冲突模式的锁(但是,一个事务决不会和自身冲突。例如,它可以在同一个表上获得ACCESS EXCLUSIVE锁然后接着获取ACCESS SHARE锁)。非冲突锁模式可以由许多事务同时持有。请特别注意有些锁模式是自冲突的(例如,在一个时刻ACCESS EXCLUSIVE锁不能被多于一个事务持有)而其他锁模式不是自冲突的(例如,ACCESS SHARE锁可以被多个事务持有)。

冲突的锁模式PostgreSQL中的事务、隔离和锁_第2张图片
具体解释如下:

ACCESS SHARE 只与ACCESS EXCLUSIVE锁模式冲突。SELECT命令在被引用的表上获得一个这种模式的锁。通常,任何只读取表而不修改它的查询都将获得这种锁模式。
ROW SHARE 与EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。SELECT FOR UPDATE和SELECT FOR SHARE命令在目标表上取得一个这种模式的锁 (加上在被引用但没有选择FOR UPDATE/FOR SHARE的任何其他表上的ACCESS SHARE锁)。
ROW EXCLUSIVE 与SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。命令UPDATE、DELETE和INSERT在目标表上取得这种锁模式(加上在任何其他被引用表上的ACCESS SHARE锁)。通常,这种锁模式将被任何修改表中数据的命令取得。
SHARE UPDATE EXCLUSIVE 与SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。这种模式保护一个表不受并发模式改变和VACUUM运行的影响。由VACUUM(不带FULL)、ANALYZE、CREATE INDEX CONCURRENTLY和ALTER TABLE VALIDATE以及其他ALTER TABLE的变体获得。
SHARE 与ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。这种模式保护一个表不受并发数据改变的影响。由CREATE INDEX(不带CONCURRENTLY)取得。
SHARE ROW EXCLUSIVE 与ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。这种模式保护一个表不受并发数据修改所影响,并且是自排他的,这样在一个时刻只能有一个会话持有它。由CREATE TRIGGER和很多 ALTER TABLE的很多形式所获得(见 ALTER TABLE)。
EXCLUSIVE 与ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。这种模式只允许并发的ACCESS SHARE锁,即只有来自于表的读操作可以与一个持有该锁模式的事务并行处理。由REFRESH MATERIALIZED VIEW CONCURRENTLY获得。
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和REFRESH MATERIALIZED VIEW(不带CONCURRENTLY)命令获取。ALTER TABLE的很多形式也在这个层面上获得锁(见ALTER TABLE)。这也是未显式指定模式的LOCK TABLE命令的默认锁模式。

提示: 只有一个ACCESS EXCLUSIVE锁阻塞一个SELECT(不带FOR UPDATE/SHARE)语句。
一旦被获取,一个锁通常将被持有直到事务结束。 但是如果在建立保存点之后才获得锁,那么在回滚到这个保存点的时候将立即释放该锁。 这与ROLLBACK取消保存点之后所有的影响的原则保持一致。 同样的原则也适用于在PL/pgSQL异常块中获得的锁:一个跳出块的错误将释放在块中获得的锁。

行级锁

一个事务可能会在相同的行上保持冲突的锁,甚至是在不同的子事务中。但是除此之外,两个事务永远不可能在相同的行上持有冲突的锁。行级锁不影响数据查询,它们只阻塞对同一行的写入者和加锁者。

PostgreSQL中的事务、隔离和锁_第3张图片

FOR UPDATE FOR UPDATE会导致由SELECT语句检索到的行被锁定,就好像它们要被更新。这可以阻止它们被其他事务锁定、修改或者删除,一直到当前事务结束。
FOR NO KEY UPDATE 行为与FOR UPDATE类似,不过获得的锁较弱:这种锁将不会阻塞尝试在相同行上获得锁的SELECT FOR KEY SHARE命令。任何不获取FOR UPDATE锁的UPDATE也会获得这种锁模式。
FOR SHARE 行为与FOR NO KEY UPDATE类似,不过它在每个检索到的行上获得一个共享锁而不是排他锁。一个共享锁会阻塞其他事务在这些行上执行UPDATE、DELETE、SELECT FOR UPDATE或者SELECT FOR NO KEY UPDATE,但是它不会阻止它们执行SELECT FOR SHARE或者SELECT FOR KEY SHARE。
FOR KEY SHARE 行为与FOR SHARE类似,不过锁较弱:SELECT FOR UPDATE会被阻塞,但是SELECT FOR NO KEY UPDATE不会被阻塞。一个键共享锁会阻塞其他事务执行修改键值的DELETE或者UPDATE,但不会阻塞其他UPDATE,也不会阻止SELECT FOR NO KEY UPDATE、SELECT FOR SHARE或者SELECT FOR KEY SHARE。

PostgreSQL不会在内存里保存任何关于已修改行的信息,因此对一次锁定的行数没有限制。 不过,锁住一行会导致一次磁盘写,例如, SELECT FOR UPDATE将修改选中的行以标记它们被锁住,并且因此会导致磁盘写入。

死锁

死锁的4个必要条件:互斥条件、请求与保持条件、不可剥夺条件、循环等待条件。
防止死锁的最好方法通常是保证所有使用一个数据库的应用都以一致的顺序在多个对象上获得锁。我们也应该保证一个事务中在一个对象上获得的第一个锁是该对象需要的最严格的锁模式。如果我们无法提前验证这些,那么可以通过重试因死锁而中断的事务来即时处理死锁。

只要没有检测到死锁情况,寻求一个表级或行级锁的事务将无限等待冲突锁被释放。这意味着一个应用长时间保持事务开启不是什么好事(例如等待用户输入)。

参考:
1.《POSTGRESQL修炼之道——从小工到专家》
2. PostgreSQL 9.6.0 手册——http://www.postgres.cn/docs/9.6/mvcc.html

本文对于表锁和行锁的理解不深刻,还有待总结整理。

你可能感兴趣的:(PostgreSQL)