第十三章 PostgreSQL的并发控制
作者:瀚高PG实验室(Highgo PG Lab) 丹心明月
注:本文章主要翻译自《PostgreSQL 13.0 Documentation》第十三章
本章介绍当多个会话同时尝试对相同数据进行访问时,PostgreSQL的行为。在此情形,目标是,在保证数据严格一致性的前提下,尽可能保证访问有效率。所有数据库应用程序开发者均需熟悉此章所述内容。
13.1 并发控制简介
PostgreSQL提供了多种方式以控制对数据的并发访问。在数据库内部,数据的一致性使用多版本模式(多版本并发控制(Multiversion Concurrency Control),即MVCC)维护。这意味着每个SQL语句查询到的数据,是查询开始时间节点的快照(一个数据版本),而与查询期间数据状态无关。此机制确保语句不会查询到由并发事务对同一行数据进行修改而产生的不一致数据,从而为每个数据库会话提供了事务隔离特性。MVCC通过避免传统数据库系统中的锁定方法,最大程度上减小了在多用户并发场景下的锁争用,从而提高了性能。
使用MVCC而不使用锁定进行并发控制最大的优势在于,MVCC中读数据不会阻塞写数据;写数据同样也不会阻塞读数据。PostgreSQL及时在最严格的事务隔离级别下,仍可保证此行为。
PostgreSQL还提供了表级锁和行级锁。不过,MVCC会提供比锁更好的性能。此外,应用程序定义的咨询锁提供了一种获取与单个事务无关的锁的机制。
13.2 事务隔离
SQL标准定义了四种事务隔离级别。最严苛的级别是序列化(serializable,即依次执行)。其他三个级别是根据并发事务之间的交互作用产生的现象定义的,每个级别对应于不能在此级别产生的现象。标准中指出,在序列化级别上,所有这些现象都不可能发生。(这不足为奇,既然事务都是序列化执行了,那还有什么所谓的交互呢?)
各级别上禁止的事务交互现象有:
脏读
事务读取到另一并发事务未提交的数据。
不可重复读
同一事务内,读取之前读过的数据时,发现读取到了开始查询时间点之后其他事务修改后的数据。
幻读
同一事务,重复执行查询出来的符合条件行数,受另一最近提交的事务影响。
序列化异常
一次提交一组事务,与随机顺序单个事务运行的结果不同。
表13.1列出了SQL标准及PostgreSQL实现的事务隔离级别:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 序列化异常 |
读未提交 | 允许,但PG中不允许 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 可能 | 可能 | 可能 |
重复读 | 不可能 | 不可能 | 允许,但PG中不允许 | 可能 |
序列化 | 不可能 | 不可能 | 不可能 | 不可能 |
PostgreSQL可以指定四种隔离级别中的任一种,但实际上,其内部仅支持三种隔离级别,即PostgreSQL中的读未提交与读已提交是一样的。这是因为读已提交是PostgreSQL中MVCC架构的最低隔离级别。
上表还显示,PostgreSQL中的重复读隔离级别不允许幻读。SQL标准中,只是指定哪种现象不能在某一隔离级别中出现,而为定义哪种现象必须在哪一隔离级别中出现。PostgreSQL中相关隔离级别的行为,在下面章节中细讲。
可使用SET TRANSACTION命令设置事务隔离级别。
注:
有些PostgreSQL的数据类型和函数对于事务行为有其特殊的规则。特别的,对于序列的更改(以及使用serial定义的列)会立马对其他事务可见,且若取消更改也不会回滚。详情参见第9.17节和8.1.4节。
13.2.1 读已提交事务隔离级别
读已提交为PostgreSQL中默认的事务隔离级别。事务使用此级别,SELECT查询(不带FOR UPDATE/SHARE子句)仅会看到在查询开始时间点之前、已提交的数据;不会查询到未提交数据,也不会查询到该时间点之后提交的数据。实际上,实际上,SELECT读取到的是查询执行时数据库的一个快照。不过,SELECT可以看到当前事务内执行的更新操作,即使该操作还未提交(即,事务隔离仅隔离其他事务的影响)。请注意,即使在同一事务内的两个相同SELECT语句,只要在两个SELECT中间,其他事务对相关数据有修改,那么查询到的结果也会不同。
UPDATE,DELETE,SELECT FOR UPDATE和SELECT FOR SHARE命令在查询目标行时的机制与SELECT相同:仅检索、修改在命令执行前已提交的数据。不过,如果事务B在执行修改时,有并行执行的事务A对相关行先进行了操作或对行持有了锁,那么事务B会等待前面的事务A提交或回滚。如果前面的事务A执行了回滚,那么事务B会对之前查到的数据进行更新修改;如果前面的事务A执行了提交,如果事务A是删掉了这一行,那么事务B会忽略这一行;如果是更新了这一行,那么事务B会尝试更新这一行的最新版本。事务B会重新确认,修改后的行的新版本是否仍符合命令中的检索条件,如果符合,则会更新。若在SELECT FOR UPDATE和SELECT FOR SHARE中,则意味着该最新行处于锁定中,并会将锁定状态告知客户端。
带有ON CONFLICT DO UPDATE子句的INSERT语句,机制类似。在读已提交模式下,所有要插入的行,如果不发生不相关的错误,那么要么执行插入,要么执行更新。如果冲突是由另一个对该INSERT还不可见的事务引起的,UPDATE命令仍会受该行影响。
理解实验:
postgres=# show transaction_isolation ;
transaction_isolation
-----------------------
read committed
(1 row)
--首先有表及数据:
postgres=# \d test
Table "public.test"
Column | Type | Collation | Nullable | Default
--------+------------------------+-----------+----------+---------
id | integer | | not null |
name | character varying(100) | | |
Indexes:
"test_pkey" PRIMARY KEY, btree (id)
postgres=*# select * from test;
id | name
----+-------
1 | one
2 | two
3 | three
(3 rows)
--启动事务A,插入id为5的行,且不提交:
postgres=# begin;
BEGIN
postgres=*# insert into test values(5,'five2');
INSERT 0 1
postgres=*#
--在另一事务B执行会冲突的insert:
postgres=# begin;
BEGIN
postgres=*# select * from test;
id | name
----+-------
1 | one
2 | two
3 | three
(3 rows)
postgres=*# insert into test values(5,'five') on conflict(id) do update set name='conf_upd'; --命令卡住
--事务A commit后,事务B命令执行完成,commit之后查看结果:
postgres=# select * from test;
id | name
----+----------
1 | one
2 | two
3 | three
5 | conf_upd
(4 rows)
postgres=#
带有ON CONFILCT DO NOTHING子句的INSERT语句,可能会因为其他暂对INSERT快照不可见的事务而导致不插入任何行。在此提示:读已提交模式。
因为以上规则,所以对于更新命令,可能会看到一个不一致的快照:它可以看到并行执行的更新命令对于同一行的处理,不过看不到那些对其他行的处理。该行为使得读已提交模式不太适用于包含复杂查询的情形。例如,考虑使用类似于如下的事务对银行账号进行更新:
BEGIN;
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534;
COMMIT;
如果有两个此类事务并行改变账号12345的存款,那么很明显的,我们需要第二个事务依据第一个事务更新后的数据进行后处理。因为每个命令仅影响预定的行,故让其看到行更新版本并不会造成有碍观瞻的不一致。
在读已提交模式中,有更多会造成意料之外结果的复杂用法。例如,假设DELETE命令操作数据,而其限定的数据却被另一个命令添加和删除。例如,假设website是一个有两行数据的表,其hits列的数据为9和10:
BEGIN;
UPDATE website SET hits = hits + 1;
-- run from another session: DELETE FROM website WHERE hits = 10;
COMMIT;
虽然hits列在UPDATE之前和之后均存在,但是DELETE语句却不会删到任何数据。这是因为delete跳过了更新之前值为9的行,当UPDATE命令结束,DELETE陷入锁等待,而新行的值由10变为了11,故而删不到数据了。
由于读已提交模式会以一个新快照开始每个命令,其中快照包括该瞬间之前已提交的所有事务,因此在任何情况下,同一事务中的后续命令都会看到已提交并发事务的影响。上面的问题是单个命令是否可看到数据库的绝对一致视图。
读已提交模式提供的部分事务隔离足以满足许多应用程序的需要,并且该模式使用起来快速简便。但是,这还不足以适用于所有情况。与读已提交模式相比,执行复杂查询和更新的应用程序可能需要一个更加严格的数据库一致视图。
13.2.2 重复读事务隔离级别
重复读事务隔离级别仅可看到在事务开始前已提交的数据;绝不会看到未提交数据或事务执行期间由并行事务提交的修改。(不过,select可以看到同一事务内的修改,即使该修改未提交。)PostgreSQL提供了比SQL标准要求的更严格的隔离保证,并防止表13.1中列出的,除序列化异常外所有现象的发生。
该级别与读已提交不同之处在于,查询看到的快照,是事务中第一个非事务控制语句开始时的快照,而不是该事务中当前语句开始时的快照。因此,同一事务中的SELECT命令,多次执行,将看到相同的数据,即:看不到事务开始之后其他事务提交的更改。(即,是事务开始时的快照,而不是事务中语句开始时的快照。)
使用该级别的应用程序需要设置重新执行因序列化失败而停止的事务。
UPDATE,DELETE,SELECT FOR UPDATE和SELECT FOR SHARE命令在检索目标行的机制上,与SELECT相同:仅会影响在事务执行时已提交的数据。不过,在检索、处理到相关行的时候,行有可能被其他并行事务修改、删除或锁定。此情形下,可重复读事务A将等待正在操作该行的事务B提交或回滚。如果事务B回滚,那么事务A照常运行;但如果事务B提交,那么可重复读事务A会回滚,并报错:
ERROR: could not serialize access due to concurrent update
因为可重复读事务无法处理在事务开始后其他事务所处理后的行。
若应用程序收到该错误信息,那么其应回滚当前事务,并从头开始,重新执行该事务。
请注意,仅修改性的事务有必要重试;只读事务永不会有序列化争用问题。
可重复读模式严格保证事务看到的数据库快照的稳定性。不过该快照并不一定总是与同一级别的并发事务的某些串行执行一致。例如,即使是该级别下的只读事务,也可能看到表明批次已完成的控制记录,但却看不到逻辑上应该属于该批次的某些明细记录,因为该只读事务读取到的是该控制记录的较早版本。若试图在此隔离级别下,使用事务来强制执行业务规则,如果不谨慎使用显式锁以阻止冲突事务,那可能会无法如期运转。
可重复读隔离级别使用快照隔离(snapshot isolation)技术实现。这与减少并发性,使用传统锁技术的系统相比,可能会观察到行为和性能上的差异。一些系统甚至提供具有不同行为的可重复读和快照隔离两种隔离级别。SQL标准对此两种隔离级别的区别进行了规范化。
13.2.3 序列化事务隔离级别
序列化事务隔离级别提供最高的事务隔离级别。该级别模拟已提交事务的串行执行,就像事务是依次而非并发的执行。不过,类似于可重复读级别,使用此级别的应用程序也应准备好事务重试以应对序列化失败。实际上,此隔离级别除了会监控并行执行事务与串行执行这些事务不一致的情况外,其他与可重复读的工作原理一样。该监控不会引入可重复读中除阻塞以外的阻塞,但监控会有一些开销,并且检测到的会造成序列化异常的情形会触发一个序列化失败。
示例如下,假设有表mytab,含数据:
class | value
-------+-------
1 | 10
1 | 20
2 | 100
2 | 200
假设序列化事务A执行:
SELECT SUM(value) FROM mytab WHERE class = 1;
然后将结果插入表,数据为class=2,value=30。与此同时(并行),序列化事务B执行:
SELECT SUM(value) FROM mytab WHERE class = 2;
将得到的结果300作为value值,class=1插入表。然后两个事务同时进行提交。如果事务均为可重复读模式,那么均允许提交;不过因为没有与结果一致的串行执行顺序,因此使用序列化事务将允许提交一个事务,另一个事务会返回以下消息并回滚:
ERROR: could not serialize access due to read/write dependencies among transactions
这是因为,如果A在B之前提交,那么B会得到和为330,而非300;同样,如果反过来,A获得的结果也会不同。
当依赖于序列化事务防止异常时,只读取已提交数据至关重要。这对于只读事务同样适用(不过,在延迟只读事务中读取的数据,在读取时即可见,因为这样的事务会等待直到其可获取没有此类问题的快照后,才会开始读取数据)。在所有其他情况下,应用程序都不应依赖于随后会终止事务读取到的结果;而应重试事务,直到其成功。
PostgreSQL使用谓词锁定(predicate locking)保证序列化。这意味着它会保持锁定,这些锁能够让它判断,在它先运行的情况下,一个写操作会在何时影响一个并发事务中之前读取到的结果。在PostgreSQL中,这些锁不会导致任何阻塞,因此不会造成死锁。它们用来定义和标识并行序列化事务之间的依赖性。相反,在读已提交或可重复读事务隔离级别中,要保证数据的一致性,可能需要在整张表加锁,从而可能阻塞其他用户对该表的使用;或者可能使用SELECT FOR UPDATE或者SELECT FOR SHARE语句,这不仅仅会导致阻塞,还会导致磁盘读。
PostgreSQL中的谓词锁定,与其他数据库系统一样,是基于事务实际读取的数据。它在pg_locks中以mode为SIReadLock展示。在查询执行期间获得何种特定锁取决于查询所使用的执行计划;并且可以在事务过程中,组合多种细粒度锁(例如行锁)和粗粒度锁(例如页级锁)以防止用于跟踪锁定的内存耗尽。在只读事务中,如果没有可能会引起序列化异常的冲突,那么它可能在完成前就会释放掉SIRead锁。实际上,只读事务往往可以在开始运行时就会释放该锁以防止进行谓词锁定。如果执行事务时显式指定SERIALIZABLE READ ONLY DEFERRABLE,那么该事务会在检测完成前保持谓词锁定(此为序列化事务会阻塞,而可重复读事务却不会阻塞的唯一场景)。另一方面,SIRead锁通常需要保留到事务提交之后,直到重叠的读写事务提交为止。
序列化事务的使用可以简化开发。它的机制保证了可以不考虑事务的执行顺序,甚至不需要考虑事务所执行的内容、以及事务是否能够成功执行。不过使用此技术,有一个处理序列化错误(SLSTATE值为40001)的机制很重要,因为很难准确的预测哪些事务是读写依赖项,并需要回滚以防止序列化异常。虽然监控读写依赖项会有消耗,重新执行因为序列化错误而终止的事务也会有消耗;但是综合考虑使用显式锁以及SELECT FOR UPDATE或SELECT FOR SHARE所带来的消耗及阻塞,在某些场景下,序列化事务仍为最优选。
尽管PostgreSQL中的序列化事务隔离级别,在可证明并发事务执行结果与执行顺序无关(或存在执行依赖)时,才允许并发事务提交,但却无法始终防止在实际串行执行中不会发生的异常。实际上,即使在插入键之前显式的检查出该键不存在表中,也可能会因为重叠的序列化事务而导致唯一约束冲突。这可以通过显式确认所有插入可能重复键的序列化事务是否可执行来规避。例如,应用程序在插入新键前先检索一下以确认其不存在,或者通过检索出现存键的最大值,将其加一作为新键值。即使在并发事务串行执行的情况下不会发生唯一约束冲突,如果序列化事务不遵守此协议,而直接插入新值,那也可能会造成唯一约束冲突。
在使用序列化事务进行并发控制时,为获得最优性能,需考虑:
-
如果可以,就将事务声明为只读;
-
控制活动连接数,如果需要的话,使用连接池。这一点很重要,且对于使用序列化事务的繁忙系统尤为重要;
-
为保护完整性,尽量在事务中仅执行所需的操作;
-
执行完即断掉连接,不要使连接处于“idle in transaction”状态。可以使用idle_in_transaction_session_timeout来自动断开超时会话;
-
取消显式锁,SELECT FOR UPDATE和SELECT FOR SHARE,因为序列化事务会自动提供这些保护;
-
当因为谓词锁表内存不足时,系统会强制将多个页级谓词锁组合到单个关系级谓词锁中,此时序列化失败率可能会增加。此问题可以通过加大max_pred_locks_per_transaction,max_pred_locks_per_relation和/或max_pred_locks_per_page来解决。
-
顺序扫描将始终需要关系级别的谓词锁定。这会导致序列化失败率的提高。该情形可通过适当减少random_page_cost和/或cpu_tuple_cost以提高索引扫描的使用。需要权衡事务回滚和重启与查询执行时间总体变化之间的关系。
序列化事务隔离级别使用序列化快照隔离(基于在快照隔离中添加检查序列化异常)实现。相较于使用传统锁定技术的系统,会有行为及性能上的差异。
13.3 显式锁
PostgreSQL提供多种锁模式以控制对表数据的并发访问。应用程序可以利用这些锁模式以实现MVCC无法提供的机制。当然,PostgreSQL中大部分命令在执行时会自动获取适当的锁以确保相关的表不被删掉或更改。(例如,对同一表,TRUNCATE命令不能与其他命令并行执行,故该命令会获取在该表上的排他锁。)
可通过查询pg_locks系统视图查看当前数据库服务中的锁信息。更多有关监控锁管理状态的信息,请参见第27章。
13.3.1 表级锁
以下列出了可用的锁模式,以及在何种场景下PostgreSQL会自动使用它们。也可以使用LOCK命令显式地获取这些锁。请谨记,所有这些锁模式均为表级锁,即使锁名中包含“行”(这种命名是历史遗留问题)。名称在某种程度上反应了每个锁模式的典型用法--但语义都相同。各种锁模式之间的唯一真正区别就是锁模式之间的冲突。两个事务不能同时在同一张表获取相互冲突的锁模式。(不过,事务本身无冲突,例如,对于同一张表,它可以先获得ACCESS EXCLUSIVE锁,随后又获得ACCESS SHARE锁。)多个事务可以并行获取不冲突的锁模式。请注意,有些锁模式,与自身会产生冲突(例如,同时仅可以有一个事务获得ACCESS EXCLUSIVE锁);有些却不会(例如,多个事务可同时获取ACCESS SHARE锁)。
表级锁模式
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锁模式冲突。此模式可防止表发生并发模式(schema)更改和VACUUM。
由以下命令获取:VACUUM(无FULL)、ANALYZE、CREATE INDEX CONCURRENTYLY、REINDEX CONCURRENTLY、CREATE STATISTICS以及特殊的ALTER INDEX和ALTER TABLE变体(更多新详情请参见命令ALTER INDEX和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的某些格式获取。
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)。该模式确保仅持有该锁的事务访问该目标表。
由命令DROP TABLE,TRUNCATE,REINDEX,CLUSTER,VACUUM FULL和REFRESH MATERIALIZED VIEW(无CONCURRENTLY)获取。ALTER INDEX和ALTER TABLE的一些模式同样获取此级别的锁模式。这也是LOCK TABLE语句的默认锁模式。
仅ACCESS EXCLUSIVE锁会阻塞SELECT(无FOR UPDATE/SHARE)语句。
锁一旦获取,一般会持续到事务结束。不过如果锁是在创建快照之后获取的,那么在回滚快照时会立马释放锁。这与ROLLBACK命令取消在快照后所有命令的影响这一原则相一致。PL/pgSQL异常块中获取的锁也是如此:从异常块中抛出的错误会释放块中获得的锁。
冲突锁模式
请求的锁模式 | 已存在的锁模式 | |||||||
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 |
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 |
13.3.2 行级锁
除了表级锁,还有行级锁,以下列出了行级锁及在什么情况下PostgreSQL会自动的使用它们。请注意,在不同的子事务中,事务可以在同一行上获得冲突的锁;但除此之外,两个事务永远不能在同一行上获得冲突的锁。行级锁不会影响检索数据;它们仅会阻塞对同一行的写入及锁。如表级锁一样,行级锁仅会在事务结束或快照回滚时释放。
行级锁模式
FOR UPDATE
FOR UPDATE将SELECT语句返回的行锁定用于更新。这可以防止这些行在事务结束之前被其他事务锁定、修改或删除。也就是说,其他尝试针对这些行执行UPDATE,DELETE,SELECT FOR UPDATE,SELECT FOR NO KEY UPDATE,SELECT FOR SHARE或者SELECT FOR KEY SHARE的事务在当前事务结束之前会一直被阻塞;相反的,SELECT FOR UPDATE将会等待在同一行执行这些命令的并行事务,然后会返回更新后的行(如果这些行被删除了,那么不会返回行)。不过,在可重复读或序列化事务中,如果被锁定的行在事务开始后被修改了,那么会抛出错误。更多信息,请参见第13.4节。
DELETE行以及UPDATE某一列值,也会获取FOR UPDATE锁模式。当前,UPDATE情况下考虑的为可以在其上有唯一索引、可用于外键的列(所以不考虑部分索引和表达式索引),不过将来这种机制可能会改变。
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修改选取的行以将其标记为锁定,从而会导致磁盘写。
行级锁冲突
请求的锁模式 | 当前锁模式 | |||
FOR KEY SHARE | FOR SHARE | FOR NO KEY UPDATE | FOR UPDATE | |
FOR KEY SHARE |
|
|
| X |
FOR SHARE |
|
| X | X |
FOR NO KEY UPDATE |
| X | X | X |
FOR UPDATE | X | X | X | X |
13.3.3 页级锁
除了表级锁和行级锁,还有页级共享/排他锁用以控制对于共享缓冲池中表页的读/写。这些锁在行被获取或更新后立马释放。应用程序开发者一般无需关系此类锁。
13.3.4 死锁
对于显式锁的使用,会提高死锁(即在两个或以上事务中,相互持有对方需要获得的锁)的发生概率。例如,如果事务1在表A上获取到一个排它锁,接下来想要获得表B的排它锁;而事务2已经获得了表B的排它锁,接下来想要获得表A的排它锁,这样两个事务就都不能执行了。PostgreSQL会自动检索到死锁,并通过终止其中一个事务、允许另一个事务执行来解决死锁。(具体哪个事务会被终止很难预测。)
请注意,行级锁也有可能导致死锁(也就是说,即使没有显式使用锁,也可能会发生死锁)。假设如下场景:两个并行事务更新同一张表。
第一个事务执行以下语句:
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 11111;
以上语句在特定acctnum上获得了行级锁。然后,第二个事务执行:
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 22222;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 11111;
第一个UPDATE在特定行上获得了行锁,所以成功更新了该行。不过第二个UPDATE访问的行却已被锁定,所以它会等待持有锁的事务结束。所以,现在事务2在等待事务1执行结束。然后,事务1执行了以下语句:
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 22222;
事务1尝试在指定行获取行级锁,但却发现事务2已经持有该行级锁。所以事务1开始等待事务2执行完成。此时,事务1等待事务2,事务2等待事务1:死锁。PostgreSQL会检测到死锁,并终止其中一个事务。
解决死锁的最好办法,是通过确定所有应用程序以一致性的顺序在多对象上使用数据库锁,避免死锁。例如上例中,如果两个事务均使用相同的顺序更新行,那么就不会发生死锁。还应该确保在事务中对某个对象获取的第一个锁是所需的最严格的模式。如果无法事先验证,则可以通过重试由于死锁而中止的事务来即时处理死锁。
如果没有发生死锁,那么想要获得行级或表级锁的事务会一直等待冲突的锁释放。也就意味着,应用程序运行长事务是一个很不明智的行为(例如,等待用户输入)。
13.3.5 咨询锁
PostgreSQL提供一种创建具有应用程序定义含义的锁的方法。它们被叫做咨询锁(advisory lock),因为系统并不强制使用它们--由应用程序确保正确的使用它们。咨询锁对于并不适用于MVCC模式的锁策略很有用。例如,咨询锁的常见用法是模拟所谓的“平面文件”数据管理系统特有的悲观锁策略。虽然存储在表中的标志可以用于相同的目的,但咨询锁更快,可以避免表膨胀,并在会话结束时由服务器自动清理。
在PostgreSQL中有两种方式获取咨询锁:在会话级别或在事务级别。一旦在会话级别获得咨询锁,那么直到显式释放或会话终止才会释放。不同于标准的锁请求,会话级别的咨询锁请求不遵循事务语义:在事务中获得的锁定,即使事务回滚,依旧持有该锁定;并且即使调用的事务失败,解锁操作依旧有效。锁可以通过其拥有的进程多次获得;每个完成的锁请求必然有相应的解锁请求。另一方面,事务级别的锁请求,其机制与常规锁请求类似:它们在事务结束后自动释放,并没有显式解锁操作。对于短期使用咨询锁,此机制较会话级别的行为更加便捷一些。对于相同咨询锁标志的会话级别和事务级别的锁请求会阻塞彼此。如果一个会话已经持有了给定的咨询锁,即使其他会话正在等待该锁,该会话依旧可以成功执行其他请求;无论现在持有锁的和新的请求是会话级别还是事务级别,均如此。
类似于PostgreSQL中的所有锁,任意会话当前所持有的的咨询锁完整列表,可在系统视图pg_locks中查看。
咨询锁和常规锁都存储在一个由max_locks_per_transaction和max_connections参数定义大小的共享内存池中。一定小心不要耗尽了该内存,不然的话数据库服务将无法再授予锁。这对数据库服务能够授予的咨询锁的数量限定了上限,取决于数据库的配置,一般是数万至数十万。
在某些情况下使用咨询锁方法,特别是在包含显式排序和LIMIT自字句查询中,由于需要评估SQL表达式的顺序,所以一定要小心控制锁的获得。例如:
SELECT pg_advisory_lock(id) FROM foo WHERE id = 12345; -- ok
SELECT pg_advisory_lock(id) FROM foo WHERE id > 12345 LIMIT 100; --危险!
SELECT pg_advisory_lock(q.id) FROM
(
SELECT id FROM foo WHERE id > 12345 LIMIT 100
) q; -- ok
在上面的查询中,第二种方式很危险,因为不能保证在执行锁定函数之前先应用LIMIT。这可能会导致意料之外的锁定,从而导致释放失败(直到会话终止)。从应用程序的角度来看,这些锁尽管在pg_locks中仍然可见,但却是悬而未决的。
在9.27.10节中介绍了用于操作咨询锁的函数。
13.4 应用程序级别的数据一致性检查
使用读已提交事务来强制执行与数据完整性相关的业务规则非常困难,因为数据的快照随每个语句而变化,且如果发生写冲突,那么即便是单条语句也可能不会将自己限制为语句开始时的快照。
虽然可重复读事务在执行期间保持数据的稳定视图,但是使用MVCC快照进行数据的一致性检查存在一个小问题,其中涉及一些读/写冲突。如果一个事务写入数据,另一个并行事务尝试读取同一数据(无论是在写入之前还是之后),则它看不到另一个事务所进行的修改。无论谁先开始或先提交,读事务似乎都是先执行的。就目前来看,这没什么问题,但如果读事务还写入在前面提到的两个事务之前运行的并发事务要读取的数据,则可能有问题。如果看起来最后执行的事务,最先提交,那么很容易在事务执行顺序上形成一个环。如果这种环发生,那么在没有帮助的前提下,则完整性检查将很难正确工作。
如13.2.3节所述,序列化事务只是在可重复读事务中添加了对读写冲突的监视。当检测到在执行顺序上会导致循环时,所涉及到的事务之一将被回滚以打破该循环。
13.4.1 使用序列化事务强制使数据一致
如果使用序列化事务隔离级别保证所有读写的数据一致性数据视图,则无需做其他工作来保证一致性。在这方面,使用序列化事务来保证一致性的软件,在PostgreSQL中应该是可以正常工作的。
使用此技术时,如果应用程序软件使用自动重试因序列化失败而回滚的事务的框架,将避免给应用程序程序员造成不必要的负担。将default_transaction_isolation设置为serializable可能是一个好主意。通过使用触发器检查事务隔离级别,以确保不使用其他事务隔离级别,以免有意或无意的破坏完整性检查,也是一个明智的选择。
性能方面的建议,请参见第13.2.3节。
请注意,使用序列化事务进行完整性保护的策略,当前还不能用在热备模式中(参见第26.5节)。所以,在热备模式下,可能就需要使用可重复读模式和在主节点使用显式锁来控制。
13.4.2 使用显式锁强制数据一致
当可能发生非序列化写的时候,为保护行的当前有效性并保护它免受并发更新, 则必须使用SELECT FOR UPDATE,SELECT FOR SHARE或适当的LOCAK TABLE语句。(SELECT FOR UPDATE 和SELECT FOR SHARE仅锁定检索行以防止并行更新,而LOCK TABLE会锁定整个表)。在将应用程序从其他环境迁移到PostgreSQL数据库时,需要将这考虑在内。
同时请注意,事实是,从其他环境转换过来的SELECT FOR UPDATE可能无法保证并发事务不会更新或删除选定的行。为此,在PostgreSQL中,即使不需要更改任何值,也必须实际上更新行。SELECT FOR UPDATE暂时阻塞其他事务获取相同的锁或执行会影响锁定行的UPDATE或DELETE,但是一旦持有此锁的事务提交或回滚,除非在锁定时对行进行了实际的UPDATE,否则被阻塞的事务将继续执行之前冲突的操作。
对于非可序列化的MVCC,全局有效性检查需要进行额外的考虑。例如,在银行应用程序中,当两个表都被主动更新时,可能需要检查一个表中的所有贷项总和是否等于另一表中的借项总和。在读已提交模式下,比较两个SELECT sum(...)命令的结果可能并不可靠,因为第二个查询可能会包含第一个查询未计算在内的事务结果。在单个可重复读事务中执行两个求和操作,将仅给出可重复读事务开始之前提交的事务的效果的准确快照-但人们可能会合理地怀疑,查询出来的结果是否仍是执行时所要的结果。如果可重复读事务本身在尝试进行一致性检查之前先进行了一些更改,则检查的有用性将变得更加值得商榷,因为现在它包括了一些但不是全部的事务开始后的更改。在这种情况下,谨慎的人可能希望锁定检查所需的所有表,以便对当前现状有一个无可争辩的快照。SHARE(或更高)锁模式可确保除了当前事务之外,在锁定表中没有未提交的更改。
同样请注意,若基于显式锁进行并行更改控制,无论使用读已提交模式或可重复读模式,在执行查询前要谨慎处理锁的获取。可重复读事务获取的锁可确保没有其他修改该表的事务仍在运行,但是如果事务看到的快照早于获得锁的时间,则它可能包含表中一些现在提交的变更。可重复读事务的快照一般在事务第一次查询或数据修改命令(SELECT,UPDATE,INSERT,DELETE)开始时冻结,因此可以在快照冻结之前显式获得锁。
13.5 并发控制注意事项
一些DDL命令(当前仅TRUNCATE和ALTER TABLE命令中进行表重写操作的命令)并不是MVCC安全的。也就意味着,在truncate或表重写提交后,如果并行事务使用的是DDL命令提交之前的快照,那么对于并行事务来说,表看起来可能是空的。对于仅在DDL命令开始之前未访问相关表的事务,这会是一个问题--这样的事务至少会对表持有ACCESS SHARE表级锁,从而在事务终止前,都会阻塞其他DDL操作。因此,对于在目标表上进行的连续查询,这些命令不会在表内容中引起任何明显的不一致,但是它们可能会导致目标表的内容与数据库中其他表之间的明显不一致。
对于热备(参见第26.5节)复制的目标端,暂未支持序列化事务隔离级别。当前热备中所支持的最高事务隔离级别为可重复读。热备环境中,对于源端执行的可序列化事务,在备端,仅可实现最终的一致。
不能使用当前事务的隔离级别对系统目录进行内部访问。这意味着新创建的数据库对象(例如表)对于并发的可重复读和序列化事务可见,不过它们所包含的行不可见。相反,在较高的隔离级别中,不能在系统视图中看到并发事务创建的数据库对象。
13.6 锁与索引
虽然PostgreSQL提供对表数据的非阻塞读/写,但是当前,并未为PostgreSQL中实现的每种索引访问方法提供非阻塞读/写访问功能。各种索引类型的处理方式如下:
B树索引,GiST和SP-GiST索引
使用短暂的共享/排他页级锁进行数据读/写访问。在索引行获取或插入后,立马释放锁。这些索引类型提供最高的并发,而不会造成死锁。
哈希索引
使用共享/排他hash-backet级别的锁进行数据读/写访问。在整个桶(bucket)处理完成后,立即释放锁。桶级别的锁比索引级别的锁提供了更好的并发,但因为桶级锁持有的时间较长,所以死锁的可能性变大。
GIN索引
使用短暂的共享/排他页级锁进行数据的读/写访问。在索引行获取或插入后,立马释放锁。但请注意,插入GIN索引值通常每行会产生几次索引键插入,因此GIN对于单个值的插入可能会做大量工作。
当前,在并发应用程序中,B树索引提供最好的性能;因为B树索引比其他索引提供更多功能,所以在需要索引标量数据的并发应用程序中,建议使用B树索引。而对于非标量数据,建议使用GiST,SP-GiST或GIN索引。