译者|康绍景评论|SunShujuanIsolation的定义是当数据库并发执行多个事务时,不会影响其他事务的执行。本文解释了这些隔离级别并概述了它们之间的权衡。我们还建议选择最适合您需要的隔离级别。让我们从有效使用隔离级别所需的最少知识开始,并检查代表大多数应用程序的两个用例及其对不同隔离级别的影响。用例1:银行交易客户从银行账户取款:开始交易;读取用户余额;在活动表中创建行(我们避免将其称为事务以避免与数据库事务混淆);subtractfromamountread提取金额后,更新用户余额;提交。在交易完成之前,我们不希望用户的余额发生变化。用例2:零售交易一位国际客户以不同于标价的货币从零售店购买商品:开始交易;读取exchange_rate表以获得最新的汇率;在订单表中创建一行;提交。假设有一个单独的进程在不断地更新汇率,但是我们不关心汇率在读取之??后是否发生了变化,即使当前交易还没有完成。Serializable隔离级别是唯一满足ACID属性理论定义的级别。它本质上是说不允许两个并发事务相互干扰彼此的更改,并且如果一个接一个地执行,则必须产生相同的结果。不幸的是,Serializable通常被认为是不切实际的,即使对于非分布式数据库也是如此。所有现有的流行数据库(如Postgres和MySQL)都不推荐它并非巧合。为什么这个设置如此不切实际?让我们看两个用例:在银行用例中,Serializable是完美的。数据库读取用户余额后,保证用户余额不会发生变化。因此,应用业务逻辑是安全的,例如确保用户有足够的余额,并根据读取的值写入新的余额。在银行用例中,Serializable是完美的。在零售用例中,Serializable也能正常工作。在创建订单的事务成功之前,不允许更新汇率的进程执行其操作。由于事件的精确顺序,这听起来像是一个很棒的功能。但是,如果创建订单的交易缓慢而复杂怎么办?也许它需要去仓库检查库存。也许它必须对下订单的用户进行信用检查。它将锁定该行,防止速率进程更新它。这种意外的依赖关系会阻止系统扩展。可序列化设置也经常死锁。例如,如果两个事务读取一个用户的余额,它们将在该行上放置一个共享读锁。如果事务稍后修改该行,它们将尝试将读锁升级为写锁。这将导致死锁,因为每个事务都会被另一个事务持有的读锁阻塞。正如我们将在下面看到的,不同的隔离级别可以很容易地避免这个问题。换句话说,有问题的工作负载不会随着Serializable设置而扩展。如果工作负载没有争议,我们就不需要这个隔离级别。较低的隔离度可能同样有效。为了解决这个不必要且代价高昂的安全问题,必须重构应用程序。例如,获取汇率的代码在交易开始之前被调用,或者使用单独的连接来完成读取过程。虽然理论上不那么纯粹,但其他隔离级别允许您根据具体情况执行序列化读取。这使得它们在编写可伸缩系统时更加灵活和有用。无锁实现具有无需锁定数据即可提供可序列化一致性的方法。然而,这样的系统遇到上述相同的问题,即冲突事务以不同的方式失败。问题的根本原因在于隔离级别本身,任何实现都不能使您摆脱这些限制。RepeatableReadRepeatableRead是一个晦涩的设置。因为它区分了点选择和搜索,并为每个点定义了不同的行为。这不是非黑即白的,并已导致许多其他实施。这里不详细讨论此隔离级别。但是,对于我们的用例,RepeatableRead提供与Serializable相同的保证,因此继承相同的问题。尽管SnapshotRead隔离级别不是ANSI标准,但它已变得越来越流行。也称为MVCC。这种隔离级别的优点是它是无争用的:它在事务开始时创建一个快照。所有读取都发送到此快照而不获取任何锁。但是写操作遵循严格的可串行化规则。SnapshotRead事务对于只读工作负载最有价值,因为您可以看到一致的数据库快照。这避免了在加载事务上相互依赖的不同数据片段时出现意外。您还可以使用快照功能在特定时间读取多个表,然后观察自该快照以来发生的更改。这对于想要将更改流式传输到分析数据库的更改数据捕获工具来说非常方便。对于执行写入的事务,快照功能不是很有用。您主要想控制该值自上次读取后是否允许更改。如果你想允许这个值改变,你一读它就会变得无效,因为其他人可以稍后更新它。因此,无论您是从快照读取还是获取最新值都没有关系。如果不需要更改,则需要最新值并且必须锁定该行以防止更改。换句话说,SnapshotRead适用于只读工作负载,但对于写入工作负载,它并不比ReadCommitted好,我们将在接下来介绍。在此隔离级别中重新应用零售用例很自然,没有争用:从汇率中读取的值产生交易创建时的快照值。当此交易正在进行时,允许单独的交易来更新汇率。银行用例怎么样?数据库允许您锁定数据。例如,MySQL能够“select...locksinsharedmode”(读锁)。此模式将读取提升为可序列化事务的读取。当然,这种隔离级别的死锁风险也是有继承性的。较低的隔离级别提供了两全其美的方法。您可以发出“select...forupdate”(写锁)。此锁可防止另一个事务获取此行上的任何类型的锁。这种悲观锁定方法乍一听很糟糕,但它允许两个竞争事务成功完成而不会遇到死锁。第二个事务将等待第一个事务完成,此时它将读取并锁定新值的行。MySQL默认支持SnapshotRead隔离级别,但会调用它REPEATABLE_READ。分布式数据库虽然有多种方法可以有效地实现单个数据库的可重复读取,但在分布式数据库中,问题变得更加复杂。这是因为事务可以跨越多个分片。如果是这样,系统必须提供严格的排序保证。这种排序要求系统使用集中式并发控制机制或全局一致的时钟。这两种方法本质上都试图将原本可以彼此独立执行的事件紧密耦合。因此,在期望分布式数据库支持分布式快照读取之前,必须理解并愿意接受这些权衡。CommittedReadCommitted隔离比SnapshotRead更明确,因为它不断返回数据库的最新视图。这也是隔离级别中争议最少的。在这个层次上,你可能每次读一行都会得到不同的值。ReadCommitted设置还允许您通过发出读取或写入锁来促进读取,从而有效地允许您按需执行可序列化读取。如前所述,这种方法为打算修改数据的应用程序事务提供了两全其美的方法。Postgres支持的默认隔离级别是ReadCommitted。读取未提交隔离级别通常被认为是不安全的,不建议用于分布式或非分布式设置。这是因为您可能正在读取稍后可能会回滚的数据(或最初不存在的数据)。分布式事务的主题与隔离级别正交,但必须在此处介绍,因为它在保持事物松散耦合方面很重要。在分布式系统中,如果两行位于不同的分片或数据库中,并且您想在单个事务中以原子方式修改它们,则会产生两阶段提交(2PC)的开销。这需要更多工作:创建有关分布式事务的元数据并将其保存到持久存储中。准备所有单独的交易发布。提交的决定被保存到元数据中。向准备好的事务发出提交。prepare要求您保存元数据,以便如果节点在提交(或回滚)之前崩溃,事务可以在新的领导者中恢复。分布式事务还与隔离级别交互。例如,假设2PC事务只有第一次提交成功,第二次提交被延迟。如果应用程序已经读取了第一次提交的结果,那么数据库必须阻止应用程序读取第二次提交的行,直到完成为止。相反,如果应用程序在第二次提交之前读取了一行,它肯定看不到第一次提交的效果。数据库必须做额外的工作来支持分布式事务的隔离保证。如果应用程序可以容忍这些部分提交怎么办?然后我们正在做应用程序不关心的不必要的工作。可能值得引入一个新的隔离级别,如ReadPartialCommits。请注意,这与ReadUncommitted的不同之处在于,用户读取的数据最终可能会被回滚。最后,过度使用2PC会降低系统的整体可用性和延迟。这是因为性能最差的分片将决定您的有效可用性。总结为了可伸缩,应用程序应该避免依赖数据库的任何高级隔离特性。相反,它应该尽可能少地使用保证。如果应用程序可以编写为使用ReadCommitted隔离级别,则不建议迁移到SnapshotRead。可序列化或可重复读取。最好避免多语句事务,但随着应用程序的增长,这可能变得不可避免。此时,尝试主要依赖事务的原子保证,并保持数据库系统支持的最低隔离级别。如果使用分片数据库,请完全避免分布式事务。这可以通过将相关行保留在同一个分片中来实现。这必须从一开始就完成,因为很难将非并发程序重构为并发程序。译者介绍康少京,社区编辑,从事通讯行业,从事底层驱动开发岗位。原标题:OptimizingIsolationLevelsforScalingDistributedDatabases,作者:SuguSougoumarane
