PostgreSQL-1中的MVCC。 隔离

The material will be based on training courses (in Russian) on administration that Pavel pluzanov and I are creating. Not everyone likes to watch video (I definitely do not), and reading slides, even with comments, is no good at all.

该材料将基于Pavel pluzanov和我正在创建的有关管理的培训课程 (俄语)。 并非每个人都喜欢看视频(我绝对不喜欢),并且即使有评论也无法阅读幻灯片。

2-Day Introduction to PostgreSQL 11. PostgreSQL 11 2天简介 。

Of course, the articles will not be exactly the same as the content of the courses. I will talk only about how everything is organized, omitting the administration itself, but I will try to do it in more detail and more thoroughly. And I believe that the knowledge like this is as useful to an application developer as it is to an administrator.

当然,文章与课程内容将不会完全相同。 我将只谈论一切的组织方式,省略了政府本身,但是我将尝试更详细,更彻底地做到这一点。 而且我相信这样的知识对应用程序开发人员和管理员一样有用。

I will target those who already have some experience in using PostgreSQL and at least in general understand what is what. The text will be too difficult for beginners. For example, I will not say a word about how to install PostgreSQL and run psql.

我将针对那些已经拥有使用PostgreSQL经验并且至少总体上了解什么的人。 对于初学者来说,文字太难了。 例如,我不会说任何有关如何安装PostgreSQL和运行psql的信息。

The stuff in question does not vary much from version to version, but I will use the current, 11th vanilla PostgreSQL.

所涉及的内容因版本而异,但是我将使用当前的第11原始PostgreSQL。

The first series deals with issues related to isolation and multiversion concurrency, and the plan of the series is as follows:

第一个系列处理与隔离和多版本并发相关的问题,该系列的计划如下:

  1. Isolation as understood by the standard and PostgreSQL (this article).

    标准和PostgreSQL理解的隔离(本文)。
  2. Forks, files, pages — what is happening at the physical level.

    叉子,文件,页面 -在物理级别发生的事情。

  3. Row versions, virtual transactions and subtransactions.

    行版本 ,虚拟事务和子事务。

  4. Data snapshots and the visibility of row versions; the event horizon.

    数据快照和行版本的可见性; 事件范围。

  5. In-page vacuum and HOT updates.

    页内真空和HOT更新 。

  6. Normal vacuum.

    正常真空 。

  7. Autovacuum.

    自动抽真空 。

  8. Transaction id wraparound and freezing.

    交易ID环绕和冻结 。

Off we go!

出发吧!

什么是隔离,为何如此重要? (What is isolation and why is it important?)

Probably, everyone is at least aware of the existence of transactions, has come across the abbreviation ACID, and has heard about isolation levels. But we still happen to face the opinion that this pertains to theory, which is not necessary in practice. Therefore, I will spend some time trying to explain why this is really important.

可能每个人至少都知道事务的存在,遇到过缩写ACID,并且听说过隔离级别。 但是我们仍然碰巧认为这与理论有关,这在实践中是不必要的。 因此,我将花一些时间来解释为什么这确实很重要。

You are unlikely to be happy if an application gets incorrect data from the database or if the application writes incorrect data to the database.

如果应用程序从数据库中获取了不正确的数据,或者应用程序将错误的数据写入了数据库,则您不太可能感到高兴。

But what is “correct” data? It is known that integrity constraints, such as NOT NULL or UNIQUE, can be created at the database level. If the data always meet integrity constraints (and this is so since the DBMS guarantees it), then they are integral.

但是什么是“正确”数据? 众所周知,可以在数据库级别创建完整性约束 ,例如NOT NULL或UNIQUE。 如果数据始终满足完整性约束(之所以如此,因为DBMS保证了),则它们是不可或缺的。

Are correct and integral the same things? Not exactly. Not all constraints can be specified at the database level. Some of the constraints are too complicated, for example, that cover several tables at once. And even if a constraint in general could have been defined in the database, but for some reason it was not, it does not mean that the constraint can be violated.

正确不可分割的同一件事吗? 不完全是。 并非所有约束都可以在数据库级别上指定。 有些约束过于复杂,例如,一次覆盖多个表。 即使通常可以在数据库中定义约束,但由于某种原因,它也没有定义,但这并不意味着可以违反该约束。

So, correctness is stronger than integrity, but we do not know exactly what this means. We have nothing but admit that the “gold standard” of correctness is an application that, as we would like to believe, is written correctly and never runs wrong. In any case, if an application does not violate the integrity, but violates the correctness, the DBMS will not know about it and will not catch the application “red-handed”.

因此, 正确性要比完整性强,但是我们并不确切知道这意味着什么。 我们只不过承认正确性的“黄金标准”是一种应用程序,正如我们希望的那样,它是正确编写的,绝不会出错。 在任何情况下,如果应用程序没有违反完整性,而是违反了正确性,则DBMS不会知道它,也不会“红手”捕获该应用程序。

Further we will use the term consistency to refer to correctness.

此外,我们将使用术语一致性来指代正确性。

Let us, however, assume that an application executes only correct sequences of operators. What is the role of DBMS if the application is correct as it is?

但是,让我们假设应用程序仅执行正确的运算符序列。 如果应用程序正确无误,DBMS的作用是什么?

First, it turns out that a correct sequence of operators can temporarily break data consistency, and, oddly enough, this is normal. A hackneyed but clear example is a transfer of funds from one account to another. The consistency rule may sound like this: a transfer never changes the total amount of money on the accounts (this rule is quite difficult to specify in SQL as an integrity constraint, so it exists at the application level and is invisible to the DBMS). A transfer consists of two operations: the first reduces the funds on one account, and the second one — increases them on the other. The first operation breaks data consistency, while the second one restores it.

首先,事实证明,正确的运算符序列可以暂时破坏数据一致性,这很正常,这很奇怪。 一个朴实却清晰的例子是将资金从一个帐户转移到另一个帐户。 一致性规则听起来像这样: 转账永远不会改变帐户上的总金额 (此规则在SQL中很难指定为完整性约束,因此它存在于应用程序级别,并且对于DBMS是不可见的)。 转帐包括两个操作:第一个操作减少一个帐户上的资金,第二个操作-增加另一个帐户上的资金。 第一个操作破坏数据一致性,而第二个操作恢复数据一致性。

What if the first operation is performed and the second is not? In fact, without much ado: during the second operation there may occur an electricity failure, a server crash, division by zero — whatever. It is clear that the consistency will be broken, and this cannot be permitted. In general, it is possible to resolve such issues at the application level, but at the cost of tremendous efforts; however, fortunately, it is not necessary: this is done by the DBMS. But to do this, the DBMS must know that the two operations are an indivisible whole. That is, a transaction.

如果执行第一个操作而第二个不执行该怎么办? 实际上,事不宜迟:在第二次操作过程中,可能会发生电力故障,服务器崩溃,被零除的情况-无论如何。 很明显,一致性将被破坏,并且这是不允许的。 通常,可以在应用程序级别解决此类问题,但需要付出大量努力; 但是,幸运的是,这不是必需的:这是由DBMS完成的。 但是,为此,DBMS必须知道这两个操作是不可分割的整体。 也就是说, 交易

It turns out interesting: as the DBMS knows that operations make up a transaction, it helps maintain consistency by ensuring that the transactions are atomic, and it does this without knowing anything about specific consistency rules.

事实证明,这很有趣:由于DBMS知道操作构成一个事务,因此它通过确保事务是原子性的来帮助保持一致性,而这样做却不了解特定的一致性规则。

But there is a second, more subtle point. As soon as several simultaneous transactions appear in the system, which are absolutely correct separately, they may fail to work correctly together. This is because the order of operations is mixed up: you cannot assume that all the operations of one transaction are performed first, and then all the operations of the other one.

但是还有第二点,更微妙的一点。 一旦几个同时发生的事务(分别绝对正确)出现在系统中,它们可能无法一起正常工作。 这是因为操作顺序混合了:您不能假定一个事务的所有操作都先执行,然后又执行另一个事务的所有操作。

A note about simultaneity. Indeed, transactions can run simultaneously on a system with a multi-core processor, disk array, etc. But the same reasoning holds for a server that executes commands sequentially, in a time-sharing mode: during certain clock cycles one transaction is executed, and during next certain cycles the other one is. Sometimes the term concurrent execution is used for a generalization.

关于同时性的说明。 确实,事务可以在具有多核处理器,磁盘阵列等的系统上同时运行。但是,在分时共享模式下,按顺序执行命令的服务器也可以使用相同的推理:在某些时钟周期内执行一个事务,在接下来的某些周期中,另一个周期是。 有时,术语“ 并发执行”用于概括。

Situations when correct transactions work together incorrectly are called anomalies of concurrent execution.

正确的事务无法正常工作的情况称为并发执行异常

For a simple example: if an application wants to get correct data from the database, it must not, at least, see changes of other uncommitted transactions. Otherwise, you can not only get inconsistent data, but also see something that has never been in the database (if the transaction is canceled). This anomaly is called a dirty read.

举一个简单的例子:如果一个应用程序想要从数据库中获取正确的数据,它至少不能看到其他未提交事务的变化。 否则,您不仅可以获取不一致的数据,还可以查看数据库中从未存在过的内容(如果取消了事务)。 这种异常称为脏读

There are other, more complex, anomalies, which we will deal with a bit later.

还有其他更复杂的异常,我们将在稍后处理。

It is certainly impossible to avoid concurrent execution: otherwise, what kind of performance can we talk of? But you cannot either work with incorrect data.

当然,避免并发执行是不可能的:否则,我们可以谈谈什么样的性能? 但是您不能使用不正确的数据。

And again the DBMS comes to the rescue. You can make transactions executed as if sequentially, as if one after another. In other words — isolated from one another. In reality, the DBMS can perform operations mixed up, but ensure that the result of a concurrent execution will be the same as the result of some of the possible sequential executions. And this eliminates any possible anomalies.

DBMS再次解救。 您可以使事务顺序执行一样好像一个接一个地执行。 换句话说-彼此隔离 。 实际上,DBMS可以混合执行各种操作,但是要确保并发执行的结果与某些可能的顺序执行的结果相同。 这样可以消除任何可能的异常情况。

So we arrived at the definition:

因此,我们得出了以下定义:

This definition unites the first three letters of the acronym ACID. They are so closely related with one another that it makes no sense to consider one without the others. In fact, it is also difficult to detach the letter D (durability). Indeed, when a system crashes, it still has changes of uncommitted transactions, with which you need to do something to restore data consistency.

此定义将首字母缩写ACID的前三个字母组合在一起。 它们之间的联系如此紧密,以至于没有一个人就没有一个考虑是没有意义的。 实际上,也难以分离字母D(耐久性)。 确实,当系统崩溃时,它仍具有未提交事务的更改,您需要使用这些更改来恢复数据一致性。

Everything would have been fine, but the implementation of complete isolation is a technically difficult task entailing a reduction in system throughput. Therefore, in practice very often (not always, but almost always) the weakened isolation is used, which prevents some, but not all anomalies. This means that a part of the work to ensure data correctness falls on the application. For this very reason it is very important to understand which level of isolation is used in the system, what guarantees it gives and what it does not, and how to write correct code under such conditions.

一切都会好起来的,但是实现完全隔离是一项技术难题,需要降低系统吞吐量。 因此,实际上,经常(不是总是,但几乎总是)使用弱隔离,这可以防止某些但不是全部异常。 这意味着确保数据正确性的一部分工作落在应用程序上。 因此,了解系统中使用的隔离级别,提供哪些保证,不提供什么以及在这种情况下如何编写正确的代码非常重要。

SQL标准中的隔离级别和异常 (Isolation levels and anomalies in SQL standard)

The SQL standard has long described four levels of isolation. These levels are defined by listing anomalies that are allowed or not allowed when transactions are executed simultaneously at this level. Therefore, to talk about these levels, it is necessary to get to know the anomalies.

SQL标准很早就描述了四个隔离级别。 通过列出在此级别同时执行事务时允许或不允许的异常来定义这些级别。 因此,要谈论这些级别,有必要了解异常。

I emphasize that in this part we are talking about the standard, that is, about a theory, on which practice is significantly based, but from which at the same time it considerably diverges. Therefore, all the examples here are speculative. They will use the same operations on customer accounts: this is quite demonstrative, although, admittedly, has nothing to do with how bank operations are organized in reality.

我强调,在这一部分中,我们谈论的是标准,即关于一种理论的理论,实践是该实践的显着基础,但与此同时,实践却大相径庭。 因此,这里的所有示例都是推测性的。 他们将对客户帐户使用相同的操作:虽然没有关系,但与真实的银行操作组织方式无关,这是非常有示范性的。

更新失败 (Lost update)

Let's start with lost update. This anomaly occurs when two transactions read the same row of the table, then one transaction updates that row, and then the second transaction also updates the same row without taking into account the changes made by the first transaction.

让我们从丢失更新开始。 当两个事务读取表的同一行,然后一个事务更新该行,然后第二个事务也更新同一行而不考虑第一个事务所做的更改时,就会发生此异常。

For example, two transactions are going to increase the amount on the same account by ₽100 (₽ is the currency sign for Russian rouble). The first transaction reads the current value (₽1000) and then the second transaction reads the same value. The first transaction increases the amount (this gives ₽1100) and writes this value. The second transaction acts the same way: it gets the same ₽1100 and writes this value. As a result, the customer lost ₽100.

例如,两次交易将使同一帐户上的金额增加100英镑(₽是俄罗斯卢布的货币符号)。 第一个事务读取当前值(₽1000),然后第二个事务读取相同的值。 第一笔交易增加金额(amount1100)并写入该值。 第二笔交易的行为方式相同:它获得₽1100并写入此值。 结果,客户损失了100英镑。

The standard does not allow lost updates at any isolation level.

该标准不允许在任何隔离级别丢失更新。

脏读和未提交读 (Dirty read and Read Uncommitted)

A dirty read is what we have already got acquainted with. This anomaly occurs when a transaction reads changes that have not been committed yet by another transaction.

脏读是我们已经熟悉的内容。 当一个事务读取另一个事务尚未提交的更改时,就会发生此异常。

For example, the first transaction transfers all the money from the customer's account to another account, but does not commit the change. Another transaction reads the account balance, to get ₽0, and refuses to withdraw cash to the customer, although the first transaction aborts and reverts its changes, so the value of 0 has never existed in the database.

例如,第一笔交易将所有资金从客户的帐户转移到另一个帐户,但不提交更改。 另一笔交易读取了帐户余额,得到₽0,并拒绝向客户提取现金,尽管第一笔交易中止并还原了其更改,因此数据库中从来没有0的值。

The standard allows dirty reads at the Read Uncommitted level.

该标准允许在“读取未提交”级别进行脏读取。

不可重复读取和已提交读取 (Non-repeatable read and Read Committed)

A non-repeatable read anomaly occurs when a transaction reads the same row twice, and in between the reads, the second transaction modifies (or deletes) that row and commits the changes. Then the first transaction will get different results.

当事务两次读取同一行,并且在两次读取之间,第二个事务修改(或删除)该行并提交更改时,将发生不可重复的读取异常。 然后,第一笔交易将获得不同的结果。

For example, let a consistency rule forbid negative amounts on customer accounts. The first transaction is going to reduce the amount on the account by ₽100. It checks the current value, gets ₽1000 and decides that the decrease is possible. At the same time the second transaction reduces the amount on the account to zero and commits the changes. If the first transaction now rechecked the amount, it would get ₽0 (but it has already decided to reduce the value, and the account “goes into the red”).

例如,让一致性规则禁止客户帐户上出现负数 。 第一笔交易将使帐户中的金额减少100英镑。 它检查当前值,得到₽1000,并确定可能减小。 同时,第二笔交易将帐户上的金额减少为零并提交更改。 如果现在第一笔交易重新核对了金额,它将得到₽0(但是它已经决定减少该金额,并且帐户“变成红色”)。

The standard allows non-repeatable reads at the Read Uncommitted and Read Committed levels. But Read Committed does not allow dirty reads.

该标准允许不可重复的读取处于“读取未提交”和“读取已提交”级别。 但是Read Committed不允许脏读。

幻像读取和可重复读取 (Phantom read and Repeatable Read)

A phantom read occurs when a transaction reads a set of rows by the same condition twice, and in between the reads, the second transaction adds rows that meet that condition (and commits the changes). Then the first transaction will get a different sets of rows.

当事务两次按相同条件读取一组行时,就会发生幻像读取 ,并且在两次读取之间,第二个事务会添加满足该条件的行(并提交更改)。 然后,第一个事务将获得不同的行集。

For example, let a consistency rule prevent a customer from having more than 3 accounts. The first transaction is going to open a new account, checks the current number of accounts (say, 2), and decides that opening is possible. At the same time, the second transaction also opens a new account for the customer and commits the changes. Now if the first transaction rechecked the number, it would get 3 (but it is already opening another account, and the customer appears to have 4 of them).

例如,让一致性规则阻止客户拥有3个以上的帐户 。 第一笔交易将开设一个新帐户,检查当前帐户数(例如2),并确定可以开设。 同时,第二笔交易还将为客户开设一个新帐户并提交更改。 现在,如果第一笔交易重新检查了该数字,它将得到3(但它已经在开设另一个帐户,并且客户似乎拥有4个)。

The standard allows phantom reads at the Read Uncommitted, Read Committed, and Repeatable Read levels. However, non-repeatable read is not allowed at the Repeatable Read level.

该标准允许幻像读取处于“未提交读取”,“已提交读取”和“可重复读取”级别。 但是,在“可重复读取”级别上不允许进行不可重复读取。

没有异常和可序列化 (The absence of anomalies and Serializable)

The standard defines one more level — Serializable — which does not allow any anomalies. And this is not the same as to forbid lost updates and dirty, non-repeatable, or phantom reads.

该标准定义了另一个级别-Serializable-不允许任何异常。 这与禁止丢失更新以及脏的,不可重复的或幻像的读取不同。

The thing is that there are much more known anomalies than listed in the standard and also an unknown number of yet unknown ones.

事实是,已知异常比标准中列出的要多得多,并且未知数量也未知。

The Serializable level must prevent absolutely all anomalies. It means that at this level, an application developer does not need to think about concurrent execution. If transactions perform a correct sequence of operators working separately, the data will be consistent also when these transactions are executed simultaneously.

可序列化级别必须绝对防止所有异常。 这意味着在此级别上,应用程序开发人员无需考虑并发执行。 如果事务执行正确的操作符序列(分别工作),则同时执行这些事务时,数据也将保持一致。

汇总表 (Summary table)

Now we can provide a well-known table. But here the last column, which is missing from the standard, is added for clarity.

现在我们可以提供一个众所周知的表。 但是为了清楚起见,此处添加了标准中缺少的最后一列。

Lost changes Dirty read Non-repeatable read Phantom read Other anomalies
Read Uncommitted Yes Yes Yes Yes
Read Committed Yes Yes Yes
Repeatable Read Yes Yes
Serializable
遗失的变更 脏读 不可重复读 幻影阅读 其他异常
读未提交
阅读已提交
可重复读
可序列化

为什么要这些异常? (Why exactly these anomalies?)

Why does the standard list only a few of the many possible anomalies, and why are they exactly these?

为什么标准仅列出许多可能的异常中的几个,为什么它们正是这些异常?

No one seems to know it for sure. But here the practice is evidently ahead of the theory, so it is possible that at that time (of the SQL:92 standard) other anomalies were not just thought of.

似乎没有人知道这一点。 但是这里的做法显然比理论要先进,因此在那时(SQL:92标准)可能还没有想到其他异常。

In addition, it was assumed that the isolation must be built on locks. The idea behind the widely used Two-Phase Locking protocol (2PL) is that during execution, a transaction locks the rows it is working with and releases the locks on completion. Considerably simplifying, the more locks a transaction acquires, the better it is isolated from other transactions. But the performance of the system also suffers more, because instead of working together, transactions begin to queue for the same rows.

另外,假定隔离必须建立在锁上。 广泛使用的两阶段锁定协议 (2PL)背后的思想是,在执行过程中,事务将锁定正在使用的行,并在完成时释放锁定。 相当简化,一个事务获取的锁越多,它与其他事务的隔离性就越好。 但是,系统的性能也会受到更大的影响,因为交易不是一起工作,而是开始排队等待相同的行。

My sense is that it's just the number of locks required, which accounts for the difference between the isolation levels of the standard.

我的感觉是,这只是所需的锁数,这说明了标准的隔离级别之间的差异。

If a transaction locks the rows to be modified from updating, but not from reading, we get the Read Uncommitted level: lost changes are not allowed, but uncommitted data can be read.

如果事务锁定了要更新的行而不是读取的行,我们将获得“读取未提交”级别:不允许丢失丢失的更改,但可以读取未提交的数据。

If a transaction locks the rows to be modified from both reading and updating, we get the Read Committed level: you cannot read uncommitted data, but you can get a different value (non-repeatable read) when you access the row again.

如果事务锁定了要修改的行,使其无法读取和更新,则将获得“读取已提交”级别:您无法读取未提交的数据,但是当您再次访问该行时,您可以获得一个不同的值(不可重复读取)。

If a transaction locks the rows both to be read and to be modified and both from reading and updating, we get the Repeatable Read level: re-reading the row will return the same value.

如果事务锁定了要读取和修改的行以及读取和更新的行,我们将获得“可重复读取”级别:重新读取该行将返回相同的值。

But there is an issue with Serializable: you cannot lock a row that does not exist yet. Therefore, a phantom read is still possible: another transaction may add (but not delete) a row that meets the conditions of a previously executed query, and that row will be included in the re-selection.

但是Serializable有一个问题:您不能锁定不存在的行。 因此,幻象读取仍然是可能的:另一个事务可以添加(但不能删除)满足先前执行的查询条件的行,并且该行将包含在重新选择中。

Therefore, to implement the Serializable level, normal locks do not suffice — you need to lock conditions (predicates) rather than rows. Therefore, such locks were called predicate. They were proposed in 1976, but their practical applicability is limited by fairly simple conditions for which it is clear how to join two different predicates. As far as I know, such locks have never been implemented in any system so far.

因此,要实现可序列化级别,普通锁不能满足要求-您需要锁定条件(谓词)而不是行。 因此,此类锁称为谓词 。 它们是在1976年提出的,但是它们的实际适用性受到相当简单的条件的限制,对于这些条件,很明显如何将两个不同的谓词结合在一起。 据我所知,到目前为止,此类锁从未在任何系统中实现。

PostgreSQL中的隔离级别 (Isolation levels in PostgreSQL)

Over time, lock-based protocols of transaction management were replaced with the Snapshot Isolation protocol (SI). Its idea is that each transaction works with a consistent snapshot of the data at a certain point in time, and only those changes get into the snapshot that were committed before it was created.

随着时间的流逝,基于锁定的事务管理协议已被快照隔离协议(SI)取代。 其想法是,每个事务在某个时间点都使用数据的一致快照,并且只有那些更改会进入创建快照之前提交的快照。

This isolation automatically prevents dirty reads. Formally, you can specify the Read Uncommitted level in PostgreSQL, but it will work exactly the same way as Read Committed. Therefore, further we will not talk about the Read Uncommitted level at all.

这种隔离会自动防止脏读。 正式地,您可以在PostgreSQL中指定Read Uncommitted级别,但是它的工作方式与Read Committed完全相同。 因此,进一步,我们将不再谈论“读取未提交”级别。

PostgreSQL implements a multiversion variant of this protocol. The idea of multiversion concurrency is that multiple versions of the same row can coexist in a DBMS. This allows you to build a snapshot of the data using existing versions and to use a minimum of locks. Actually, only subsequent changes to the same row are locked. All other operations are performed simultaneously: write transactions never lock read-only transactions, and read-only transactions never lock anything.

PostgreSQL实现了该协议的多版本变体。 多版本并发的想法是,同一行的多个版本可以在DBMS中共存。 这使您可以使用现有版本构建数据快照,并使用最少的锁。 实际上,只有随后对同一行的更改被锁定。 所有其他操作是同时执行的:写事务永远不会锁定只读事务,而只读事务永远不会锁定任何东西。

By using data snapshots, isolation in PostgreSQL is stricter than required by the standard: the Repeatable Read level does not allow not only non-repeatable reads, but also phantom reads (although it does not provide complete isolation). And this is achieved without loss of efficiency.

通过使用数据快照,PostgreSQL中的隔离比标准要求的严格:“可重复读取”级别不仅不允许不可重复的读取,还允许幻像读取(尽管它不提供完全隔离)。 并且这在不损失效率的情况下实现。

Lost changes Dirty read Non-repeatable read Phantom read Other anomalies
Read Uncommitted Yes Yes Yes
Read Committed Yes Yes Yes
Repeatable Read Yes
Serializable
遗失的变更 脏读 不可重复读 幻影阅读 其他异常
读未提交
阅读已提交
可重复读
可序列化

We will talk in the next articles of how multiversion concurrency is implemented “under the hood,” and now we will look in detail at each of the three levels with a user's eye (as you know, the most interesting is hidden behind “other anomalies”). To do this, let's create a table of accounts. Alice and Bob have ₽1000 each, but Bob has two opened accounts:

在下一篇文章中,我们将讨论如何在幕后实施多版本并发,现在,我们将以用户的眼光详细地研究这三个级别中的每个级别(如您所知,最有趣的是隐藏在“其他异常背后”)。 ”)。 为此,我们创建一个帐户表。 爱丽丝和鲍勃各有1000英镑,但鲍勃有两个已开设的帐户:

=> CREATE TABLE accounts(
  id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
  number text UNIQUE,
  client text,
  amount numeric
);
=> INSERT INTO accounts VALUES
  (1, '1001', 'alice', 1000.00),
  (2, '2001', 'bob', 100.00),
  (3, '2002', 'bob', 900.00);

阅读已提交 (Read Committed)

没有脏读 (The absence of dirty read)

It is easy to make sure that dirty data cannot be read. We start the transaction. By default it will use the Read Committed isolation level:

很容易确保不会读取脏数据。 我们开始交易。 默认情况下,它将使用Read Committed隔离级别:

=> BEGIN;
=> SHOW transaction_isolation;
transaction_isolation 
-----------------------
 read committed
(1 row)

More precisely, the default level is set by the parameter, which can be changed if necessary:

更准确地说,默认级别由参数设置,如有必要,可以更改:

=> SHOW default_transaction_isolation;
default_transaction_isolation 
-------------------------------
 read committed
(1 row)

So, in an open transaction, we withdraw funds from the account, but do not commit the changes. The transaction sees its own changes:

因此,在未结交易中,我们从帐户中提取资金,但不提交更改。 交易会看到自己的变化:

=> UPDATE accounts SET amount = amount - 200 WHERE id = 1;
=> SELECT * FROM accounts WHERE client = 'alice';
id | number | client | amount 
----+--------+--------+--------
  1 | 1001   | alice  | 800.00
(1 row)

In the second session, we will start another transaction with the same Read Committed level. To distinguish between the transactions, commands of the second transaction will be indented and marked with a bar.

在第二个会话中,我们将开始另一个具有相同“读取已提交”级别的事务。 为了区分事务,第二个事务的命令将缩进并用条形标记。

In order to repeat the above commands (which is useful), you need to open two terminals and run psql in each one. In the first terminal, you can enter the commands of one transaction, and in the second one — those of the other.

为了重复上述命令(这很有用),您需要打开两个终端并在每个终端中运行psql。 在第一个终端中,您可以输入一个事务的命令,而在第二个终端中,您可以输入另一个事务的命令。

|  => BEGIN;
|  => SELECT * FROM accounts WHERE client = 'alice';
|   id | number | client | amount 
|  ----+--------+--------+---------
|    1 | 1001   | alice  | 1000.00
| (1 row)

As expected, the other transaction does not see uncommitted changes since dirty reads are not allowed.

正如预期的那样,其他事务不会看到未提交的更改,因为不允许脏读。

不可重复读 (Non-repeatable read)

Now let the first transaction commit the changes and the second one re-execute the same query.

现在,让第一个事务提交更改,第二个事务重新执行相同的查询。

=> COMMIT;
| => SELECT * FROM accounts WHERE client = 'alice';
|   id | number | client | amount 
|  ----+--------+--------+--------
|    1 | 1001   | alice  | 800.00
| (1 row)
|  => COMMIT;

The query already gets new data — and this is the non-repeatable read anomaly, which is allowed at the Read Committed level.

该查询已获取新数据-这是不可重复的读取异常,在“读取已提交”级别允许。

Practical conclusion: in a transaction, you cannot make decisions based on data read by a previous operator because things can change between execution of the operators. Here is an example whose variations occur so often in application code that it is considered a classic antipattern: 实际结论 :在事务中,您不能基于前一个操作员读取的数据来做出决策,因为在执行这些操作员之间可能会发生变化。 这是一个示例,其变化经常在应用程序代码中发生,因此被认为是经典的反模式:
IF (SELECT amount FROM accounts WHERE id = 1) >= 1000 THEN
        UPDATE accounts SET amount = amount - 1000 WHERE id = 1;
      END IF;

During the time that passes between checking and updating, other transactions can change the state of the account any way, so such a “check” secures from nothing. It is convenient to imagine that between operators of one transaction any other operators of other transactions can “wedge,” for example, as follows:

在检查和更新之间的这段时间内,其他交易可以以任何方式更改帐户的状态,因此这种“检查”可以确保一切。 可以方便地想象一个交易的操作者之间的其他交易的其他操作者可以“楔入”,例如,如下所示:

IF (SELECT amount FROM accounts WHERE id = 1) >= 1000 THEN
       -----
      |   UPDATE accounts SET amount = amount - 200 WHERE id = 1;
      |   COMMIT;
       -----
        UPDATE accounts SET amount = amount - 1000 WHERE id = 1;
      END IF;

If everything can be spoiled by rearranging the operators, then the code is written incorrectly. And do not deceive yourself that such a coincidence will not happen — it will, for sure.

如果一切都可以通过重新布置运算符来破坏,则代码编写错误。 并且不要欺骗自己,这样的巧合不会发生-当然可以。

But how to write code correctly? The options tend to be as follows:

但是如何正确编写代码? 选项通常如下:

  • Not to write code.

    不写代码。

    This is not a joke. For example, in this case, checking easily turns into an integrity constraint:

    这不是一个玩笑。 例如,在这种情况下,检查很容易变成完整性约束:

    ALTER TABLE accounts ADD CHECK amount >= 0;

    ALTER TABLE accounts ADD CHECK amount >= 0;

    No checks are needed now: simply perform the operation and, if necessary, handle the exception that will occur if an integrity violation is attempted.

    现在无需检查:只需执行该操作,并在必要时处理尝试完整性冲突时发生的异常。

  • To use a single SQL statement.

    使用单个SQL语句。

    Consistency problems arise since in the time interval between operators another transaction can complete, which will change the visible data. And if there is one operator, then there are no time intervals.

    出现一致性问题是因为在操作员之间的时间间隔内,另一个事务可以完成,这将更改可见数据。 而且,如果只有一名操作员,则没有时间间隔。

    PostgreSQL has enough techniques to solve complex problems with one SQL statement. Let's note common table expressions (CTE), in which, among the rest, you can use INSERT/UPDATE/DELETE statements, as well as the INSERT ON CONFLICT statement, which implements the logic of “insert, but if the row already exists, update” in one statement.

    PostgreSQL有足够的技术可以用一条SQL语句解决复杂的问题。 让我们注意一下通用表表达式(CTE),在其余表中,您可以使用INSERT / UPDATE / DELETE语句,以及INSERT ON CONFLICT语句,该语句实现了“插入,但如果该行已经存在,一项声明中的“更新”。

  • Custom locks.

    自定义锁。

    The last resort is to manually set an exclusive lock on all the necessary rows (SELECT FOR UPDATE) or even on the entire table (LOCK TABLE). This always works, but nullifies the benefits of multiversion concurrency: some operations will be executed sequentially instead of concurrent execution.

    最后的方法是在所有必需的行(SELECT FOR UPDATE)上甚至在整个表(LOCK TABLE)上手动设置排他锁。 这始终有效,但是使多版本并发的好处无效:某些操作将顺序执行,而不是并发执行。

读不一致 (Inconsistent read)

Before proceeding to the next level of isolation, you have to admit that it's not all as simple as it sounds. The implementation of PostgreSQL is such that it allows for other, less known, anomalies that are not regulated by the standard.

在进入下一个隔离级别之前,您必须承认它并不像听起来那样简单。 PostgreSQL实现允许其他不受标准规范的鲜为人知的异常。

Let's assume that the first transaction started funds transfer from one Bob's account to the other:

假设第一笔交易开始将资金从一个Bob的帐户转移到另一个帐户:

=> BEGIN;
=> UPDATE accounts SET amount = amount - 100 WHERE id = 2;

At the same time, another transaction counts Bob's balance, and the calculation is performed in a loop over all Bob's accounts. In fact, the transaction starts with the first account (and, obviously, sees the previous state):

同时,另一笔交易计算了Bob的余额,并且该计算在所有Bob的帐户中循环执行。 实际上,交易是从第一个帐户开始的(显然,看到的是先前的状态):

|  => BEGIN;
|  => SELECT amount FROM accounts WHERE id = 2;
|   amount 
|  --------
|   100.00
|  (1 row)

At this point in time, the first transaction completes successfully:

此时,第一个事务成功完成:

=> UPDATE accounts SET amount = amount + 100 WHERE id = 3;
=> COMMIT;

And the other one reads the state of the second account (and already sees the new value):

另一个读取第二个帐户的状态(并且已经看到新值):

|  => SELECT amount FROM accounts WHERE id = 3;
|   amount 
|  ---------
|   1000.00
|  (1 row)
|  => COMMIT;

Therefore, the second transaction got ₽1100 in total, that is, incorrect data. And this is an inconsistent read anomaly.

因此,第二笔交易总计₽1100,即数据不正确。 这是不一致的读取异常。

How to avoid such an anomaly while staying at the Read Committed level? Of course, use one operator. For example:

在保持“读取已提交”级别时如何避免这种异常? 当然,请使用一个运算符。 例如:

SELECT sum(amount) FROM accounts WHERE client = 'bob';

Up to here I asserted that data visibility could only change between operators, but is that so obvious? And if the query takes long, can it see a part of the data in one state and a part in another one?

到现在为止,我断言数据可见性只能在操作员之间改变,但这是如此明显吗? 而且,如果查询花费的时间很长,是否可以查看处于一种状态的数据的一部分和处于另一种状态的数据的一部分?

Let's check. A convenient way to do this is to insert a forced delay into the operator by calling the pg_sleep function. Its parameter specifies the delay time in seconds.

让我们检查。 一种方便的方法是通过调用pg_sleep函数将强制延迟插入到运算符中。 其参数以秒为单位指定延迟时间。

=> SELECT amount, pg_sleep(2) FROM accounts WHERE client = 'bob';

While this operator is executed, we transfer the funds back in another transaction:

执行此运算符后,我们将在另一笔交易中将资金转回:

|  => BEGIN;
|  => UPDATE accounts SET amount = amount + 100 WHERE id = 2;
|  => UPDATE accounts SET amount = amount - 100 WHERE id = 3;
|  => COMMIT;

The result shows that the operator sees the data in the state that they had at the time when execution of the operator started. This is undoubtedly correct.

结果显示,操作员以执行操作员开始时的状态查看数据。 这无疑是正确的。

amount  | pg_sleep 
---------+----------
    0.00 | 
 1000.00 | 
(2 rows)

But it's not that simple here either. PostgreSQL allows you to define functions, and functions have the concept of a volatility category. If a VOLATILE function is called in a query and another query is executed in that function, the query inside the function will see data that are inconsistent with the data in the main query.

但这也不是那么简单。 PostgreSQL允许您定义函数,并且函数具有波动类别的概念。 如果在查询中调用了VOLATILE函数,并在该函数中执行了另一个查询,则该函数内部的查询将看到与主查询中的数据不一致的数据。

=> CREATE FUNCTION get_amount(id integer) RETURNS numeric AS $$
  SELECT amount FROM accounts a WHERE a.id = get_amount.id;
$$ VOLATILE LANGUAGE sql;
=> SELECT get_amount(id), pg_sleep(2)
FROM accounts WHERE client = 'bob';
|  => BEGIN;
|  => UPDATE accounts SET amount = amount + 100 WHERE id = 2;
|  => UPDATE accounts SET amount = amount - 100 WHERE id = 3;
|  => COMMIT;

In this case, we get incorrect data — ₽100 are lost:

在这种情况下,我们得到的数据不正确-损失了£100:

get_amount | pg_sleep 
------------+----------
     100.00 | 
     800.00 | 
(2 rows)

I emphasize that this effect is possible only at the Read Committed isolation level and only with the VOLATILE functions. The trouble is that by default, exactly this isolation level and this volatility category are used. Don't fall into thе trap!

我强调,只有在“读已提交”隔离级别和VOLATILE函数中,这种效果才可能实现。 问题在于,默认情况下,仅使用此隔离级别和此波动类别。 不要陷入陷阱!

读取不一致以换取丢失的更改 (Inconsistent read in exchange for lost changes)

We can also get an inconsistent read within a single operator during an update, although in a somewhat unexpected way.

在更新过程中,尽管以某种出乎意料的方式,我们也可能在单个运算符中读取不一致的内容。

Let's see what happens when two transactions try to modify the same row. Now Bob has ₽1000 on two accounts:

让我们看看当两个事务试图修改同一行时会发生什么。 现在鲍勃在两个帐户上有1000英镑:

=> SELECT * FROM accounts WHERE client = 'bob';
id | number | client | amount 
----+--------+--------+--------
  2 | 2001   | bob    | 200.00
  3 | 2002   | bob    | 800.00
(2 rows)

We start a transaction that reduces Bob's balance:

我们开始进行交易以减少Bob的余额:

=> BEGIN;
=> UPDATE accounts SET amount = amount - 100 WHERE id = 3;

At the same time, in another transaction interest accrues on all customer accounts with the total balance equal to or greater than ₽1000:

同时,在另一笔交易中,所有客户帐户的总余额等于或大于1000英镑,产生利息:

|  => UPDATE accounts SET amount = amount * 1.01
|  WHERE client IN (
|    SELECT client
|    FROM accounts
|    GROUP BY client
|    HAVING sum(amount) >= 1000
|  );

Execution of the UPDATE operator consists of two parts. First, actually SELECT is executed, which selects the rows to update that meet the appropriate condition. Because the change in the first transaction is not committed, the second transaction cannot see it, and the change does not affect the selection of rows for interest accrual. Well then, Bob's accounts meet the condition and once the update is executed, his balance should increase by ₽10.

UPDATE运算符的执行包括两个部分。 首先,实际上是执行SELECT,这将选择要更新的符合适当条件的行。 因为第一笔交易中的更改未提交,所以第二笔交易看不到它,因此该更改不会影响应计利息行的选择。 好了,鲍勃的帐户满足条件,一旦执行更新,他的余额应增加10英镑。

The second stage of the execution is updating the selected rows one by one. Here the second transaction is forced to “hang” because the row with id = 3 is already locked by the first transaction.

执行的第二阶段是逐一更新所选行。 在这里,第二个事务被迫“挂起”,因为id = 3的行已被第一个事务锁定。

Meanwhile, the first transaction commits the changes:

同时,第一个事务提交更改:

=> COMMIT;

What will the result be?

结果将是什么?

=> SELECT * FROM accounts WHERE client = 'bob';
id | number | client | amount 
----+--------+--------+----------
  2 | 2001   | bob    | 202.0000
  3 | 2002   | bob    | 707.0000
(2 rows)

Well, on one hand, the UPDATE command should not see the changes of the second transaction. But on the other hand, it should not lose the changes committed in the second transaction.

好吧,一方面,UPDATE命令应该看不到第二个事务的更改。 但是,另一方面,它不应丢失在第二笔交易中提交的更改。

Once the lock is released, UPDATE re-reads the row it is trying to update (but only this one). As a result, Bob accrued ₽9, based on the amount of ₽900. But if Bob had ₽900, his accounts should not have been in the selection at all.

释放锁后,UPDATE将重新读取它尝试更新的行(但仅此行)。 结果,鲍勃根据900英镑的金额累计了9英镑。 但是如果鲍勃有900英镑,那么他的帐户根本就不会出现在选择中。

So, the transaction gets incorrect data: some of the rows are visible at one point in time, and some at another one. Instead of a lost update we again get the anomaly of inconsistent read.

因此,该事务获取了不正确的数据:某些行在某个时间点可见,而另一些在另一时间点可见。 我们不再丢失丢失的更新,而是再次得到不一致的读取异常。

x := (SELECT amount FROM accounts WHERE id = 1);
      UPDATE accounts SET amount = x + 100 WHERE id = 1;
x := (SELECT amount FROM accounts WHERE id = 1);
      UPDATE accounts SET amount = x + 100 WHERE id = 1;

The database is not to blame: it gets two SQL statements and knows nothing about the fact that the value of x + 100 is somehow related to accounts amount. Avoid writing code that way.

该数据库不应该受到指责:它获得两个SQL语句,并且对x + 100的值与帐户金额有某种关系这一事实一无所知。 避免以这种方式编写代码。

可重复读 (Repeatable Read)

缺少不可重复和幻像读取 (The absence of non-repeatable and phantom reads)

The very name of the isolation level assumes that reading is repeatable. Let's check it, and at the same time make sure there are no phantom reads. To do this, in the first transaction, we revert Bob's accounts to their previous state and create a new account for Charlie:

隔离级别的确切名称假定读取是可重复的。 让我们检查一下,同时确保没有幻像读取。 为此,在第一笔交易中,我们将Bob的帐户还原为以前的状态,并为Charlie创建一个新帐户:

=> BEGIN;
=> UPDATE accounts SET amount = 200.00 WHERE id = 2;
=> UPDATE accounts SET amount = 800.00 WHERE id = 3;
=> INSERT INTO accounts VALUES
  (4, '3001', 'charlie', 100.00);
=> SELECT * FROM accounts ORDER BY id;
id | number | client | amount 
----+--------+---------+--------
  1 | 1001   | alice   | 800.00
  2 | 2001   | bob     | 200.00
  3 | 2002   | bob     | 800.00
  4 | 3001   | charlie | 100.00
(4 rows)

In the second session, we start the transaction with the Repeatable Read level by specifying it in the BEGIN command (the level of the first transaction is inessential).

在第二个会话中,我们通过在BEGIN命令中指定可重复读取级别来启动事务(第一个事务的级别是非必需的)。

|  => BEGIN ISOLATION LEVEL REPEATABLE READ;
|  => SELECT * FROM accounts ORDER BY id;
|   id | number | client | amount 
|  ----+--------+--------+----------
|    1 | 1001   | alice  |   800.00
|    2 | 2001   | bob    | 202.0000
|    3 | 2002   | bob    | 707.0000
|  (3 rows)

Now the first transaction commits the changes and the second re-executes the same query.

现在,第一个事务提交更改,第二个事务重新执行相同的查询。

=> COMMIT;
| => SELECT * FROM accounts ORDER BY id;
|   id | number | client | amount 
|  ----+--------+--------+----------
|    1 | 1001   | alice  | 800.00
|    2 | 2001   | bob    | 202.0000
|    3 | 2002   | bob    | 707.0000
|  (3 rows)
|  => COMMIT;

The second transaction still sees exactly the same data as at the beginning: no changes to existing rows or new rows are visible.

第二个事务仍然看到与开始时完全相同的数据:现有行或新行的更改均不可见。

At this level, you can avoid worrying about something that may change between two operators.

在此级别上,您可以避免担心两个操作员之间可能会发生变化的事情。

序列化错误以换取丢失的更改 (Serialization error in exchange for lost changes)

We've discussed earlier that when two transactions update the same row at the Read Committed level, an anomaly of inconsistent read may occur. This is because the waiting transaction re-reads the locked row and therefore does not see it as of the same point in time as the other rows.

前面我们已经讨论过,当两个事务在“读取已提交”级别更新同一行时,可能会发生读取不一致的异常。 这是因为等待的事务重新读取了锁定的行,因此在与其他行相同的时间点看不到它。

At the Repeatable Read level, this anomaly is not allowed, but if it does occur, nothing can be done — so the transaction terminates with a serialization error. Let's check it by repeating the same scenario with interest accrual:

在“可重复读取”级别,此异常是不允许的,但是如果发生这种异常,则无法进行任何操作-因此,事务因序列化错误而终止。 让我们通过重复产生应计利息的相同场景来检查它:

=> SELECT * FROM accounts WHERE client = 'bob';
id | number | client | amount 
----+--------+--------+--------
  2 | 2001 | bob | 200.00
  3 | 2002 | bob | 800.00
(2 rows)
=> BEGIN;
=> UPDATE accounts SET amount = amount - 100.00 WHERE id = 3;
|  => BEGIN ISOLATION LEVEL REPEATABLE READ;
|  => UPDATE accounts SET amount = amount * 1.01
|  WHERE client IN (
|    SELECT client
|    FROM accounts
|    GROUP BY client
|    HAVING sum(amount) >= 1000
|  );
=> COMMIT;
|  ERROR: could not serialize access due to concurrent update
|  => ROLLBACK;

The data remained consistent:

数据保持一致:

=> SELECT * FROM accounts WHERE client = 'bob';
id | number | client | amount 
----+--------+--------+--------
  2 | 2001   | bob    | 200.00
  3 | 2002   | bob    | 700.00
(2 rows)

The same error will occur in the case of any other competitive change of a row, even if the columns of our concern were not actually changed.

即使一行的任何其他竞争性更改没有发生,即使我们关注的列没有实际更改,也会发生相同的错误。

Practical conclusion: if your application uses the Repeatable Read isolation level for write transactions, it must be ready to repeat transactions that terminated with a serialization error. For read-only transactions, this outcome is not possible. 实际结论 :如果您的应用程序对写入事务使用“可重复读取”隔离级别,则它必须准备好重复因序列化错误而终止的事务。 对于只读事务,此结果是不可能的。

写入不一致(写入偏斜) (Inconsistent write (write skew))

So, in PostgreSQL, at the Repeatable Read isolation level, all anomalies described in the standard are prevented. But not all anomalies in general. It turns out there are exactly two anomalies that are still possible. (This is true not only for PostgreSQL, but also for other implementations of Snapshot Isolation.)

因此,在PostgreSQL中,在“可重复读取”隔离级别上,可以防止标准中描述的所有异常。 但并非所有异常都一般。 事实证明,仍然有两种异常仍然可能。 (这不仅适用于PostgreSQL,而且适用于快照隔离的其他实现。)

The first of these anomalies is an inconsistent write.

这些异常中的第一个是写入不一致

Let the following consistency rule holds: negative amounts on customer accounts are allowed if the total amount on all accounts of that customer remains non-negative.

让以下一致性规则成立: 如果该客户所有帐户上的总金额保持非负数,则允许该客户帐户上的负金额

The first transaction gets the amount on Bob's accounts: ₽900.

第一笔交易在鲍勃的帐户上获得的金额为900英镑。

=> BEGIN ISOLATION LEVEL REPEATABLE READ;
=> SELECT sum(amount) FROM accounts WHERE client = 'bob';
sum 
--------
 900.00
(1 row)

The second transaction gets the same amount.

第二笔交易获得相同的金额。

|  => BEGIN ISOLATION LEVEL REPEATABLE READ;
|  => SELECT sum(amount) FROM accounts WHERE client = 'bob';
|    sum 
|  --------
|   900.00
| (1 row)

The first transaction rightfully believes that the amount of one of the accounts can be reduced by ₽600.

第一笔交易正确地认为,其中一个帐户的金额可以减少600英镑。

=> UPDATE accounts SET amount = amount - 600.00 WHERE id = 2;

And the second transaction comes to the same conclusion. But it reduces another account:

第二笔交易得出相同的结论。 但这减少了另一个帐户:

|  => UPDATE accounts SET amount = amount - 600.00 WHERE id = 3;
|  => COMMIT;
=> COMMIT;
=> SELECT * FROM accounts WHERE client = 'bob';
id | number | client | amount 
----+--------+--------+---------
  2 | 2001   | bob    | -400.00
  3 | 2002   | bob    | 100.00
(2 rows)

We managed to make Bob's balance go into the red, although each transaction is working correctly alone.

尽管每笔交易都能正常运作,但我们设法使Bob的余额变成了红色。

只读事务异常 (Read-only transaction anomaly)

This is the second and last of the anomalies that are possible at the Repeatable Read level. To demonstrate it, you will need three transactions, two of which will change the data, and the third will only read it.

这是可重复读取级别可能出现的第二个异常,也是最后一个异常。 为了演示它,您将需要三个事务,其中两个事务将更改数据,而第三个事务将仅读取它。

But first let's restore the state of Bob's accounts:

但是首先让我们恢复Bob的帐户状态:

=> UPDATE accounts SET amount = 900.00 WHERE id = 2;
=> SELECT * FROM accounts WHERE client = 'bob';
id | number | client | amount
----+--------+--------+--------
  3 | 2002   | bob    | 100.00
  2 | 2001   | bob    | 900.00
(2 rows)

In the first transaction, interest on the amount available on all Bob's accounts accrues. Interest is credited to one of his accounts:

在第一笔交易中,将累积所有Bob帐户上可用金额的利息。 利息记入他的帐户之一:

=> BEGIN ISOLATION LEVEL REPEATABLE READ; -- 1
=> UPDATE accounts SET amount = amount + (
  SELECT sum(amount) FROM accounts WHERE client = 'bob'
) * 0.01
WHERE id = 2;

Then another transaction withdraws money from another Bob's account and commits its changes:

然后,另一笔交易从另一个鲍勃的帐户中提款并进行更改:

|  => BEGIN ISOLATION LEVEL REPEATABLE READ; -- 2
|  => UPDATE accounts SET amount = amount - 100.00 WHERE id = 3;
|  => COMMIT;

If the first transaction is committed at this point, no anomaly will occur: we could assume that the first transaction was executed first and then the second (but not vice versa because the first transaction saw the state of the account with id = 3 before that account was changed by the second transaction).

如果此时执行了第一笔交易,则不会发生异常:我们可以假设第一笔交易先执行,然后再执行第二笔交易(但反之则不然,因为在此之前,第一笔交易的帐户状态为id = 3)帐户已被第二次交易更改)。

But imagine that at this point the third (read-only) transaction begins, which reads the state of some account that is not affected by the first two transactions:

但是,想象一下,第三点(只读)交易在此时开始,它读取不受前两个交易影响的某些帐户的状态:

|  => BEGIN ISOLATION LEVEL REPEATABLE READ; -- 3
|  => SELECT * FROM accounts WHERE client = 'alice';
|   id | number | client | amount 
|  ----+--------+--------+--------
|    1 | 1001   | alice  | 800.00
|  (1 row)

And only after that the first transaction is completed:

并且只有在第一笔交易完成后:

=> COMMIT;

What state should the third transaction see now?

第三笔交易现在应该看到什么状态?

|    SELECT * FROM accounts WHERE client = ‘bob’;

Once started, the third transaction could see the changes of the second transaction (which had already been committed), but not of the first (which had not been committed yet). On the other hand, we have already ascertained above that the second transaction should be considered started after the first one. Whatever state the third transaction sees will be inconsistent — this is just the anomaly of a read-only transaction. But at the Repeatable Read level it is allowed:

一旦开始,第三个事务可以看到第二个事务(已经提交)的更改,但是看不到第一个(尚未提交)的更改。 另一方面,我们已经在上面确定了第二笔交易应视为在第一笔交易之后开始。 无论第三笔交易看到什么状态,都将不一致-这只是只读交易的异常。 但在“可重复读取”级别,则允许:

|    id | number | client | amount
|   ----+--------+--------+--------
|     2 | 2001   | bob    | 900.00
|     3 | 2002   | bob    | 0.00
|   (2 rows)
|   => COMMIT;

可序列化 (Serializable)

The Serializable level prevents all anomalies possible. In fact, Serializable is built on top of the Snapshot Isolation. Those anomalies that do not occur with Repeatable Read (such as a dirty, non-repeatable, or phantom read) do not occur at the Serializable level either. And those anomalies that occur (an inconsistent write and a read-only transaction anomaly) are detected, and the transaction aborts — a familiar serialization error occurs: could not serialize access.

可序列化级别可防止所有可能的异常。 实际上,可序列化是建立在快照隔离之上的。 可重复读取不会发生的那些异常(例如脏读,不可重复读取或幻像读取)也不会在可序列化级别上发生。 并且检测到那些发生的异常(不一致的写入和只读事务异常),并且事务中止-发生一个熟悉的序列化错误: 无法序列化访问

写入不一致(写入偏斜) (Inconsistent write (write skew))

To illustrate this, let's repeat the scenario with an inconsistent write anomaly:

为了说明这一点,让我们以不一致的写入异常重复该场景:

=> BEGIN ISOLATION LEVEL SERIALIZABLE;
=> SELECT sum(amount) FROM accounts WHERE client = 'bob';
sum 
----------
 910.0000
(1 row)
|   => BEGIN ISOLATION LEVEL SERIALIZABLE;
|   => SELECT sum(amount) FROM accounts WHERE client = 'bob';
|      sum 
|   ----------
|    910.0000
|   (1 row)
=> UPDATE accounts SET amount = amount - 600.00 WHERE id = 2;
|   => UPDATE accounts SET amount = amount - 600.00 WHERE id = 3;
|   => COMMIT;
=> COMMIT;
ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during commit attempt.
HINT:  The transaction might succeed if retried.

Just like at the Repeatable Read level, an application that uses the Serializable isolation level must repeat transactions that terminated with a serialization error, as the error message prompts us.

就像在“可重复读取”级别上一样,使用“可序列化”隔离级别的应用程序必须重复以序列化错误终止的事务,因为错误消息会提示我们。

We gain simplicity of programming, but the price for that is a forced termination of some fraction of transactions and a need to repeat them. The question, of course, is how large this fraction is. If only those transactions terminated that do incompatibly overlap with other transactions, it would have been nice. But such an implementation would inevitably be resource-intensive and inefficient because you would have to track the operations on each row.

我们可以简化编程,但是这样做的代价是强制终止部分交易,并且需要重复执行。 当然,问题是这个分数有多大。 如果仅那些终止的事务与其他事务不兼容地重叠,那就太好了。 但是,由于您必须跟踪每一行的操作,因此这种实现不可避免地会占用大量资源并且效率低下。

Actually, the implementation of PostgreSQL is such that it allows false negatives: some absolutely normal transactions that are just “unlucky” will also abort. As we will see later, this depends on many factors, such as the availability of appropriate indexes or the amount of RAM available. In addition, there are some other (pretty severe) implementation restrictions, for example, queries at the Serializable level will not work on replicas, and they will not use parallel execution plans. Although the work on improving the implementation continues, the existing limitations make this level of isolation less attractive.

实际上,PostgreSQL实现允许错误的否定:一些绝对“不幸”的绝对正常的事务也将中止。 稍后我们将看到,这取决于许多因素,例如适当索引的可用性或可用的RAM数量。 In addition, there are some other (pretty severe) implementation restrictions, for example, queries at the Serializable level will not work on replicas, and they will not use parallel execution plans. Although the work on improving the implementation continues, the existing limitations make this level of isolation less attractive.

patch). And queries on replicas can start working in PostgreSQL 13 ( patch ). And queries on replicas can start working in PostgreSQL 13 ( another patch). another patch ).

Read-only transaction anomaly (Read-only transaction anomaly)

For a read-only transaction not to result in an anomaly and not to suffer from it, PostgreSQL offers an interesting technique: such a transaction can be locked until its execution is secure. This is the only case when a SELECT operator can be locked by row updates. This is what this looks like:

For a read-only transaction not to result in an anomaly and not to suffer from it, PostgreSQL offers an interesting technique: such a transaction can be locked until its execution is secure. This is the only case when a SELECT operator can be locked by row updates. This is what this looks like:

=> UPDATE accounts SET amount = 900.00 WHERE id = 2;
=> UPDATE accounts SET amount = 100.00 WHERE id = 3;
=> SELECT * FROM accounts WHERE client = 'bob' ORDER BY id;
id | number | client | amount 
----+--------+--------+--------
  2 | 2001   | bob    | 900.00
  3 | 2002   | bob    | 100.00
(2 rows)
=> BEGIN ISOLATION LEVEL SERIALIZABLE; -- 1
=> UPDATE accounts SET amount = amount + (
  SELECT sum(amount) FROM accounts WHERE client = 'bob'
) * 0.01
WHERE id = 2;
|  => BEGIN ISOLATION LEVEL SERIALIZABLE; -- 2
|  => UPDATE accounts SET amount = amount - 100.00 WHERE id = 3;
|  => COMMIT;

The third transaction is explicitly declared READ ONLY and DEFERRABLE:

The third transaction is explicitly declared READ ONLY and DEFERRABLE:

|   => BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE; -- 3
|   => SELECT * FROM accounts WHERE client = 'alice';

When trying to execute the query, the transaction is locked because otherwise it would cause an anomaly.

When trying to execute the query, the transaction is locked because otherwise it would cause an anomaly.

=> COMMIT;

And only after the first transaction is committed, the third one continues execution:

And only after the first transaction is committed, the third one continues execution:

|    id | number | client | amount
|   ----+--------+--------+--------
|     1 | 1001   | alice  | 800.00
|   (1 row)
|   => SELECT * FROM accounts WHERE client = 'bob';
|    id | number | client | amount 
|   ----+--------+--------+----------
|     2 | 2001   | bob    | 910.0000
|     3 | 2002   | bob    | 0.00
|   (2 rows)
|   => COMMIT;

Another important note: if Serializable isolation is used, all transactions in the application must use this level. You cannot mix Read-Committed (or Repeatable Read) transactions with Serializable. That is, you can mix, but then Serializable will behave like Repeatable Read without any warnings. We will discuss why this happens later, when we talk about the implementation.

Another important note: if Serializable isolation is used, all transactions in the application must use this level. You cannot mix Read-Committed (or Repeatable Read) transactions with Serializable. That is, you can mix, but then Serializable will behave like Repeatable Read without any warnings. We will discuss why this happens later, when we talk about the implementation.

So if you decide to use Serializble, it is best to globally set the default level (although this, of course, will not prevent you from specifying an incorrect level explicitly):

So if you decide to use Serializble, it is best to globally set the default level (although this, of course, will not prevent you from specifying an incorrect level explicitly):

ALTER SYSTEM SET default_transaction_isolation = 'serializable';
book and book and lecture course by Boris Novikov “Fundamentals of database technologies” (available in Russion only). lecture course by Boris Novikov “Fundamentals of database technologies” (available in Russion only).

What isolation level to use? (What isolation level to use?)

The Read Committed isolation level is used by default in PostgreSQL, and it is likely that this level is used in the vast majority of applications. This default is convenient because at this level a transaction abort is possible only in case of failure, but not as a means to prevent inconsistency. In other words, a serialization error cannot occur.

The Read Committed isolation level is used by default in PostgreSQL, and it is likely that this level is used in the vast majority of applications. This default is convenient because at this level a transaction abort is possible only in case of failure, but not as a means to prevent inconsistency. In other words, a serialization error cannot occur.

The other side of the coin is a large number of possible anomalies, which have been discussed in detail above. The software engineer always has to keep them in mind and write code so as not to allow them to appear. If you cannot code the necessary actions in a single SQL statement, you have to resort to explicit locking. The most troublesome is that code is difficult to test for errors associated with getting inconsistent data, and the errors themselves can occur in unpredictable and non-reproducible ways and are therefore difficult to fix.

The other side of the coin is a large number of possible anomalies, which have been discussed in detail above. The software engineer always has to keep them in mind and write code so as not to allow them to appear. If you cannot code the necessary actions in a single SQL statement, you have to resort to explicit locking. The most troublesome is that code is difficult to test for errors associated with getting inconsistent data, and the errors themselves can occur in unpredictable and non-reproducible ways and are therefore difficult to fix.

The Repeatable Read isolation level eliminates some of the inconsistency problems, but alas, not all. Therefore, you must not only remember about the remaining anomalies, but also modify the application so that it correctly handles serialization errors. It is certainly inconvenient. But for read-only transactions, this level perfectly complements Read Committed and is very convenient, for example, for building reports that use multiple SQL queries.

The Repeatable Read isolation level eliminates some of the inconsistency problems, but alas, not all. Therefore, you must not only remember about the remaining anomalies, but also modify the application so that it correctly handles serialization errors. It is certainly inconvenient. But for read-only transactions, this level perfectly complements Read Committed and is very convenient, for example, for building reports that use multiple SQL queries.

Finally, the Serializable level allows you not to worry about inconsistency at all, which greatly facilitates coding. The only thing that is required of the application is to be able to repeat any transaction when getting a serialization error. But the fraction of aborted transactions, additional overhead, and the inability to parallelize queries can significantly reduce the system throughput. Also note that the Serializable level is not applicable on replicas, and that it cannot be mixed with other isolation levels.

Finally, the Serializable level allows you not to worry about inconsistency at all, which greatly facilitates coding. The only thing that is required of the application is to be able to repeat any transaction when getting a serialization error. But the fraction of aborted transactions, additional overhead, and the inability to parallelize queries can significantly reduce the system throughput. Also note that the Serializable level is not applicable on replicas, and that it cannot be mixed with other isolation levels.

Read on. 继续阅读 。

翻译自: https://habr.com/en/company/postgrespro/blog/467437/

你可能感兴趣的:(数据库,python,java,mysql,oracle)