侧边栏壁纸
博主头像
DBA的笔记本博主等级

think in coding

  • 累计撰写 20 篇文章
  • 累计创建 19 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Postgresql MVCC -1 隔离级别

张彤
2024-08-01 / 0 评论 / 0 点赞 / 34 阅读 / 37590 字

Postgresql MVCC -1 隔离级别

MVCC是什么

MVCC,英文Multi-Version Concurrency Control ,多版本并发控制。

这是目前所有主流关系型数据库都具备的并发控制机制。

本文不深入讨论二段锁机制,MVCC机制等,只阐述postgresql的MVCC实现和特性。(深入部分会单开一篇具体讨论)

  • 锁机制与MVCC机制最大的不同在于,MVCC提供了一套读取数据获得的锁与写入数据的锁不冲突的机制,读永远不会阻塞写,写也永远不会阻塞读

简而言之,PG使用多版本并发控制模型,意味着在查询数据库时,每个事务都只能看到一段时间之前的数据快照,即数据库版本。这种办法可以有效的防止同一数据行上因其他事务更新而读取到的数据不一致,从而为每个数据库会话进行事务隔离。

事务隔离级别

ANSI/ISO SQL标准通过必须在并发事务之间防止的三种现象定义了四个事务隔离级别。

为了更好理解这三类现象,我们以事务A和B以及银行存取款为场景进行举例,如下:

  • 脏读(dirty reads)

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

    事务A从账户中读取余额,初始余额为$1000。

    事务B开始并执行一个未提交的事务,将$200存入账户。

    事务A读取账户余额,这时看到的余额是$1200,因为事务A读取了事务B未提交的数据。

    如果事务B随后回滚,则事务A读取的$1200就是错误的(脏读)。

  • 不可重复读(non-repeatable reads)

    一个事务重复读取了之前已经读取过得到数据,而且发现数据已经被另外一个数据更改了。(该事务已经提交)

    事务A从账户中读取余额,初始余额为$1000。

    事务B开始并执行一个事务,将$200存入账户并提交。

    事务A再次读取账户余额,发现余额变为$1200。(此时事务A并未提交,产生了重复读)

  • 幻读(phantom read)

    一个事务重新执行一个查询,返回满足搜索条件的一组行,并发现满足条件的行集合由于另一个最近提交的事务而发生了变化。

    事务A查询账户的所有交易记录,初始记录为[交易1, 交易2]。

    事务B开始并执行一个事务,添加一个新交易记录(交易3)并提交。

    事务A再次查询账户的交易记录,发现记录变为[交易1, 交易2, 交易3]。

四类隔离级别

  • 读未提交 (Read uncommitted)

    事务可以读取其他未提交事务的更改。允许脏读、不可重复读和幻读。

    事务A读取了事务B未提交的更改。

    事务A读取数据的结果可能会受到其他事务未提交更改的影响。

  • 读已提交 (Read committed)

    事务只能读取其他已提交事务的更改.防止脏读,但允许不可重复读和幻读。

    事务A读取的数据不会受到事务B未提交更改的影响。

    事务A在多次读取同一行时可能会看到不同的结果(不可重复读)。

    事务A在两次相同查询中可能会看到不同的行集(幻读)。

  • 重复读 (Repeatable read)

    事务在读取数据时,会锁定数据,以确保在事务期间,其他事务不能修改这些数据。防止脏读和不可重复读,但允许幻读。

    事务A在读取一行数据后,即使其他事务对该行进行修改,事务A再读取时也会得到相同的结果(防止不可重复读)。

    事务A在查询一组行后,事务B可以插入或删除满足相同条件的新行,导致事务A在重新查询时看到不同的结果集(幻读)。

  • 可串行化 (Serializable)

    最高的隔离级别。事务完全隔离,仿佛每个事务是顺序执行的,而不是并发执行的。防止脏读、不可重复读和幻读。

    事务A和事务B的执行完全隔离,任何时候都不会相互影响。

    事务A的所有操作在另一个事务开始之前完成,确保结果的一致性。

四类隔离级别和对应三个行为描述如下

Isolation Level

Dirty Read

Non-Repeatable Read

Phantom Read

Read uncommitted

Possible

Possible

Possible

Read committed

Not possible

Possible

Possible

Repeatable read

Not possible

Not possible

Possible

Serializable

Not possible

Not possible

Not possible

在PostgreSQL中,您可以请求四种标准事务隔离级别中的任何一种,但在内部仅实现了三种不同的隔离级别,即 PostgreSQL 的“未提交读”模式的行为类似于“已提交读”。这是因为它是将标准隔离级别映射到 PostgreSQL 的多版本并发控制架构的唯一明智的方法。

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

序列空洞

一些PostgreSQL数据类型和函数对于事务行为有特殊规则。特别是,对序列(以及使用`serial`声明的列的计数器)所做的更改对所有其他事务立即可见,并且如果进行更改的事务中止,则不会回滚

序列空洞

一些PostgreSQL数据类型和函数对于事务行为有特殊规则。特别是,对序列(以及使用serial声明的列的计数器)所做的更改对所有其他事务立即可见,并且如果进行更改的事务中止,则不会回滚

读已提交隔离级别

在Postgres中,读已提交(Read Committed)是默认的隔离级别。当事务在此隔离级别运行时,SELECT查询只会看到在查询开始之前已提交的数据,不会看到未提交的数据或并发事务在查询执行期间提交的更改。

  • 然而,SELECT查询会看到同一事务中之前执行的更新的效果,即使这些更新尚未提交。

    即使在单个事务内,当其他事务在第一个SELECT执行期间提交更改时,两次连续的SELECT查询也可能看到不同的数据。

如果在执行UPDATE语句(或DELETE或SELECT FOR UPDATE)时查询找到的目标行已经被并发未提交的事务更新,那么尝试更新该行的第二个事务将等待其他事务提交或回滚。在回滚的情况下,等待中的事务可以继续更改该行。在提交的情况下(并且如果该行仍然存在,即未被其他事务删除),查询将重新执行,以检查新行版本是否仍然满足查询的搜索条件。如果新行版本满足查询的搜索条件,则该行将被更新(或删除或标记为更新)。请注意,更新的起点将是新行版本;此外,在更新之后,当前事务中随后的SELECT查询将能看到经过双重更新的行。因此,当前事务能够看到其他事务对该特定行的影响。

示例:事务A和事务B的交互

初始状态

假设我们有一个用户账户表 accounts,其中有一条记录:

id | balance
---|--------
1  | 1000

事务A:更新余额

  1. 事务A开始,并更新用户ID为1的账户余额,将其增加到$1200。

    BEGIN;
    UPDATE accounts SET balance = 1200 WHERE id = 1;

    此时,事务A还未提交,因此其他事务无法看到这一更改。

事务B:尝试更新余额

  1. 事务B开始,并尝试将用户ID为1的账户余额减少到$800。

    BEGIN;
    UPDATE accounts SET balance = 800 WHERE id = 1;

    然而,由于事务A尚未提交,事务B必须等待事务A提交或回滚。

事务A:提交更改

  1. 事务A提交其更改。

    COMMIT;

事务B:重新执行查询

  1. 事务A提交后,事务B重新执行更新操作。此时,事务B将看到账户余额已经被事务A更新为$1200。事务B会重新执行其查询,以确保新行版本仍然满足查询条件。

    -- 再次执行更新
    UPDATE accounts SET balance = 800 WHERE id = 1;

结果

  1. 更新完成后,事务B提交其更改。

    COMMIT;

最终状态

最后,账户余额将更新为$800,并且在这个过程中,事务B能够看到事务A对该行的更新。

id | balance
---|--------
1  | 800

可以看到当一个事务尝试更新一个被并发未提交事务修改过的行时,它必须等待第一个事务提交或回滚。等待中的事务在第一个事务提交后重新执行查询,以确保数据一致性。这样,第二个事务可以看到第一个事务对特定行的影响,并在必要时重新执行其操作。

读已提交级别提供的部分事务隔离对于许多应用来说已经足够,并且这种隔离级别速度快且使用简单。然而,对于执行复杂查询和更新的应用程序,可能需要保证比读已提交级别提供的更加严格一致的数据库视图。

可重复读

可重复读(Repeatable Read)隔离级别确保事务只能看到在其开始之前已经提交的数据,它既无法看到未提交的数据,也无法看到在其执行期间由其他并发事务所做的更改。(但是,每个查询操作都能看到其所属事务内之前执行的更新操作的效果,即使这些更新还没有被提交。)这个保证比SQL标准对这个隔离级别的要求更为严格。如上所述,这是标准所特别允许的,因为标准只规定了每个隔离级别必须提供的最小保护程度。

此级别与已提交读不同,可重复读事务中的查询看到的是事务中第一个非事务控制语句开始时的快照,而不是事务中当前语句开始时的快照。因此,单个事务中的连续SELECT命令看到相同的数据,即,它们看不到在其自己的事务开始后提交的其他事务所做的更改。

使用此级别的应用程序必须准备好因序列化失败而重试事务。

在搜索目标行方面, UPDATEDELETEMERGESELECT FOR UPDATESELECT FOR SHARE命令的行为与SELECT相同:它们只会查找截至事务开始时间提交的目标行。然而,这样的目标行在被发现时可能已经被另一个并发事务更新(或删除或锁定)。在这种情况下,可重复读事务将等待第一个更新事务提交或回滚(如果仍在进行中)。如果第一个更新器回滚,则其影响被否定,并且可重复读取事务可以继续更新最初找到的行。但是,如果第一个更新程序提交(并且实际更新或删除了该行,而不仅仅是锁定它),则可重复读取事务将回滚并显示以下消息

ERROR:  could not serialize access due to concurrent update
  • 因为可重复读事务在可重复读事务开始后无法修改或锁定其他事务更改的行。

当应用程序收到此错误消息时,它应该中止当前事务并从头开始重试整个事务。第二次,事务会将先前提交的更改视为其数据库初始视图的一部分,因此使用行的新版本作为新事务更新的起点不存在逻辑冲突。

请注意,只有更新事务可能需要重试;只读事务永远不会出现序列化冲突。

可重复读模式严格保证每个事务都能看到完全稳定的数据库视图。但是,此视图不一定始终与同一级别的并发事务的某些串行(一次一个)执行一致。例如,即使是此级别的只读事务也可能会看到更新的控制记录以显示批次已完成,但看不到逻辑上属于该批次的详细记录之一,因为它读取了该控制的早期版本记录。如果不仔细使用显式锁来阻止冲突的事务,则尝试通过在此隔离级别运行的事务来强制执行业务规则不可能正常工作。

可重复读隔离级别是使用学术数据库文献和其他一些数据库产品中称为“快照隔离”的技术来实现的。与使用降低并发性的传统锁定技术的系统相比,可以观察到行为和性能方面的差异。其他一些系统甚至可能提供可重复读取和快照隔离作为具有不同行为的不同隔离级别。直到 SQL 标准开发之后,数据库研究人员才将区分这两种技术的允许现象正式化,并且超出了本手册的范围。如需完整治疗,请参阅[berenson95]

可串行化

可串行化(Serializable)隔离级别提供了最高级别的事务隔离。此隔离级别模拟顺序事务执行,仿佛事务是一个接一个地顺序执行的,而不是并发执行的。然而,使用此隔离级别的应用程序必须准备好重试事务,因为可能会遇到序列化失败。

当事务处于序列化级别时,SELECT查询只会看到在事务开始之前已提交的数据,而不会看到未提交的数据或并发事务在事务执行期间提交的更改。(然而,SELECT查询会看到同一事务中之前执行的更新的效果,即使这些更新尚未提交。)这与读已提交(Read Committed)不同,后者在事务内的每个查询开始时都会获取一个新的快照,而序列化级别获取的是事务开始时的快照。

如果在执行UPDATE语句(或DELETE或SELECT FOR UPDATE)时查询找到的目标行已经被并发未提交的事务更新,那么尝试更新该行的第二个事务将等待其他事务提交或回滚。在回滚的情况下,等待中的事务可以继续更改该行。在并发事务提交的情况下,序列化事务将被回滚,并显示错误消息:

ERROR: Can't serialize access due to concurrent update

这是因为序列化事务不能修改在序列化事务开始之后被其他事务更改的行。当应用程序收到此错误消息时,应中止当前事务,然后从头开始重试整个事务。第二次执行时,事务将看到之前已提交的更改作为其数据库初始视图的一部分,因此在使用新版本的行作为新事务更新的起点时不会有逻辑冲突。注意,只有更新事务可能需要重试——只读事务永远不会有序列化冲突。

序列化事务级别提供了严格保证,即每个事务都可以看到数据库的完全一致视图。然而,当并发更新使得无法维持序列执行的假象时,应用程序必须准备好重试事务,重做复杂事务的成本可能很高。因此,仅当更新查询包含足够复杂的逻辑,以至于在读已提交级别可能产生错误结果时,才推荐使用此级别。

在依赖可序列化事务进行并发控制时,为了获得最佳性能,应考虑以下问题:

  1. 尽可能将事务声明为只读。

  2. 控制活动连接的数量,必要时使用连接池。这始终是一个重要的性能考虑因素,但在使用可序列化事务的繁忙系统中尤为重要。

  3. 不要在单个事务中放入比完整性所需更多的内容。

  4. 不要让连接在不必要的情况下长时间处于“事务中空闲”状态。可以使用配置参数idle_in_transaction_session_timeout来自动断开长时间停留的会话。

  5. 消除不再需要的显式锁、SELECT FOR UPDATESELECT FOR SHARE,因为可序列化事务已经自动提供了保护。

  6. 当系统由于谓词锁表内存不足而被迫将多个页面级谓词锁组合成单个关系级谓词锁时,可能会发生序列化失败率增加的情况。你可以通过增加max_pred_locks_per_transactionmax_pred_locks_per_relation和/或max_pred_locks_per_page的值来避免这种情况。

  7. 顺序扫描总是需要关系级谓词锁,这可能会导致序列化失败率增加。鼓励使用索引扫描可能有所帮助,可以通过降低random_page_cost和/或增加cpu_tuple_cost来实现。务必权衡事务回滚和重启的减少与查询执行时间总体变化的利弊。

0

评论区