从零开始学PostgreSQL (十一):并发控制

2024-09-06 19:20:36 浏览数 (1)

概述

  • 事务隔离: PostgreSQL支持多种事务隔离级别,包括Read Committed(读已提交)、Repeatable Read(可重复读)和Serializable(可串行化),用于控制事务间数据的一致性和并发行为。
  • 明确锁定:
    • 表级锁定 (Table-Level Locks)
    • 行级锁定 (Row-Level Locks)用于在不同的粒度上控制对数据的访问。
    • 页级锁定 (Page-Level Locks)在索引维护和更新期间使用。
    • 死锁 (Deadlocks)可能发生,但PostgreSQL有机制来检测并解决它们。
    • 咨询锁 (Advisory Locks)允许应用程序在进程之间协调更复杂的锁定需求。
  • 数据一致性检查:
    • 可以通过Serializable事务或明确的阻塞锁来强制执行一致性,但每种方法都有其权衡。
  • 序列化失败处理: 当在Serializable隔离级别下发生冲突时,事务可能需要回滚并重试。
  • 注意事项: 某些DDL命令(如TRUNCATE和ALTER TABLE)在MVCC环境下可能影响事务的一致性视图。
  • 锁定和索引: 不同的索引类型(如B-tree、Hash、GiST、SP-GiST和GIN)使用不同类型的锁定策略,影响并发性和性能。

事务隔离级别

读已提交隔离级别

隔离级别特性

  • 读已提交(Read Committed)**是PostgreSQL的默认事务隔离级别。
  • 在此级别下,一个查询仅能看到在查询开始前已提交的数据,不会看到未提交的变化或查询期间并发事务的更改。
  • 查询内部可以看到其所在事务中先前执行的更新效果,即使这些更新尚未提交。
  • 同一事务内的连续命令可能因其他事务的提交变化而看到不同的数据状态。

数据访问行为

  • SELECT, UPDATE, DELETE, SELECT FOR UPDATE, 和 SELECT FOR SHARE命令在搜索目标行时,只识别开始时已提交的行。
  • 更新命令可能会遇到不一致的快照,能看到它正尝试更新的行上的并发更改效果,但不会看到其他行上的并发更改效果。

特定命令行为

  • 带有ON CONFLICT DO UPDATE的INSERT命令会检查并可能更新已存在的行。
  • 带有ON CONFLICT DO NOTHING的INSERT可能因其他事务的影响而不插入行。
  • MERGE命令允许组合INSERT, UPDATE, 和 DELETE操作,但其行为取决于目标和源数据的状态及联接条件。

复杂情况下的问题

  • 对于涉及复杂搜索条件的命令,读已提交模式可能不合适,因为可能产生不一致的数据视图。
  • 例如,当一个命令的操作目标同时被其他命令添加和移除时,可能会导致意料之外的结果。

可重复读隔离级别

主要特点

  • 可重复读保证事务中所有查询看到的数据与事务开始时的数据一致,即事务内的查询结果不会因外部事务的提交而改变。
  • 此隔离级别下的事务仅能看到在事务开始前已提交的数据,不会看到任何未提交的数据或在事务执行期间由其他事务提交的更改。
  • 应用程序需准备处理序列化失败,当事务试图修改已被其他事务更改的行时,将导致事务回滚,并提示序列化访问冲突。

行为差异

  • 与读已提交隔离级别不同,可重复读事务中的查询基于事务开始时的快照,而不是每个查询开始时的快照。
  • UPDATE, DELETE, MERGE, SELECT FOR UPDATE, 和 SELECT FOR SHARE命令在搜索目标行时,只识别事务开始时已提交的行,但会等待其他事务完成以处理行的更新状态。

事务重试

  • 当遇到序列化异常错误时,应用程序应中止当前事务并重试,以便在新的事务视图中包含已提交的更改,避免逻辑冲突。
  • 只读事务不会受到序列化冲突的影响,不需要重试。

技术实现

  • 可重复读隔离级别通过快照隔离技术实现,提供一个稳定且一致的数据库视图,但这个视图可能不完全反映按序列执行的事务视图。
  • 快照隔离与传统锁定技术在行为和性能上存在差异,后者通过锁定减少并发性,而快照隔离允许更高并发度但可能需要事务重试。

历史背景

  • 在PostgreSQL 9.1之前的版本中,串行化隔离级别的行为与现在的可重复读隔离级别相同,为了保留这种行为,现在推荐使用可重复读隔离级别。

串行化隔离级别

严格事务隔离

  • 串行化隔离级别确保事务的执行效果如同它们是按照某种顺序串行执行的,即使实际上它们是并发执行的。
  • 它提供了最严格的事务隔离,能防止所有类型的并发事务异常,除了序列化异常。

事务重试需求

  • 使用串行化隔离级别的应用程序必须准备好处理序列化失败的情况,这意味着可能需要重试事务。
  • 事务重试是由于事务之间存在潜在的读写依赖,这些依赖在串行化执行中是不允许的。

数据读取的有效性

  • 任何从永久表中读取的数据,在事务成功提交前都不应被视为有效,即使是只读事务也不例外。
  • 延后只读事务在读取数据前会确保快照的正确性,读取的数据立即有效。

性能与开发优势

  • 串行化事务简化了并发控制的开发,确保单个事务在任何并发环境下都能正确执行,无需了解其他事务的细节。
  • 应用程序需要通用的序列化失败处理机制,因为预测哪些事务会导致序列化异常是困难的。

性能优化建议

  • 减少事务的规模和复杂性,避免不必要的数据访问。
  • 控制数据库连接数量,合理使用连接池。
  • 避免长时间的“事务中闲置”状态,适时断开空闲连接。
  • 最小化显式锁的使用,利用串行化事务自身的保护机制。

特殊情况处理

  • 在串行化事务中,即使预先检查了唯一性约束,仍有可能发生冲突,尤其是在并发插入相同数据时。
  • 为避免这类问题,所有可能引发冲突的事务在执行前应再次确认数据状态。

配置调优

  • 调整配置参数,如max_pred_locks_per_transaction,以避免因谓词锁内存不足导致的序列化失败。
  • 优化查询计划,减少顺序扫描,以降低序列化失败的概率。

技术实现

  • 串行化隔离级别基于串行化快照隔离技术,该技术在快照隔离基础上增加了对序列化异常的检测,与使用传统锁定机制的系统在行为和性能上有所不同。

显式锁定

表级锁

以下列表展示了PostgreSQL中可用的锁模式及其自动使用的上下文。您也可以通过LOCK命令显式获取这些锁。请记住,所有这些锁模式都是表级锁,即使名称中包含“行”这个词,这也是一种历史遗留。在某种程度上,锁模式的名称反映了它们的典型用途——但语义都是相同的。不同锁模式之间的唯一真正区别在于它们与其他锁模式冲突的方式(见表13.2)。两个事务不能在同一表上同时持有冲突的锁模式。(然而,事务永远不会与自身冲突。例如,事务可以先获取一种锁,然后稍后在同一表上获取另一种锁。)非冲突的锁模式可以被多个事务同时持有。特别是需要注意的是有些锁模式是自冲突的(例如,一种锁模式一次只能被一个事务持有),而有些则不是自冲突的(例如,一种锁模式可以被多个事务同时持有)。

表级锁模式

ACCESS SHARE (AccessShareLock)

  • 冲突模式:仅与ACCESS EXCLUSIVE冲突。
  • 用途:SELECT命令获取这种锁模式。通常,任何只读取表而不修改它的查询将获取此锁模式。

ROW SHARE (RowShareLock)

  • 冲突模式:与EXCLUSIVE和ACCESS EXCLUSIVE冲突。
  • 用途:SELECT命令结合FOR UPDATE、FOR NO KEY UPDATE、FOR SHARE或FOR KEY SHARE选项时获取这种锁模式。

ROW EXCLUSIVE (RowExclusiveLock)

  • 冲突模式:与SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE冲突。
  • 用途:UPDATE、DELETE、INSERT和MERGE命令获取这种锁模式。

SHARE UPDATE EXCLUSIVE (ShareUpdateExclusiveLock)

  • 冲突模式:与SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE冲突。
  • 用途:被VACUUM(不含FULL)、ANALYZE、CREATE INDEX CONCURRENTLY、CREATE STATISTICS、COMMENT ON和REINDEX CONCURRENTLY以及某些ALTER INDEX和ALTER TABLE变体获取。

SHARE (ShareLock)

  • 冲突模式:与ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE冲突。
  • 用途:被CREATE INDEX CONCURRENTLY获取。

SHARE ROW EXCLUSIVE (ShareRowExclusiveLock)

  • 冲突模式:与ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE冲突。
  • 用途:被CREATE TRIGGER和某些形式的ALTER TABLE获取。

EXCLUSIVE (ExclusiveLock)

  • 冲突模式:与ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE冲突。
  • 用途:被REFRESH MATERIALIZED VIEW CONCURRENTLY获取。

ACCESS EXCLUSIVE (AccessExclusiveLock)

  • 冲突模式:与所有其他锁模式冲突。
  • 用途:被DROP TABLE、TRUNCATE、REINDEX、CLUSTER、VACUUM FULL、REFRESH MATERIALIZED VIEW(不含CONCURRENTLY)和ALTER INDEX、ALTER TABLE以及LOCK TABLE命令获取。许多形式的ANALYZE也获取此级别的锁。这也是未显式指定模式的语句的默认锁模式。

提示

  • 阻止SELECT FOR UPDATE/SHARE:只有ACCESS EXCLUSIVE锁会阻止SELECT FOR UPDATE/SHARE语句。

锁的生命周期

  • 一旦获取,锁通常会持续到事务结束。但如果在建立保存点后获取锁,则如果回滚到该保存点,则立即释放锁。这是与保存点原则一致的,即回滚到保存点取消了保存点之后的所有效果。对于在PL/pgSQL异常块中获取的锁,从异常块中逃逸也会释放这些锁

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能够自动检测到死锁情况,并通过终止其中一个涉及的事务来解决死锁,允许其他事务完成。(具体哪个事务被终止难以预测,不应依赖于此。)

值得注意的是,死锁也可能由于行级锁而发生(因此,即使没有使用显式锁,死锁也可能发生)。考虑两个并发事务修改同一张表的情况。第一个事务执行:

代码语言:javascript复制
UPDATE accounts SET balance = balance   100.00 WHERE acctnum = 11111;

这将获取指定账户编号行的行级锁。然后,第二个事务执行:

代码语言:javascript复制
UPDATE accounts SET balance = balance   100.00 WHERE acctnum = 22222;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 11111;

第一条语句成功获取了指定行的行级锁,因此成功更新了该行。但是,第二条语句发现它试图更新的行已经被锁定,所以它等待获取锁的事务完成。此时,事务二正在等待事务一完成才能继续执行。接着,事务一执行:

代码语言:javascript复制
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 22222;

事务一尝试获取指定行的行级锁,但是它无法做到:事务二已经持有了这样的锁。因此,它等待事务二完成。这样,事务一被事务二阻塞,而事务二被事务一阻塞:形成了死锁条件。PostgreSQL会检测这种情况并终止其中一个事务。

防止死锁的最佳策略

通常,避免死锁的最好防御措施是确保所有使用数据库的应用程序以一致的顺序获取多个对象上的锁。在上面的例子中,如果两个事务都按照相同的顺序更新行,就不会发生死锁。还应确保事务中对对象首次获取的锁是最严格的模式,该事务对该对象将需要的。如果预先验证这一点不可行,那么可以实时处理因死锁而终止的事务,通过重新执行这些事务。

只要没有检测到死锁情况,寻求表级或行级锁的事务将无限期地等待冲突的锁被释放。这意味着应用程序长时间保持事务开放(例如,在等待用户输入时)是一个糟糕的想法,因为它可能导致其他事务的长时间等待。

总结

  • 页级锁用于控制共享缓冲池中表页的访问,但在应用层面通常不需要关注。
  • 死锁发生在两个或多个事务相互等待对方释放锁的情况,PostgreSQL能够自动检测并终止其中一个事务来解决。
  • 防止死锁的关键在于确保锁的获取顺序一致,并且获取最严格的锁模式。
  • 应用程序应避免长时间保持事务开放,以免造成其他事务的阻塞。

咨询锁(Advisory Locks)

PostgreSQL提供了创建具有应用程序定义意义的锁的手段,这些被称为咨询锁。之所以称为咨询锁,是因为系统本身并不强制其使用——应用层需要负责正确地使用它们。咨询锁对于那些不适合多版本并发控制(MVCC)模型的锁定策略特别有用。例如,咨询锁常用于模仿传统“平面文件”数据管理系统中的悲观锁定策略。尽管也可以通过存储在表中的标志实现类似目的,但咨询锁更快,避免了表膨胀问题,并且服务器会在会话结束时自动清理这些锁,无需应用层干预。

在PostgreSQL中,有两种方式可以获取咨询锁:会话级和事务级。一旦在会话级获取了咨询锁,除非明确释放或会话结束,否则锁将一直保持。与标准锁请求不同,会话级的咨询锁请求不受事务语义的影响:在后续回滚的事务中获取的锁仍将在回滚后保持,同样,解锁操作即便在调用事务失败后也是有效的。拥有锁的进程可以多次获取同一锁;每次成功的锁请求都必须有对应的解锁请求,锁才会真正释放。另一方面,事务级的锁请求行为更像常规的锁请求:它们在事务结束时自动释放,没有显式的解锁操作。这种行为对于短期使用咨询锁的情况往往比会话级的行为更为方便。相同咨询锁标识的会话级和事务级锁请求将以预期的方式相互阻塞。如果一个会话已经持有了给定的咨询锁,其额外的请求总是会成功,即使其他会话正在等待该锁;这一规则不论现有锁持有和新请求是在会话级还是事务级都适用。

与PostgreSQL中的所有锁一样,任何会话当前持有的所有咨询锁的完整列表可以在系统视图pg_locks中找到。

咨询锁和常规锁都存储在一个由配置变量max_locks_per_transaction和max_connections定义大小的共享内存池中。必须小心不要耗尽这个内存,否则服务器将无法授予任何锁。这实际上限定了服务器可授予的咨询锁的数量,通常取决于服务器配置,上限在几万至几十万之间。

在某些使用咨询锁的方法中,特别是在涉及显式排序和LIMIT子句的查询中,必须小心控制因SQL表达式求值顺序而获取的锁。例如:

代码语言:javascript复制
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视图中仍然是可见的。

总结

  • 咨询锁为应用程序提供了一种自定义锁定机制,适合于复杂或特殊的锁定需求。
  • 这些锁可以以会话级或事务级的方式获取,会话级锁在会话结束或明确释放前一直持有,而事务级锁则在事务结束时自动释放。
  • 使用咨询锁时,必须注意不要耗尽共享内存池,否则服务器将无法分配新的锁。
  • 在涉及LIMIT和显式排序的查询中使用咨询锁时,应小心控制锁的获取顺序,避免意外的锁获取和未释放的锁。

应用程序级别的数据一致性检查

数据一致性检查在应用层面的实施

  • 使用读已提交(Read Committed)事务难以强制执行关于数据完整性的业务规则,因为数据视图随每条语句的执行而变化,且单个语句可能因写入冲突而不局限于其快照。
  • 尽管可重复读(Repeatable Read)事务在整个执行过程中拥有稳定的数据视图,但使用MVCC快照进行数据一致性检查时存在读/写冲突的微妙问题,可能导致事务执行顺序的循环,影响完整性检查。

通过串行化事务强制执行一致性

  • 若所有写入和需要数据一致性视图的读取均使用串行化事务隔离级别,则无需额外努力即可确保一致性。
  • 在PostgreSQL中,为确保一致性的软件,若使用串行化事务编写,应正常工作。
  • 为减轻应用程序程序员的负担,应用程序软件应通过框架自动重试因序列化失败而回滚的事务。
  • 设置default_transaction_isolation为serializable可能是明智的,并应采取措施确保不使用其他事务隔离级别,以防止意外或绕过完整性检查。
  • 注意,串行化事务的完整性保护目前尚未扩展到热备模式或逻辑副本中,使用热备或逻辑复制的用户可能需要在主服务器上使用可重复读和显式锁定。

通过显式阻塞锁强制执行一致性

  • 当存在非串行化的写入时,要确保行的当前有效性并保护其不受并发更新的影响,必须使用SELECT FOR UPDATE、SELECT FOR SHARE或适当的LOCK TABLE语句。
  • SELECT FOR UPDATE和SELECT FOR SHARE仅针对返回的行防止并发更新,而LOCK TABLE则锁定整个表。
  • 在PostgreSQL中,要确保并发事务不会更新或删除选定的行,必须实际更新该行,即使不需要更改任何值。SELECT FOR UPDATE暂时阻止其他事务获取相同的锁或执行可能影响锁定行的UPDATE或DELETE,但一旦持有此锁的事务提交或回滚,除非在持有锁时对行进行了实际的UPDATE,否则被阻止的事务将继续执行冲突操作。
  • 在非串行化MVCC下进行全局有效性检查需要额外的考虑。例如,在银行应用程序中,可能希望检查一个表中的所有贷方总额等于另一表中的借方总额,当两个表都在积极更新时,简单比较两个连续命令的结果在读已提交模式下不可靠。在单个可重复读事务中进行两次求和只能准确反映在可重复读事务开始前已提交的事务的效果,但到结果交付时,答案的关联性可能值得怀疑。如果可重复读事务本身在尝试进行一致性检查前应用了一些更改,检查的有用性更加值得商榷,因为它包含了部分而非全部的事务开始后的更改。在这种情况下,细心的人可能希望锁定所有用于检查的表,以获得当前现实的无可争议的画面。SHARE模式(或更高)的锁保证锁定表中没有未提交的更改,除了当前事务的更改。

注意事项

  • 如果依赖显式锁定来防止并发更改,应使用读已提交模式,或在可重复读模式下小心地在执行查询前获取锁。可重复读事务获取的锁保证没有其他修改表的事务仍在运行,但如果事务看到的快照早于获取锁的时间点,它可能早于某些现已提交的表更改。可重复读事务的快照实际上在其第一条查询或数据修改命令(SELECT、INSERT、UPDATE、DELETE、MERGE)开始时冻结,因此可以在快照冻结前显式获取锁。

序列化失败处理

在PostgreSQL中,采用Repeatable Read和Serializable隔离级别的事务可能因为防止序列化异常而产生错误。如前所述,使用这些隔离级别的应用程序必须准备好重试因序列化错误而失败的事务。这种错误消息文本会根据具体情形变化,但它总是会有SQLSTATE代码40001(serialization_failure)。

同样,重试死锁失败也是合理的做法。这类失败的SQLSTATE代码是40P01(deadlock_detected)。

在某些情况下,重试唯一键失败(SQLSTATE代码23505,unique_violation)和排除约束失败(SQLSTATE代码23P01,exclusion_violation)也是合适的。例如,如果应用程序在检查当前存储的键之后选择了一个主键列的新值,它可能会因为另一个应用程序实例同时选择了相同的键而遭遇唯一键失败。这实际上是一种序列化失败,但服务器无法将其识别为序列化问题,因为它不能“看到”插入值与之前的读取之间的联系。还有一些特殊情况,即使理论上服务器有足够的信息判断序列化问题是根本原因,它仍会发出唯一键或排除约束错误。虽然无条件重试序列化失败错误是推荐的做法,但重试其他错误代码时需要更加小心,因为它们可能代表持久性错误状况而非暂时性故障。

重要的是要重试整个事务,包括决定发送哪些SQL语句或使用哪些值的所有逻辑。因此,PostgreSQL不提供自动重试设施,因为它无法在保证正确性的前提下做到这一点。

事务重试并不能保证重试的事务一定能完成;可能需要多次重试。在高度竞争的情况下,事务完成可能需要多次尝试。涉及冲突的预提交事务时,可能直到预提交事务提交或回滚,才能取得进展。

注意事项

  • MVCC与DDL命令: 在PostgreSQL中,TRUNCATE和重写形式的ALTER TABLE命令在提交后,可能会让使用旧快照的并发事务看到目标表为空,但仅限于那些在DDL操作开始前没有访问过该表的事务。若事务之前访问过表,则会阻止DDL命令直至事务完成,防止数据不一致性。
  • Serializable隔离级别与热备: 热备模式下,Serializable隔离级别未被支持;最严格的隔离级别为Repeatable Read。这意味着在备机上运行的Repeatable Read事务可能遇到与主节点上事务序列化执行不一致的临时状态。
  • 系统目录访问与隔离级别: 新建的数据库对象如表,对运行在Repeatable Read或Serializable隔离级别的事务可见,但这些对象中的行内容对这些事务是不可见的。直接查询系统目录的事务不会看到与新建对象相关的行,即便处于较高的隔离级别中。这表明系统目录的访问不遵循当前事务的隔离级别。

锁定和索引

PostgreSQL中不同索引类型的锁机制和性能特点总结如下:

B-树、GiST和SP-GiST索引:

  • 使用短期的页级共享或独占锁来支持读/写操作。
  • 锁在每次索引行检索或插入后立即释放。
  • 这些索引类型提供高并发且避免死锁的情况。

Hash索引:

  • 使用哈希桶级的共享或独占锁来支持读/写操作。
  • 锁在处理完整个哈希桶后释放。
  • 虽然桶级锁提供较好的并发性,但锁的持续时间长于单一索引操作,这可能引发死锁。

GIN索引:

  • 使用短期的页级共享或独占锁来支持读/写操作。
  • 插入一个GIN索引值通常会在每一行产生多个索引键插入,这意味着对单个值的插入可能涉及大量工作。

目前,B-树索引因其高性能和丰富的功能,最适合并发应用程序中对标量数据的索引。而对于非标量数据,建议使用GiST、SP-GiST或GIN索引。B-树索引在处理并发性方面表现最优,而Hash索引和GIN索引各有其特定的应用场景和潜在的性能考量。

总结

PostgreSQL提供了强大的事务隔离和锁定机制,允许用户根据应用的具体需求调整并发控制策略。选择正确的隔离级别和锁定类型对于保证数据一致性、避免死锁以及优化性能至关重要。应用程序设计者应当理解这些概念,以便做出明智的决策,并处理可能出现的异常情况,如序列化失败。此外,合理地选择和使用索引可以显著提高并发环境下的数据访问效率。

0 人点赞