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


13

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

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

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

为了澄清起见,我们试图防止两个线程在同一时间窗口中选择同一条记录(如果它们试图同时更新记录)在它们捕获相同版本的记录时彼此覆盖,因此它们将无法覆盖彼此。我们认为,除非我们执行#4,否则如果两个线程同时输入各自的事务(但尚未发布其更新),则有机会在它们进行更新时,将使用UPDATE的第二个线程...其中version = X将对旧数据进行操作。

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


有什么问题?使用UPDATE增加版本号,然后第二个UPDATE将失败,因为版本号与读取时的版本号不同-这正是您想要的。
AndreKR 2012年

你确定吗?不清楚的是,除非将事务隔离级别设置为特定设置,否则其他线程实际上将更新。如果您都同时输入事务,则第二个线程在进行更新时会很好地看到OLD数据。MySQL在ACID领域不像Oracle那样健壮,因此正在寻找在MySQL中实现乐观锁定的最佳实践方法,以防止脏读/更新。
BestPractices 2012年

但是然后在提交过程中事务总会失败,对吗?
AndreKR 2012年

有迹象表明,人会愿意做一个选择的更新,以应对这种情况:dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html
BestPractices

@BestPractices您需要任何 SELECT ... FOR UPDATE或乐观锁定由行版本,而不是两个。详见答案。
克雷格·林格

Answers:


17

您的开发人员弄错了。您需要一个 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;

在这两个版本中(A)(B)发布一个UPDATE测试和设置行版本的,并更改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;

现在在中(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 SELECT ... FOR UPDATE完全多余的。无论如何,它将被锁定。如果其他人更新了读取和后续写入之间的行,则您的版本将不再匹配,因此更新将失败。这就是乐观锁定的工作方式。UPDATESELECTUPDATE

目的SELECT ... FOR UPDATE是:

  • 管理锁顺序以避免死锁;和
  • 要扩展行锁的范围,以便在您要从行中读取数据时,可以在应用程序中进行更改,然后基于原始行写新行,而不必使用SERIALIZABLE隔离或行版本控制。

您无需同时使用乐观锁定(行版本控制)和SELECT ... FOR UPDATE。使用一个或另一个。


谢谢克雷格。您是正确的-开发人员弄错了。感谢您运行此测试。
BestPractices 2012年

SQL服务器呢?是否始终独立于事务隔离级别而在更新的行上始终获得锁?
plalx 2014年

@plalx嗯,文档怎么说?如果您像这样进行交互式测试会怎样?
克雷格·林格2014年

@CraigRinger,如果B在A提交之前但在A更新之后获得了锁,将会发生什么?
MengT

1
@MengT不能,这就是为什么它是锁的。
Craig Ringer

0
UPDATE tbl SET owner = $me,
               id = LAST_INSERT_ID(id)
    WHERE owner = ''
    LIMIT 1;
$id = SELECT LAST_INSERT_ID();
Do some stuff (arbitrarily long time)...;
UPDATE  tbl SET owner = '' WHERE id = $id;

不需要甚至不需要锁(不是表,不是事务):

  • UPDATE是原子的
  • LAST_INSERT_ID()是特定于会话的,因此是线程安全的。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.