“选择更新”并使用悲观锁定进行更新

ABC*_*ABC 1 java spring jdbc pessimistic-locking

我正在尝试使用 select for update 来实现悲观锁定,因为我希望其他线程等待,直到释放所选行上的锁。我所理解的部分是在经历了多个线程Spring JDBC select for update和各种类似的线程之后,如果 select 和 update 发生在同一方法中,因此它们是同一事务的一部分,那么这是可以实现的。

在我的例子中,问题是我有一个用于 DAO 功能的 JAR,其中有一个selectforUpdate方法可用,并且有一个单独的更新方法可用,这两种方法都有一个 finally 块,其中包含

resultSet.close();
statement.close();
connection.close();
Run Code Online (Sandbox Code Playgroud)

现在我正在努力找出是否有一种方法可以从 JAR 外部使用这两种方法,也许可以通过用@Transactional注释来注释我的方法并使其以某种方式工作。因此只有在执行 update 方法后才会释放该锁。

rzw*_*oot 13

你犯了一个错误。使用错误的工具来完成工作。事务级别和FOR UPDATE的目的是保证数据完整性。时期。它不是为控制流程而设计的,如果你用它来控制流程,它迟早会咬你的屁股。

\n

让我试着解释一下SELECT FOR UPDATE它的用途,这样当我稍后告诉你它绝对不是为了你想要用它做的事情时,你就更容易理解。

\n

想象一下一家银行。够简单的。银行前面有一些自动柜员机和一个网站,您可以在其中查看交易并将资金转入其他帐户。

\n

想象一下,你(ABC)和我(雷尼尔)正试图从银行中诈骗一些。这是我们的计划:我们将其设置为您的帐户中有 \xe2\x82\xac1000,- 而我什么都没有。

\n

然后,您从手机登录该网站,并开始转账,将 \xe2\x82\xac1000,- 转账到我的帐户。但是,当您这样做时,就在中间,您从 ATM 中提取了 \xe2\x82\xac10,- 。

\n

如果银行搞砸了他们的交易,你的帐户中可能会出现 \xe2\x82\xac990,- ,而我的帐户中可能会出现 \xe2\x82\xac1000,- ,我们就诈骗了银行。这就是发生这种情况的方式(如果在示例中途您认为:我已经知道这些东西,我知道 FOR UPDATE 的作用! - 我不太确定您知道,请仔细阅读)

\n

自动柜员机代码

\n
startTransaction();\nint currentBalance = sql("SELECT balance FROM account WHERE user = ?", abc);\nif (currentBalance < requestedWithdrawal) throw new InsufficientFundsEx();\nsql("UPDATE account SET balance = ? WHERE user = ?", currentBalance - requestedWithdrawal, abc);\ncommit();\nmoneyHopper.spitOut(requestedWithdrawal);\n
Run Code Online (Sandbox Code Playgroud)\n

网站代码

\n
startTransaction();\nint balanceTo = sql("SELECT balance FROM account WHERE user = ?", reinier);\nint balanceFrom = sql("SELECT balance FROM account WHERE user = ?", abc);\nif (transfer > balanceFrom) throw new InsufficientFundsEx();\nsql("UPDATE account SET balance = ? WHERE user = ?", balanceTo + transfer, reinier);\nsql("UPDATE account SET balance = ? WHERE user = ?", balanceFrom - transfer, abc);\ncommit();\ncontroller.notifyTransferSucceeded();\n
Run Code Online (Sandbox Code Playgroud)\n

怎么会出错

\n

出错的方式是,如果 和balanceTobalanceFrom“锁定”,那么ATM 取款就会通过,然后来自网站事务的更新 SQL 语句就会通过(这实际上消除了 ATM 取款 - 无论 ATM 吐出的是什么自由钱),或者如果 ATM 的余额检查锁定,转账将完成,然后 ATM 的更新将完成(这为收件人(即我)提供了他们的 \xe2\x82\xac1000,-,并确保 ATM 代码的更新,将你的余额设置为 990,是最后发生的事情,给我们 \xe2\x82\xac990,- 免费资金。

\n

那么解决办法是什么?提示:不适合更新

\n

解决办法是考虑事务的含义。事务的目的是将操作变成原子概念。要么你的账户减少了转账金额,而我的账户则增加了相同的金额,或者什么也没有发生。

\n

对于改变事物的语句(UPDATE 和 INSERT)来说,这是很明显的。当我们谈论读取数据时,这有点奇怪。这些读取是否应该被视为事务的一部分?

\n

一种方法是说:不,除非您FOR UPDATE在所有内容的末尾添加,在这种情况下,是 - 即仅在FOR UPDATE事务结束之前应用锁定这些行。

\n

但这并不是确保数据完整性的唯一方法

\n

乐观锁定可以拯救你——或者更确切地说,你的灭亡

\n

一种更常见的方法称为 MVCC(多版本并发控制),并且速度快得多。MVCC(也称为乐观锁定)背后的想法是假设不会发生冲突。没有什么是锁定的。相反,[A] 在提交之前,事务中所做的所有更改对于任何其他事务中运行的内容都是完全不可见的,并且 [B] 当您执行COMMIT事务时,数据库会检查您在此事务范围内所做的所有操作是否仍然有效保持' - 例如,如果您在此事务中更新了一行,而该行也被已提交的另一个事务修改,则您在提交时会收到错误,而不是在运行 UPDATE 语句时收到错误。

\n

在这个框架中,我们仍然可以讨论 SELECT 的含义。这在 java/JDBC 中称为事务隔离级别,并且可以在数据库连接上进行配置。银行应该使用以避免此问题的最佳级别称为TransactionLevel.SERIALIZABLE。可序列化实际上意味着一切都会弄脏其他一切:如果在事务期间您读取了一些数据,并且当您提交时,同一个 SELECT 语句将产生不同的结果,因为其他事务修改了某些内容,那么 COMMIT 就会失败。

\n

它们因所谓的“RetryException”而失败。这实际上就是它所说的:只需从顶部开始您的交易。如果您考虑一下那个银行的例子,这是有道理的:如果银行做得正确并设置了可序列化事务隔离级别,将会发生什么,ATM 机的事务或转账事务都会遇到重试异常。假设银行正确编写了代码,并且他们实际上执行了异常告诉您的操作(重新开始),那么他们将重新开始,其中包括重新读取余额。现在不可能发生银行欺诈的情况。

\n

至关重要的是,在该SERIALIZABLE模型中,锁定永远不会发生,并且FOR UPDATE根本没有任何意义

\n

因此,通常,FOR UPDATE 不会执行任何操作,完全无操作,具体取决于数据库的设置方式。

\n

FOR UPDATE并不意味着“锁定接触此行的其他事务”。不管你多么想要它。

\n

某些数据库实现,甚至数据库引擎和连接配置的某种组合可能会以这种方式实现,但这是一个非常挑剔的设置,您的应用程序应包含强烈建议操作员永远不要更改数据库设置,永远不要切换数据库的文档引擎,永远不要更新数据库引擎,永远不要更新 JDBC 驱动程序,永远不要弄乱连接设置。

\n

这是一种你真的、真的不想在你的代码中添加的愚蠢警告。

\n

解决方案是停止用电锯在吐司上涂黄油。即使你认为你可以设法用它在吐司上加一些黄油,但这根本不是它的用途,就像根本一样,我们都只是在等待,直到你在这里失去拇指。别再这样做了。拿把黄油刀。

\n

如果您想让一个线程等待另一个线程,请不要使用数据库,而应使用锁对象。如果你想让一个进程等待另一个进程,不要使用数据库,不要使用锁对象(你不能;进程不共享内存);使用一个文件。新的java文件IO有一个选项可以原子地创建一个文件(这意味着,如果文件已经存在,则抛出异常,否则创建文件,并以原子方式执行此操作,这意味着如果两个进程都运行此“原子地创建新文件”代码,你可以保证一个成功,一个抛出)。

\n

如果您想要数据完整性,并且这是您想要悲观锁定的唯一原因,请停止这种想法 - 保证数据完整性是数据库的工作,而不是您的工作。MVCC/乐观锁定数据库保证,无论您如何努力尝试这个答案顶部的恶作剧,银行都不会被骗,尽管如此,悲观锁定并不涉及。

\n

对于“最终用途”,就像您在这里所做的那样,JDBC 本身很糟糕(有意地,有点太多了,难以深入)。给自己找一个让它变得更好的抽象,比如 JDBI 或 JOOQ。这些工具还具有与数据库交互的唯一正确方法,即所有数据库代码都必须位于 lambda 中。那是因为您不想手动处理这些重试异常,您希望数据库访问框架来处理它。银行代码实际上应该是这样的:

\n
dbAccess.run(db -> {\n    int balance = db.sql("SELECT balance FROM account WHERE user =?", abc);\n    if (balance < requested) throw new InsufficientBalanceEx();\n    db.update("UPDATE account SET balance = ? WHERE user = ?", balance - requested, abc);\n    return requested;\n};\n
Run Code Online (Sandbox Code Playgroud)\n

这样,“框架”(该run方法背后的代码)就可以捕获 retryex,并根据需要经常重新运行 lambda。重新运行很棘手 - 如果服务器上的两个线程都导致另一个线程重试(这并不难做到),那么您可能会陷入无限循环,它们都重新启动并再次导致另一个线程重试,无限。解决方案实际上就是掷骰子。重试时,您应该滚动一个随机数并等待那么多毫秒,并且每次进一步重试,滚动的范围都应该增加。如果这对您来说听起来很愚蠢,请知道您当前正在使用它:这也是以太网的工作原理(当线路上发生冲突时,以太网使用随机退避)。以太网获胜,令牌环失败。这在工作中是完全相同的原理(令牌环是悲观锁定,以太网是乐观锁定,呃,只需尝试一下并检测它是否出错,然后重做它,并撒上一些随机指数退避以确保您不会得到两个系统步调一致,永远搞砸对方的尝试)。

\n

  • @rzwitserloot 我从未声称 MVCC 与可序列化隔离级别是同一回事。我的意思是,虽然乐观锁定可能会在某些情况下提高性能,但它会带来必须重新运行整个关键部分的风险。所以,你不能真正声称它普遍更有效 (3认同)
  • @zwitserloot '如果你想让一个线程等待另一个线程,不要使用数据库,而是使用锁对象。' 如果您已经使用数据库,请使用数据库进行同步。不要在混合物中添加更多工具。 (2认同)