概述
事务隔离级别
读已提交隔离级别
隔离级别特性
数据访问行为
特定命令行为
复杂情况下的问题
可重复读隔离级别
主要特点
行为差异
事务重试
技术实现
历史背景
串行化隔离级别
严格事务隔离
事务重试需求
数据读取的有效性
性能与开发优势
性能优化建议
特殊情况处理
配置调优
技术实现
显式锁定
表级锁
以下列表展示了PostgreSQL中可用的锁模式及其自动使用的上下文。您也可以通过LOCK命令显式获取这些锁。请记住,所有这些锁模式都是表级锁,即使名称中包含“行”这个词,这也是一种历史遗留。在某种程度上,锁模式的名称反映了它们的典型用途——但语义都是相同的。不同锁模式之间的唯一真正区别在于它们与其他锁模式冲突的方式(见表13.2)。两个事务不能在同一表上同时持有冲突的锁模式。(然而,事务永远不会与自身冲突。例如,事务可以先获取一种锁,然后稍后在同一表上获取另一种锁。)非冲突的锁模式可以被多个事务同时持有。特别是需要注意的是有些锁模式是自冲突的(例如,一种锁模式一次只能被一个事务持有),而有些则不是自冲突的(例如,一种锁模式可以被多个事务同时持有)。
表级锁模式
ACCESS SHARE (AccessShareLock)
ROW SHARE (RowShareLock)
ROW EXCLUSIVE (RowExclusiveLock)
SHARE UPDATE EXCLUSIVE (ShareUpdateExclusiveLock)
SHARE (ShareLock)
SHARE ROW EXCLUSIVE (ShareRowExclusiveLock)
EXCLUSIVE (ExclusiveLock)
ACCESS EXCLUSIVE (AccessExclusiveLock)
提示
锁的生命周期
sted Lock Mode | Existing Lock Mode | |||||||
---|---|---|---|---|---|---|---|---|
ACCESS SHARE | ROW SHARE | ROW EXCL. | SHARE UPDATE EXCL. | SHARE | SHARE ROW EXCL. | EXCL. | ACCESS EXCL. | |
ACCESS SHARE | X | |||||||
ROW SHARE | X | X | ||||||
ROW EXCL. | X | X | X | X | ||||
SHARE UPDATE EXCL. | X | X | X | X | X | |||
SHARE | X | X | X | X | X | |||
SHARE ROW EXCL. | X | X | X | X | X | X | ||
EXCL. | X | X | X | X | X | X | X | |
ACCESS EXCL. | X | X | X | X | X | X | X | X |
行级锁
除了表级锁之外,PostgreSQL还支持行级锁,这允许更细粒度的并发控制。行级锁在不同的场景下由PostgreSQL自动应用,并且其冲突情况如表13.3所示。需要注意的是,一个事务可以在同一行上持有相互冲突的锁,即使这些锁在不同的子事务中;但是,两个不同的事务不能在同一行上同时持有冲突的锁。行级锁不会影响数据的查询,它们只阻止对相同行的数据修改和锁定操作。行级锁和表级锁一样,在事务结束或保存点回滚时释放。
行级锁模式
FOR UPDATE
当使用FOR UPDATE时,所检索的行将被锁定,如同为更新操作准备。这阻止了其他事务在此行上的锁定、修改或删除操作,直到当前事务结束。任何尝试在这些行上执行SELECT FOR UPDATE, UPDATE, DELETE, SELECT FOR NO KEY UPDATE, SELECT FOR SHARE, 或 SELECT FOR KEY SHARE的其他事务都将被阻塞,直到当前事务结束;反之,如果在事务中执行了这些命令之一,那么它将等待任何并发的事务完成,然后锁定并返回更新后的行(如果行被删除,则不返回行)。在REPEATABLE READ或SERIALIZABLE隔离级别下,如果要锁定的行自事务开始以来已发生变化,则会抛出错误。
FOR UPDATE锁模式也会被任何DELETE操作或更新特定列值的UPDATE语句获取。目前,对于UPDATE语句而言,考虑的列是那些具有可用于外键的唯一索引的列,不包括部分索引和表达式索引,但这在未来可能会改变。
FOR NO KEY UPDATE
类似于FOR UPDATE,但所获得的锁较弱:这种锁不会阻止尝试在同一行上获取锁的命令。此锁模式也由不获取任何锁的UPDATE语句获取。
FOR SHARE
类似于FOR NO KEY UPDATE,但获取的是共享锁而不是排他锁。共享锁阻止其他事务对这些行进行UPDATE, DELETE, 或 SELECT FOR UPDATE操作,但不影响SELECT FOR NO KEY UPDATE或SELECT FOR SHARE。
FOR KEY SHARE
类似于FOR SHARE,但锁更弱:UPDATE被阻止,但SELECT FOR NO KEY UPDATE不被阻止。键共享锁阻止其他事务执行UPDATE或任何改变键值的UPDATE操作,但它不会阻止SELECT FOR NO KEY UPDATE, SELECT FOR SHARE, 或 SELECT FOR KEY SHARE,也不会阻止SELECT, INSERT, DELETE。
行级锁冲突
不同行级锁模式之间的冲突。例如,如果一个事务正在使用FOR UPDATE锁,那么其他事务试图获取FOR KEY SHARE, FOR SHARE, 或 FOR NO KEY UPDATE锁将被阻止,直到FOR UPDATE锁被释放。
总结
页级锁
除了表级和行级锁,PostgreSQL还使用页级共享/排他锁来控制对共享缓冲池中表页的读写访问。这些锁在一行被检索或更新后立即释放。应用程序开发者通常不必关心页级锁,但为了完整性,这里提及了它们的存在。
死锁
显式锁的使用可能会增加死锁的发生几率,即两个或更多事务各自持有另一个事务所需的锁。例如,如果事务1获取了对表A的排他锁,然后尝试获取表B的排他锁,而此时事务2已经对表B获取了排他锁,并且现在想要获取表A的排他锁,那么这两个事务都无法继续。PostgreSQL能够自动检测到死锁情况,并通过终止其中一个涉及的事务来解决死锁,允许其他事务完成。(具体哪个事务被终止难以预测,不应依赖于此。)
值得注意的是,死锁也可能由于行级锁而发生(因此,即使没有使用显式锁,死锁也可能发生)。考虑两个并发事务修改同一张表的情况。第一个事务执行:
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 11111;
这将获取指定账户编号行的行级锁。然后,第二个事务执行:
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 22222;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 11111;
第一条语句成功获取了指定行的行级锁,因此成功更新了该行。但是,第二条语句发现它试图更新的行已经被锁定,所以它等待获取锁的事务完成。此时,事务二正在等待事务一完成才能继续执行。接着,事务一执行:
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 22222;
事务一尝试获取指定行的行级锁,但是它无法做到:事务二已经持有了这样的锁。因此,它等待事务二完成。这样,事务一被事务二阻塞,而事务二被事务一阻塞:形成了死锁条件。PostgreSQL会检测这种情况并终止其中一个事务。
防止死锁的最佳策略
通常,避免死锁的最好防御措施是确保所有使用数据库的应用程序以一致的顺序获取多个对象上的锁。在上面的例子中,如果两个事务都按照相同的顺序更新行,就不会发生死锁。还应确保事务中对对象首次获取的锁是最严格的模式,该事务对该对象将需要的。如果预先验证这一点不可行,那么可以实时处理因死锁而终止的事务,通过重新执行这些事务。
只要没有检测到死锁情况,寻求表级或行级锁的事务将无限期地等待冲突的锁被释放。这意味着应用程序长时间保持事务开放(例如,在等待用户输入时)是一个糟糕的想法,因为它可能导致其他事务的长时间等待。
总结
咨询锁(Advisory Locks)
PostgreSQL提供了创建具有应用程序定义意义的锁的手段,这些被称为咨询锁。之所以称为咨询锁,是因为系统本身并不强制其使用——应用层需要负责正确地使用它们。咨询锁对于那些不适合多版本并发控制(MVCC)模型的锁定策略特别有用。例如,咨询锁常用于模仿传统“平面文件”数据管理系统中的悲观锁定策略。尽管也可以通过存储在表中的标志实现类似目的,但咨询锁更快,避免了表膨胀问题,并且服务器会在会话结束时自动清理这些锁,无需应用层干预。
在PostgreSQL中,有两种方式可以获取咨询锁:会话级和事务级。一旦在会话级获取了咨询锁,除非明确释放或会话结束,否则锁将一直保持。与标准锁请求不同,会话级的咨询锁请求不受事务语义的影响:在后续回滚的事务中获取的锁仍将在回滚后保持,同样,解锁操作即便在调用事务失败后也是有效的。拥有锁的进程可以多次获取同一锁;每次成功的锁请求都必须有对应的解锁请求,锁才会真正释放。另一方面,事务级的锁请求行为更像常规的锁请求:它们在事务结束时自动释放,没有显式的解锁操作。这种行为对于短期使用咨询锁的情况往往比会话级的行为更为方便。相同咨询锁标识的会话级和事务级锁请求将以预期的方式相互阻塞。如果一个会话已经持有了给定的咨询锁,其额外的请求总是会成功,即使其他会话正在等待该锁;这一规则不论现有锁持有和新请求是在会话级还是事务级都适用。
与PostgreSQL中的所有锁一样,任何会话当前持有的所有咨询锁的完整列表可以在系统视图pg_locks中找到。
咨询锁和常规锁都存储在一个由配置变量max_locks_per_transaction和max_connections定义大小的共享内存池中。必须小心不要耗尽这个内存,否则服务器将无法授予任何锁。这实际上限定了服务器可授予的咨询锁的数量,通常取决于服务器配置,上限在几万至几十万之间。
在某些使用咨询锁的方法中,特别是在涉及显式排序和LIMIT子句的查询中,必须小心控制因SQL表达式求值顺序而获取的锁。例如:
SELECT pg_advisory_lock(id) FROM foo WHERE id = 12345; -- 安全
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; -- 安全
在上述查询中,第二种形式是危险的,因为LIMIT子句的执行并非总是在锁定函数执行前得到保证。这可能会导致应用程序未预期的锁被获取,从而未能释放(直到会话结束)。从应用的角度看,这些锁将是悬空的,尽管它们在pg_locks视图中仍然是可见的。
总结
应用程序级别的数据一致性检查
数据一致性检查在应用层面的实施
通过串行化事务强制执行一致性
通过显式阻塞锁强制执行一致性
注意事项
序列化失败处理
在PostgreSQL中,采用Repeatable Read和Serializable隔离级别的事务可能因为防止序列化异常而产生错误。如前所述,使用这些隔离级别的应用程序必须准备好重试因序列化错误而失败的事务。这种错误消息文本会根据具体情形变化,但它总是会有SQLSTATE代码40001(serialization_failure)。
同样,重试死锁失败也是合理的做法。这类失败的SQLSTATE代码是40P01(deadlock_detected)。
在某些情况下,重试唯一键失败(SQLSTATE代码23505,unique_violation)和排除约束失败(SQLSTATE代码23P01,exclusion_violation)也是合适的。例如,如果应用程序在检查当前存储的键之后选择了一个主键列的新值,它可能会因为另一个应用程序实例同时选择了相同的键而遭遇唯一键失败。这实际上是一种序列化失败,但服务器无法将其识别为序列化问题,因为它不能“看到”插入值与之前的读取之间的联系。还有一些特殊情况,即使理论上服务器有足够的信息判断序列化问题是根本原因,它仍会发出唯一键或排除约束错误。虽然无条件重试序列化失败错误是推荐的做法,但重试其他错误代码时需要更加小心,因为它们可能代表持久性错误状况而非暂时性故障。
重要的是要重试整个事务,包括决定发送哪些SQL语句或使用哪些值的所有逻辑。因此,PostgreSQL不提供自动重试设施,因为它无法在保证正确性的前提下做到这一点。
事务重试并不能保证重试的事务一定能完成;可能需要多次重试。在高度竞争的情况下,事务完成可能需要多次尝试。涉及冲突的预提交事务时,可能直到预提交事务提交或回滚,才能取得进展。
注意事项
锁定和索引
PostgreSQL中不同索引类型的锁机制和性能特点总结如下:
B-树、GiST和SP-GiST索引:
Hash索引:
GIN索引:
目前,B-树索引因其高性能和丰富的功能,最适合并发应用程序中对标量数据的索引。而对于非标量数据,建议使用GiST、SP-GiST或GIN索引。B-树索引在处理并发性方面表现最优,而Hash索引和GIN索引各有其特定的应用场景和潜在的性能考量。
总结
PostgreSQL提供了强大的事务隔离和锁定机制,允许用户根据应用的具体需求调整并发控制策略。选择正确的隔离级别和锁定类型对于保证数据一致性、避免死锁以及优化性能至关重要。应用程序设计者应当理解这些概念,以便做出明智的决策,并处理可能出现的异常情况,如序列化失败。此外,合理地选择和使用索引可以显著提高并发环境下的数据访问效率。