如何在 MySQL 中正确实现乐观锁

Bes*_*ces 13 mysql locking

如何在 MySQL 中正确实现乐观锁?

我们的团队推断我们必须执行下面的 #4,否则存在另一个线程可以更新记录的相同版本的风险,但我们想验证这是最好的方法。

  1. 在要使用乐观锁定的表上创建一个版本字段,例如 column name = "version"
  2. 在选择时,确保包含版本列并记下版本
  3. 在对记录进行后续更新时,更新语句应发出“where version = X”,其中 X 是我们在 #2 中收到的版本,并将该更新语句期间的版本字段设置为 X + 1
  4. SELECT FOR UPDATE对我们要更新的记录执行 a ,以便我们序列化谁可以对我们尝试更新的记录进行更改。

为了澄清起见,我们试图阻止在同一时间窗口中选择相同记录的两个线程,如果它们同时尝试更新记录,则它们会在其中获取相同版本的记录时相互覆盖。我们相信,除非我们做 #4,否则有可能,如果两个线程同时进入各自的事务(但尚未发布更新),当它们进行更新时,第二个线程将使用 UPDATE ...其中 version = X 将对旧数据进行操作。

即使我们使用版本字段/乐观锁定,我们在更新时也必须执行这种悲观锁定的想法是否正确?

Cra*_*ger 20

你的开发人员错了。你需要要么 SELECT ... FOR UPDATE 行版本,而不是两个。

试试看。打开三个 MySQL 会话(A)(B)(C)指向同一个数据库。

(C)问题:

CREATE TABLE test(
    id integer PRIMARY KEY,
    data varchar(255) not null,
    version integer not null
);
INSERT INTO test(id,data,version) VALUES (1,'fred',0);
BEGIN;
LOCK TABLES test WRITE;
Run Code Online (Sandbox Code Playgroud)

在两个(A)(B)issueUPDATE中都测试并设置行版本,更改winner每个中的文本,以便您可以查看哪个会话是哪个:

-- In (A):

BEGIN;
UPDATE test SET data = 'winnerA',
            version = version + 1
WHERE id = 1 AND version = 0;

-- in (B):

BEGIN;
UPDATE test SET data = 'winnerB',
            version = version + 1
WHERE id = 1 AND version = 0;
Run Code Online (Sandbox Code Playgroud)

现在(C)UNLOCK TABLES;要释放锁。

(A)并且(B)将比赛的行锁。其中一个将获胜并获得锁定。另一个将阻塞在锁上。获得锁定的获胜者将继续更改行。假设(A)是赢家,您现在可以使用SELECT * FROM test WHERE id = 1.

现在COMMIT在获胜者会话中,说(A)

(B)将获得锁定并继续更新。但是,版本不再匹配,因此它不会更改任何行,如行计数结果所报告的那样。只有一个UPDATE有任何影响,客户端应用程序可以清楚地看到哪些UPDATE成功,哪些失败。无需进一步锁定。

请在此处查看 pastebin 中的会话日志。我使用mysql --prompt="A> "etc 来轻松区分会话之间的区别。我复制和粘贴按时间顺序交错的输出,所以它不是完全原始的输出,我可能在复制和粘贴时出错。自己测试看看。


如果您没有添加行版本字段,那么您需要SELECT ... FOR UPDATE能够可靠地确保排序。

如果您考虑一下,如果您立即执行 a而不重新使用来自 的数据,或者您正在使用行版本控制,则 aSELECT ... FOR UPDATE完全多余的。在将采取锁定反正。如果其他人在您的读取和后续写入之间更新了该行,您的版本将不再匹配,因此您的更新将失败。这就是乐观锁的工作原理。UPDATESELECTUPDATE

目的SELECT ... FOR UPDATE是:

  • 管理锁排序以避免死锁;和
  • 当您想要从一行读取数据时,要扩展行锁的跨度,请在应用程序中更改它,并在无需使用SERIALIZABLE隔离或行版本控制的情况下写入基于原始行的新行。

您不需要同时使用乐观锁定(行版本控制)和SELECT ... FOR UPDATE. 使用其中之一。