用具体(Java)示例解释乐观锁。

39
我花了整个上午阅读了Google搜索出来的关于乐观锁的文章, 但是我还是不太明白。
知道乐观锁涉及添加用于跟踪记录“版本”的列,该列可以是时间戳、计数器或任何其他版本跟踪构造。但我仍然不明白如何确保写入完整性(这意味着如果多个进程同时更新同一实体,则之后,实体应正确反映其应处于的真实状态)。
有人能提供一个在Java中使用乐观锁的具体、易于理解的示例吗?假设我们有一个Person实体,并使用MySQL数据库。
public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private Color favoriteColor;
}

这意味着Person实例将被持久化到一个名为people的MySQL表中:

CREATE TABLE people (
    person_id PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,        # } I realize these column defs are not valid but this is just pseudo-code
    age INT NOT NULL,
    color_id FOREIGN KEY (colors) NOT NULL  # Say we also have a colors table and people has a 1:1 relationship with it
);

现在假设有两个软件系统,或者一个系统上有两个线程,它们正在尝试同时更新同一个“人”实体:
  • 软件/线程#1正在尝试持久化姓氏更改(从“John Smith”到“John Doe”)
  • 软件/线程#2正在尝试持久化最喜欢的颜色更改(从红色到绿色)

我的问题:

  1. 如何在people和/或colors表上实现乐观锁定? (寻找具体的DDL示例)
  2. 然后,您如何在应用程序/Java层中利用此乐观锁定? (寻找特定的代码示例)
  3. 能否向我演示一种情况,其中DDL /代码更改(来自上述#1和#2)将在我的场景(或任何其他场景)中发挥作用,并且会正确地“乐观锁定”people/colors表? 基本上,我想看到乐观锁定的实际效果,并易于理解为什么它有效。

1
你想要锁定什么,是内存中的Java对象还是数据库中的条目? - Philip Frank
2个回答

47
通常在使用乐观锁时,您还需要使用像Hibernate或其他支持@Version的JPA实现库。
例如,可以这样阅读:
public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private Color favoriteColor;
    @Version
    private Long version;
}

显然,如果您没有使用支持此功能的框架,则添加@Version注释是没有意义的。
DDL可能如下:
CREATE TABLE people (
    person_id PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,        # } I realize these column defs are not valid but this is just pseudo-code
    age INT NOT NULL,
    color_id FOREIGN KEY (colors) NOT NULL,  # Say we also have a colors table and people has a 1:1 relationship with it
    version BIGINT NOT NULL
);

版本控制是什么?

  1. 在存储实体之前,您需要检查数据库中存储的版本是否与您已知的版本相同。
  2. 如果相同,则将数据与增加了一的版本一起存储。

为了在两个步骤之间完成这两个步骤而不会冒着其他进程更改数据的风险,通常会通过类似于以下语句来处理:

UPDATE Person SET lastName = 'married', version=2 WHERE person_id = 42 AND version = 1;

执行语句后,您需要检查是否已更新行。如果是,则表示自读取数据以来没有其他人更改数据,否则其他人更改了数据。如果有人更改了数据,则通常会收到您正在使用的库的 OptimisticLockException 异常。
该异常应导致撤销所有更改,并重新启动更改值的过程,因为实体要更新的条件可能不再适用。
因此,无冲突:
1. 进程 A 读取 Person 2. 进程 A 写入 Person,从而增加版本 3. 进程 B 读取 Person 4. 进程 B 写入 Person,从而增加版本
冲突:
1. 进程 A 读取 Person 2. 进程 B 读取 Person 3. 进程 A 写入 Person,从而增加版本 4. 进程 B 尝试保存时收到异常,因为自读取 Person 以来版本已更改
如果 Colour 是另一个对象,则应按相同方案对其进行版本控制。
什么不是乐观锁定?
  • 乐观锁并不是解决合并冲突的魔法,它只是防止进程意外地覆盖另一个进程所做的更改。
  • 乐观锁实际上并不是真正的数据库锁。它仅通过比较版本列的值来工作。您无法阻止其他进程访问任何数据,因此请预期会出现OptimisticLockException异常。

应该使用哪种列类型作为版本?

如果多个不同的应用程序访问您的数据,则最好使用由数据库自动更新的列。例如,对于MySQL

version TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;

这样实现乐观锁的应用程序将会注意到被笨拙应用程序所做的更改。
如果您更新实体的频率高于TIMESTAMP的分辨率或其Java解释,则此方法可能无法检测到某些更改。此外,如果您让Java生成新的TIMESTAMP,则需要确保运行应用程序的所有机器都处于完美的时间同步状态。
如果您的所有应用程序都可以更改整数、长整型等版本,则通常是一个好的解决方案,因为它永远不会受到不同时钟设置的影响 ;-)
还有其他情况。例如,您可以使用哈希或甚至在每次要更改行时随机生成一个String。重要的是,在任何进程持有数据进行本地处理或在缓存中时,不要重复值,因为该进程将无法通过查看版本列来检测更改。
作为最后的手段,您可以使用所有字段的值作为版本。虽然在大多数情况下这将是最昂贵的方法,但这是一种在不更改表结构的情况下获得类似结果的方式。如果您使用Hibernate,则有@OptimisticLocking注释来强制执行此行为。在实体类上使用@OptimisticLocking(type = OptimisticLockType.ALL)以在读取实体后任何行更改时失败,或者使用@OptimisticLocking(type = OptimisticLockType.DIRTY)以仅在另一个进程更改您更改的字段时失败。

@ShubhamKumar 这取决于这100个线程对数据的操作。有时候你可以将数据分成较小的部分,有时候你可以通过制定整个更新的 SQL 来消除显式锁定的需求,例如增加计数器,有时候你可能需要真正的锁定,这样更新就只是一个接一个地执行。乐观锁定在通常只有一个线程会更新每个版本实体时效果最好,否则你可能会让一个线程无限重试。 - TheConstructor
如果所有线程都试图更新相同版本的实体,那该怎么办?为此使用乐观锁定是否是个好主意? - Shubham Kumar
@ShubhamKumar 乐观锁定将确保线程不会覆盖另一个线程的更改。如果一个线程被阻止更新条目,它必须读取当前状态并重新应用修改。但是,由于最多有98个其他线程执行相同的操作,因此它可能再次失败。因此,如果峰值达到100,通常情况下1个线程可能还可以,否则我建议不要这样做。 - TheConstructor
如果有多个表需要更新,使用每个表的乐观锁定并在数据库上启动事务是否足够? - dierre
@dierre 这取决于事务隔离级别以及对象及其更新之间的关系。如果一个进程同时只更新表A,而另一个进程只更新表B,则两者都可以成功。这可能是个问题,也可能不是。您可以定义一个公共版本,在任何这些表格发生更改时进行更新,并在事务中更新此版本。当然,这样做不太乐观(即更长,可能会阻塞事务),但可以有所帮助。 - TheConstructor
显示剩余2条评论

4
@TheConstructor: 很好的解释了什么是乐观锁,以及它不是什么。当你说“乐观锁不是处理冲突更改的魔法”时,我想评论一下。我曾经管理过一个DataFlex应用程序,允许用户在表单上编辑记录。当他们点击“保存”按钮时,应用程序会进行所谓的“多用户重新读取”数据,拉取当前值,并与用户修改的内容进行比较。如果用户修改的字段在此期间没有被更改,则只将这些字段写回记录中(在重新读取+写操作期间仅锁定记录),因此,2个用户可以透明地修改同一记录上的不同字段而没有问题。它不需要版本戳,只需要知道哪些字段已被修改。

当然,这不是完美的解决方案,但在那种情况下它完成了工作。它是乐观的,允许无关的更改,并在冲突更改时向用户显示错误。这是SQL之前最好的解决方案,但今天仍然是一个很好的设计原则,也许适用于更多的对象或Web相关场景。


1
我没有足够的声望在实际问题上进行评论,所以:Person和Color的示例:Person具有实体ID,因此它是“正在修改的内容”,Colors表是相关的,而不是主要的(您将其标记为外键),因此对其进行的添加永远不会发生冲突。显然,即使人改变了颜色,也不能从中删除行?您只能有意义地一次更新一个实体,否则您将进入级联更新等领域。我认为您的模型很紧张。将Color改为Person的唯一属性如何? - no comprende
1
有时候,“多用户重读”是个好主意,有时候则不然。比如说,你有个同事会标记那些邮件退回的用户,以便他们不能下订单。同时,地址可能已经被更改,通常情况下这会导致删除无法送达标记。在这种情况下,最好拒绝第二次编辑,并询问用户修改后的数据是否有效。这可以通过乐观锁定轻松实现。 - TheConstructor
1
这实际上是个好主意,可以在乐观锁定的基础上加入一个版本字段(即如果版本字段未更改,则进行更新,如果已更改,则查看我们是否修改了自最初提取记录以来未修改的字段)。我看到的最大实际挑战是您必须知道先前的值,例如,您正在编辑v4,您要保存并且记录已经处于v6 [在数据库中完成和提交],如何知道哪些字段已更改?解决是可能的,但可能会很棘手。表格可能需要提交旧值->新值。 - Brad Peabody

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接