乐观与悲观锁定

Jas*_*ker 510 sql-server locking optimistic-locking pessimistic-locking

我理解乐观和悲观锁定*之间的区别.现在有人可以向我解释我何时会使用其中任何一个?

这个问题的答案是否会根据我是否使用存储过程来执行查询而改变?

*但只是为了检查,乐观的意思是"在阅读时不要锁定桌子",悲观意味着"在阅读时锁定桌面".

Con*_*lls 742

乐观锁定是一种策略,您可以在其中读取记录,记录版本号(其他方法包括日期,时间戳或校验和/哈希),并在写回记录之前检查版本是否未更改.当你写回记录时,你过滤版本的更新,以确保它是原子的.(即,在检查版本并将记录写入磁盘之间未进行更新)并在一次更新中更新版本.

如果记录是脏的(即与您的版本不同),则中止事务并且用户可以重新启动它.

此策略最适用于大容量系统和三层体系结构,在这些体系结构中,您不必为会话维护与数据库的连接.在这种情况下,客户端实际上无法维护数据库锁,因为从池中获取连接,并且您可能没有使用从一个访问到下一个访问的相同连接.

悲观锁定是指您在完成操作之前锁定专用的记录.它具有比乐观锁定更好的完整性,但要求您小心应用程序设计以避免死锁.要使用悲观锁定,您需要直接连接到数据库(通常是双层客户端服务器应用程序中的情况)或可以独立于连接使用的外部可用事务ID.

在后一种情况下,您使用TxID打开事务,然后使用该ID重新连接.DBMS维护锁定并允许您通过TxID选择备份会话.这就是使用两阶段提交协议(例如XACOM +事务)的分布式事务的工作方式.

  • 乐观锁定不一定使用版本号.其他策略包括使用(a)时间戳或(b)行本身的整个状态.后一种策略很难看,但在您无法修改架构的情况下,无需使用专用版本列. (134认同)
  • @supercat - 不同意乐观锁定的准确性低于100% - 只要它检查交易的所有输入记录应该在一段时间内保持不变,它就像那些保持不变的悲观锁定(选择更新样式)一样准确同样的记录.主要区别在于乐观锁定仅在存在冲突时才会产生开销,而悲观锁定则减少了冲突开销.如果大多数交易不冲突,那么乐观是最好的 - 我希望大多数应用程序通常都是如此. (18认同)
  • 您应该提到,选择还取决于读取与写入的比率:如果您的应用程序主要是很多用户的只读应用程序,并且有时您写入数据,那么请选择乐观锁定。例如,StackOverflow 有很多人阅读页面,有时有人编辑一个:在悲观锁定中,谁会获得锁定?第一个?在乐观锁定中,想要编辑页面的人只要拥有页面的最新版本就可以进行编辑。 (4认同)
  • 乐观锁的概念并不一定需要 100% 可靠的方式来知道某些东西是否已被更改;无法检测的更改是不可接受的,但偶尔的错误更改报告可能不会太糟糕,特别是如果收到此类报告的代码重新读取数据并检查它是否确实已更改。 (2认同)
  • @geek - 您的持久性提供程序必须明确支持乐观并发。如果您使用的是 ORM,您可能需要做一些功课,了解它如何(或是否)执行此操作。在实践中,DB 层应该查询时间戳,然后(在更新时)使用包含 ts=@timestamp 或类似内容的谓词进行更新。然后检查更新的行数。如果为零,那么您的记录版本已经过时,并且必须从中重新加载并向用户返回并发错误。 (2认同)
  • @geek - 分布式事务协议(如XA)允许在一个或多个系统周围对单独的事务标识符进行网格化.这种类型的协议确实允许通过池化连接使用锁,因为事务标识符从会话解除耦合并明确提供.但是,这确实会产生一些开销,并且如果您的应用程序不仔细跟踪它们,则容易出现泄漏锁定和事务标识符.这是一个更加重量级的解决方案. (2认同)
  • @Legends - 使用optimsitic锁定肯定是Web应用程序的合适策略. (2认同)

Ily*_*tov 162

当您不期望很多碰撞时使用乐观锁定.执行正常操作的成本较低,但如果发生冲突,则在交易中止时您将支付更高的价格来解决它.

当预期发生碰撞时使用悲观锁定.简单地阻止了违反同步的事务.

要选择正确的锁定机制,您必须估计读取和写入的数量并相应地进行规划


Kei*_*ith 66

乐观地假设在你阅读时没有任何改变.

悲观假设某些东西将会锁定它.

如果数据完全读取并不重要,请使用乐观.你可能会得到奇怪的"脏"读 - 但它不太可能导致死锁等.

大多数Web应用程序都可以使用脏读 - 在极少数情况下,数据与下次重新加载不完全相符.

对于精确的数据操作(如在许多金融交易中),使用悲观.准确读取数据至关重要,没有未显示的变化 - 额外的锁定开销是值得的.

哦,并且Microsoft SQL服务器默认为页面锁定 - 基本上是您正在阅读的行以及其中的一些.行锁定更准确但速度更慢.通常值得将事务设置为read-committed或no-lock以避免在读取时出现死锁.

  • 读取一致性是一个单独的问题 - 对于PostgreSQL,Oracle和许多其他数据库,无论是否提交任何更新,您都可以获得一致的数据视图,即使是独占的行锁也不会受到影响. (2认同)
  • 埃里克·布鲁尔 (Eric Brewer) 是提出 CAP 定理的大师 [谈到银行业的一致性](http://highscalability.com/blog/2013/5/1/myth-eric-brewer-on-why-banks-are-base -not-acid-availability.html)。它与你尊敬它的原因相反。 (2认同)

ska*_*man 42

除了已经说过的内容之外,应该说乐观锁定倾向于以牺牲可预测性为代价来改善并发性.悲观锁定往往会降低并发性,但更容易预测.

你支付你的钱等

  • 我没有看到可预测性(无论你定义它)如何通过悲观锁定得到改善 - 如果你的意思是'一旦锁定就完成了事务'你是对的,但是直到事务具有所有需要的锁定,它可能会面临延迟其余的锁,实际上可能会因DB的死锁检测+分辨率逻辑而中止.使用悲观锁定的应用程序可能具有高度不可预测的执行时间 - 典型的例子是有人锁定记录X然后去吃午餐,然后用户锁定记录X和Y,然后锁定另一个Y和Z,依此类推,直到大多数用户被阻止. .. (3认同)

Nik*_*lay 19

当悲观锁定是更好的选择时,我会想到另一种情况.

对于乐观锁定,数据修改中的每个参与者必须同意使用这种锁定.但是如果有人修改数据而不关心版本列,这将破坏乐观锁定的整个想法.


小智 12

基本上有两个最受欢迎的答案.在第一个基本上说

乐观需要一个三层体系结构,在这种体系结构中,您不必为会话保持与数据库的连接,而悲观锁定是指您在完成操作之前锁定记录以供独占使用.与乐观锁定相比,它具有更好的完整性,您需要直接连接到数据库.

另一个答案是

乐观(版本控制)更快,因为没有锁定但是(悲观)锁定在争用较高时表现更好,并且最好防止工作而不是丢弃它并重新开始.

要么

当您遇到罕见的碰撞时,乐观锁定效果最佳

因为它被放在这个页面上.

我创建了我的答案来解释"保持连接"与"低冲突"的关系.

要了解哪种策略最适合您,请不要考虑数据库具有的每秒事务数,而是考虑单个事务的持续时间.通常,您打开trasnaction,performa操作并关闭事务.这是一个简短的经典事务ANSI已经考虑到并且很好地逃脱了锁定.但是,您如何实施预订系统,许多客户同时预订相同的房间/座位?

您浏览优惠,填写表格,提供大量可用选项和当前价格.这需要花费很多时间,选项可能会过时,所有价格之间的价格无效,你开始填写表格并按下"我同意"按钮,因为你访问过的数据没有锁定,其他人,更敏捷,有意思更改所有价格,您需要重新启动新价格.

您可以在阅读时锁定所有选项.这是一种悲观的情景.你明白为什么它很糟糕.您的系统可以由一个只是开始预订并吸烟的小丑打倒.在他结束之前,没有人能保留任何东西.您的现金流量降至零.这就是为什么乐观的保留在现实中使用.那些磨蹭太久的人必须以更高的价格重新开始预订.

在这种乐观的方法中,您必须记录您读取的所有数据(如我的重复读取)并使用您的数据版本到达提交点(我想以您在此报价中显示的价格购买股票,而不是当前价格).此时,将创建ANSI事务,它会锁定数据库,检查是否有任何更改并提交/中止您的操作.IMO,这是MVCC的有效模拟,它也与Optimistic CC相关联,并假设您的事务在中止的情况下重新启动,即您将进行新的预订.此处的交易涉及人类用户决策.

我不太了解如何手动实现MVCC,但我认为长期运行的事务选项是重启是了解主题的关键.如果我在任何地方都错了,请纠正我.我的答案是由Alex Kuznecov章节推动的.


Koe*_*egg 11

在大多数情况下,乐观锁定更有效,并提供更高的性能.在悲观锁定和乐观锁定之间进行选择时,请考虑以下事项:

  • 如果用户尝试同时更新数据有很多更新和相对较高的机会,则悲观锁定很有用.例如,如果每个操作一次可以更新大量记录(银行可能会在每个月末向每个帐户添加利息收入),并且两个应用程序同时运行此类操作,则会发生冲突.

  • 在包含经常更新的小表的应用程序中,悲观锁定也更合适.在这些所谓的热点的情况下,冲突很可能是乐观锁定浪费了回滚冲突交易的努力.

  • 如果冲突的可能性非常低,则乐观锁定很有用 - 有许多记录但用户相对较少,或者很少更新,而且大多数是读取类型的操作.


Yil*_*maz 9

假设在电子商务应用程序中,用户想要下订单。该代码将由多个线程执行。在 中pessimistic locking,当我们从数据库获取数据时,我们将其锁定,这样其他线程就无法修改它。我们处理数据,更新数据,然后提交数据。之后,我们释放锁。这里的锁定持续时间很长,我们从一开始就锁定了数据库记录直到提交。

在 中optimistic locking,我们获取数据并在不加锁的情况下处理数据。所以多个线程可以同时执行到目前为止的代码。这会加快速度。当我们更新时,我们锁定数据。我们必须验证没有其他线程更新该记录。例如,如果库存中有 100 件商品,我们必须将其更新为 99(因为您的代码可能是quantity=queantity-1),但如果另一个线程已经使用了 1,则应该是 98。我们在race condition这里。在这种情况下,我们重新启动线程,以便从头开始执行相同的代码。但这是一个昂贵的操作,你已经结束了然后又重新启动。如果我们有一些竞争条件,那不会有什么大不了的,如果竞争条件很高,就会有很多线程需要重新启动。我们可能会循环运行。在竞争条件较高的情况下,我们应该使用“悲观锁定”


Vla*_*cea 6

处理冲突时,有两种选择:

  • 您可以尝试避免冲突,这就是悲观锁的作用。
  • 或者,您可以允许发生冲突,但是在提交事务时需要检测到冲突,这就是乐观锁定的作用。

现在,让我们考虑以下丢失更新异常

丢失的更新

丢失更新异常可能发生在“ 读取已提交”隔离级别中。

在上图中,我们可以看到Alice相信她可以从她account的帐户中提取40英镑,但没有意识到Bob刚刚更改了帐户余额,现在该帐户中只剩下20个了。

悲观锁定

悲观锁定通过对帐户进行共享或读取锁定来实现此目标,因此可以防止Bob更改帐户。

丢失更新悲观锁定

在上图中,Alice和Bob都将获得account两个用户都已读取的表行的读取锁。使用“可重复读”或“可序列化”时,数据库将在SQL Server上获取这些锁。

由于Alice和Bob都已读取accountPK值为的1,因此他们都无法更改它,直到一个用户释放读取锁。这是因为写操作需要获取写/排他锁,而共享/读锁会阻止写/排他锁。

只有在Alice提交了事务并account在行上释放了读取锁之后,Bob UPDATE才会恢复并应用更改。在Alice释放读取锁之前,Bob的UPDATE会阻塞。

有关数据访问框架如何使用底层数据库悲观锁定支持的更多详细信息,请参阅本文

乐观锁

乐观锁定允许发生冲突,但是在版本更改后应用Alice的UPDATE时会检测到冲突。

应用层交易

这次,我们还有一个version专栏。version列每次执行UPDATE或DELETE时都会增加,并且在UPDATE和DELETE语句的WHERE子句中也使用该列。为此,我们需要version在执行UPDATE或DELETE之前发出SELECT并读取当前值,否则,我们将不知道将哪个版本值传递给WHERE子句或进行递增。

有关数据访问框架如何实现乐观锁定的更多详细信息,请参阅本文

应用层交易

关系数据库系统已经出现在70年代末80年代初,当时客户通常会通过终端连接到大型机。这就是为什么我们仍然看到数据库系统定义诸如SESSION设置之类的术语的原因。

如今,通过Internet,我们不再在同一数据库事务的上下文中执行读写操作,而ACID不再足够。

例如,考虑以下用例:

在此处输入图片说明

没有乐观锁定,即使数据库事务使用了Serializable,也不会捕获此丢失的更新。这是因为读写是在单独的HTTP请求中执行的,因此是在不同的数据库事务上执行的。

因此,即使在使用包含用户思考时间的应用程序级事务时,乐观锁定也可以帮助您防止丢失更新。

有关应用程序级或逻辑事务的更多详细信息,请参阅本文

结论

乐观锁定是一种非常有用的技术,即使在使用不太严格的隔离级别(如“读取已提交”)或在后续数据库事务中执行读写操作时,它也可以正常工作。

乐观锁定的缺点是,在捕获到时OptimisticLockException,数据访问框架会触发回滚,因此会丢失当前正在执行的事务之前我们所做的所有工作。

争用越多,冲突越多,中止交易的机会就越大。由于数据库系统需要还原所有可能涉及表行和索引记录的所有当前挂起的更改,因此回滚对于数据库系统而言可能是昂贵的。

因此,当冲突频繁发生时,悲观锁定可能比较适合,因为它减少了回滚事务的机会。

  • 对于什么场景你建议选择OptimisticLocking和PessimisticLocking?它是否取决于 OptimisticLockException 发生的频率? (3认同)
  • @StimpsonCat,从我从他的结论中读到的内容来看,是的,如果你经常遇到异常,那么最好采用悲观锁定。就像我的例子一样,发生异常的机会非常小,所以我会选择乐观锁定。 (3认同)
  • @EralpB Google、StackOverflow、YoutTube、GitHub、Twitter、LinkedIn,你到处都能找到我 (3认同)
  • 已投赞成票。尽管这些材料并不新颖,但随着越来越多的一次性作业问题充斥系统,解释清楚的答案在 SO 中变得罕见。 (2认同)

小智 5

乐观锁定的一个用例是让您的应用程序使用数据库来允许您的线程/主机之一“声明”一项任务。这是一种对我来说经常派上用场的技术。

我能想到的最好的例子是使用数据库实现的任务队列,多个线程同时声明任务。如果任务的状态为“可用”、“声明”、“已完成”,则数据库查询可以说“设置状态=“声明”,其中状态=“可用”。如果多个线程尝试以这种方式更改状态,除了第一个线程之外的所有线程都会因为脏数据而失败。

请注意,这是一个仅涉及乐观锁定的用例。因此,作为“当您不希望发生很多冲突时使用乐观锁定”的替代方法,它也可以用于您期望发生冲突但只希望一个事务成功的情况。


小智 5

关于乐观锁和悲观锁,上面已经说了很多好话。需要考虑的重要一点如下:

使用乐观锁时,我们需要注意应用程序如何从这些故障中恢复。

特别是在异步消息驱动架构中,这可能导致消息处理无序或更新丢失。

需要仔细考虑故障场景。


小智 5

乐观锁定意味着读取行时不使用排它锁,因此不会防止丢失更新写入倾斜。因此,使用乐观锁定

  • 如果没有发生丢失更新写入倾斜。
  • 或者,即使发生丢失更新写入倾斜也没有问题。

悲观锁定意味着在读取行时使用排它锁,从而防止丢失更新写入倾斜。因此,使用悲观锁定

  • 如果发生丢失更新写入倾斜
  • 或者如果出现更新丢失写入倾斜等问题。

MySQLPostgreSQL中,您可以使用排它SELECT FOR UPDATE

您可以检查我对丢失更新的回答,并 使用乐观锁定(没有SELECT FOR UPDATE悲观锁定(在SELECT FOR UPDATE)MySQL中)编写倾斜示例。