交易隔离水平以及为什么我们应该关心
#sql #database #性能

这就是关于表演的

SQL数据库一次不能一次处理一个传入连接,因为它对系统的性能是毁灭性的。我们希望数据库并行接受许多呼叫者,并尽快执行他们的请求。当这些呼叫者索要不同的数据时,即第一个呼叫者从表1中读取时,如何做到这一点,而第二个呼叫者则从表2读取。但是,经常不同的呼叫者想从并写入并写入同一表。我们应该如何处理这些查询?操作顺序和最终结果应该是什么?这是交易隔离级别发挥作用的地方。

交易是一组查询(例如 select insert update delete )执行应作为工作单位完成的数据库。这意味着他们要么需要执行全部,要么不应执行它们。执行交易需要时间。例如,单个更新语句可能会修改多个行。数据库系统需要修改每一行,这需要时间。执行更新时,可能会开始另一笔交易并尝试读取当前正在修改的行。我们可能会问的问题是 - 其他交易是否应该读取新的行值(尽管并非全部已经更新),旧的行值(尽管其中一些已经更新),或者应该等待?如果由于任何原因需要取消第一次交易,该怎么办?另一笔交易应该发生什么?

交易隔离水平控制我们如何确定交易之间的数据完整性。他们决定如何执行交易,何时应该等待以及允许出现什么异常。在理论上,我们可能希望允许一些异常来提高系统的性能。

阅读现象

根据我们如何控制数据库中的并发,可能会出现不同的读取现象。标准SQL 92定义了三个读取现象,描述了两次交易同时执行而没有交易隔离的各种问题。

我们将使用以下人员表作为示例:

+-----+-------+--------+
| id  | name  | salary |
+-----+-------+--------+
|  1  | John  |    150 |
|  2  | Jack  |    200 |
+-----+-------+--------+

肮脏的阅读

当两个交易访问相同的数据并且我们允许尚未提交的读取值时,我们可能会得到肮脏的读取。可以说,我们有两项交易进行以下操作:

+----------------------------------------+---------------------------------------------+
|             Transaction 1              |                Transaction 2                |
+----------------------------------------+---------------------------------------------+
|                                        | UPDATE People SET salary = 180 WHERE id = 1 |
| SELECT salary FROM People WHERE id = 1 |                                             |
|                                        | ROLLBACK                                    |
+----------------------------------------+---------------------------------------------+


交易2 id = 1 修改行,然后交易1 读取行并获取值 180 交易2 将事物倒退。有效地,事务1 使用数据库中不存在的值。我们期望在这里期望的是,交易1 使用在某个时间点在数据库中成功提交的值。

可重复阅读

可重复的读取是一个问题,当交易读取相同的内容两次并且每次都会获得不同的结果。可以说交易做以下操作:

+----------------------------------------+---------------------------------------------+
|             Transaction 1              |                Transaction 2                |
+----------------------------------------+---------------------------------------------+
| SELECT salary FROM People WHERE id = 1 |                                             |
|                                        | UPDATE People SET salary = 180 WHERE id = 1 |
|                                        | COMMIT                                      |
| SELECT salary FROM People WHERE id = 1 |                                             |
+----------------------------------------+---------------------------------------------+

事务1 读取一行并获取值 150 交易2 修改同一行。然后交易1 再次读取该行并获得不同的值( 180 )。

我们期望在这里读取相同的值两次。

幻影阅读

Phantom读取是一种情况,交易以相同的方式寻找行但结果不同。让我们记住以下内容:

+-----------------------------------------+-------------------------------------------------------------+
|             Transaction 1               |                        Transaction 2                        |
+-----------------------------------------+-------------------------------------------------------------+
| SELECT * FROM People WHERE salary < 250 |                                                             |
|                                         | INSERT INTO People(id, name, salary) VALUES (3, Jacob, 120) |
|                                         | COMMIT                                                      |
| SELECT * FROM People WHERE salary < 250 |                                                             |
+-----------------------------------------+-------------------------------------------------------------+

交易1 读取行,发现其中两个与条件匹配。 交易2 添加了与交易1 使用的条件相匹配的另一行。当交易1 再次读取时,它会得到一组不同的行。我们希望对 交易1

隔离水平

sql 92 标准定义了各种隔离水平,这些隔离水平定义了可能发生的现象。有4个标准级别:读取未交易的读取可重复读取 serializable 。。

读取未交易的允许交易读取尚未投入数据库的数据。这允许最高的性能,但也导致大多数不希望的读取现象。

读取的允许交易仅读取所投入的数据。这避免了阅读“后来消失”的数据的问题,但不能保护其他读取现象。

可重复的读取级别试图避免读取数据两次读取数据并获得不同的结果。

最后,序列化试图避免所有读取现象。

下表显示允许哪种现象:

+--------------------+-------------+------------------+---------+
| Level \ Phenomena  | Dirty read  | Repeatable read  | Phantom |
+--------------------+-------------+------------------+---------+
| READ UNCOMMITED    | +           | +                | +       |
| READ COMMITED      | -           | +                | +       |
| REPEATABLE READ    | -           | -                | +       |
| SERIALIZABLE       | -           | -                | -       |
+--------------------+-------------+------------------+---------+

隔离水平是每个交易定义的。例如,它允许一项交易以 serializalble级别运行,而另一个交易则可以使用读取不合格。。

它如何在引擎盖下工作

数据库需要实施确保缺乏特定读取现象的机制。通常有两种解决这些方法的广泛方法:悲观的锁定和乐观的锁定。

悲观的锁定

第一种方法称为悲观锁定。在这种方法中,我们希望通过确保交易不会引入有问题的变化来避免问题。我们通过锁定数据库的特定部分来做到这一点。当给定零件通过一项交易锁定时,另一笔交易将无法根据交易隔离级别读取或写入数据以避免问题。

数据库中有各种级别的锁:可以将它们存储在行级别,页面级别(我们可以考虑本文目的的一组行),表级别和整个数据库级别。还有各种类型的锁:读,写作的锁,可以在交易之间共享的锁,意图锁等。本文一般关注SQL数据库,因此我们将详细介绍实际实施的详细信息。

从概念上讲,为避免给定的读取现象,交易需要以保证其他交易不会引入更改导致特定类型的读取现象的方式锁定数据库的特定部分。例如,为避免肮脏的读取,我们需要锁定所有修改或读取行,以使其他交易无法读取或修改它们。

这种方法有多个优势。首先,它可以根据可以修改和可以安全进行的交易来实现细粒度。其次,当有多个在不同数据上工作时,它会很好地缩放并施加较低的开销。第三,交易不需要回滚。

但是,这可以大大降低性能。例如,如果两个交易想在同一表中读取和修改数据,并且这两种交易都在可序列化的级别上运行,那么他们将需要彼此等待完成。即使他们从桌子上碰到不同的行。

大多数数据库管理系统都使用这种方式。例如,MS SQL将其用于其4个主要隔离级别。

乐观的锁定

另一种方法称为乐观锁定。这种方法也称为快照隔离或多元次数并发控制(简称MVCC)。表中的每个实体都与其具有关联的版本编号。当我们修改行时,我们还会增加其行版本,因此其他交易可以观察到它更改。

交易开始时,它会记录版本号,因此知道行的状态是什么。当它从表中读取时,仅提取在交易开始之前修改的行。接下来,当事务修改数据并尝试将其提交数据库时,数据库会验证行版本。如果同时其他一些交易对行进行了修改,则现在拒绝更新,交易必须从头开始。

在交易触动不同行的情况下,这种方法效果很好,因为这样他们就可以在没有问题的情况下提出。这样可以提高缩放性和更高的性能,因为交易不需要锁定。但是,当交易经常修改相同的行时,某些交易将需要经常回滚。这会导致性能退化。另一个缺点是需要保留行版本。这增加了数据库系统的复杂性。
各种数据库管理系统使用此方法。例如,启用快照的Oracle或MS SQL。

实际考虑

虽然隔离级别似乎已经很好地定义了,但有许多小细节会影响数据库系统在引擎盖下的工作方式。让我看一些。

隔离水平不是强制性的

sql 92 标准定义了多个隔离水平,但不是强制性的。这意味着给定数据库管理系统中的所有级别都可以按照 serializable 实现。我们使用其他隔离水平来提高性能,但并非以任何方式执行。这意味着,如果我们依靠一个数据库管理系统中发生的特定优化,则可能在另一个数据库管理系统中不使用相同的优化。我们不应该依靠实施细节,而应该坚持标准。

默认隔离水平不标准化

默认隔离级别是每个事务配置的。这通常由用于连接到数据库的库或连接技术决定。根据您的默认设置,您可能会以不同的隔离级别进行操作,这可能会导致不同的结果或不同的性能。典型的库使用序列化读取的级别。

读取问题的问题

读取的保证交易仅读取的数据,它并不能保证其读取的数据是最新的数据。它可能会读取过去某个时刻所投入的值,但后来被另一笔交易覆盖。

读取的另一个问题级别。由于实体如何存储在引擎盖下,交易可能会两次读取特定行或跳过。让我看看为什么。

典型的数据库管理系统以有序的方式将行存储在表中,通常使用b-tree中表的主键。这是因为主要密钥通常会施加群集索引,从而导致数据在磁盘上进行物理排序。现在,让我们假设有10行,ID从1到10。让我们说我们的交易已经读取8行,因此ID从1到8个(包括)。现在,如果另一个事务修改了ID = 2并将ID值更改为11(和提交)的行,则我们将继续扫描并总共找到11行。更重要的是,我们将阅读ID = 2的行,但行不存在!

Image description

基于同一想法,我们可能会错过一排。可以说我们总共有10行,我们已经读取了1行。订购。

Image description

白色和黑色大理石问题

我们提到了两种不同的实现锁的方法。悲观的锁定锁定行,并在重新锁定时取消其他交易以修改它们。乐观的锁定存储行版本,并允许其他交易在最新数据上运行。

当它以乐观的锁定为单位,称为白色大理石和黑色大理石问题时,还有另一个问题的问题。让我们拿以下大理石表:

+-----+--------+-------------+
| id  | color  | row_version |
+-----+--------+-------------+
|  1  | black  |           1 |
|  2  | white  |           1 |
+-----+--------+-------------+

现在,我们要说我们要进行两项交易。首先试图将所有黑石变成白色。另一个试图做相反的事情 - 试图将所有白人变成黑人。我们有以下内容:

+-----------------------------------------------------------+----------------------------------------------------------+
|                      Transaction 1                        |                      Transaction 2                       |
+-----------------------------------------------------------+----------------------------------------------------------+
| UPDATE Marbles SET color = 'white' WHERE color = 'black'  | UPDATE Marbles SET color = 'black' WHERE color = 'white' |
+-----------------------------------------------------------+----------------------------------------------------------+

现在,如果我们以悲观的锁定实现序列化,则典型的实现将锁定整个表。运行两项交易后,我们以两个黑石头结尾(如果首先执行交易1 ,然后是交易2 )或两个白色石头(如果我们执行,交易2 ,然后交易1 )。

但是,如果我们使用乐观的锁定,我们将获得以下内容:

+-----+--------+-------------+
| id  | color  | row_version |
+-----+--------+-------------+
|  1  | white  |           2 |
|  2  | black  |           2 |
+-----+--------+-------------+

由于两项交易都触及了不同的行,因此它们可以并行运行。这导致了意外的结果。

概括

在本文中,我们看到了什么是交易隔离水平以及它们如何允许不同的读取现象。我们还了解了数据库系统如何在概念上实施它们,以及它们如何导致意外结果。