通过具体 (Java) 示例进行乐观锁定该版本会发生什么情况?什么是乐观锁定?要将什么列类型用作版本?

我花了一上午的时间阅读了谷歌在乐观锁定方面搅动的所有顶级文章,对于我的生活,我仍然没有真正理解它。

我知道乐观锁定涉及添加一列来跟踪记录的“版本”,并且此列可以是时间戳,计数器或任何其他版本跟踪构造。但我仍然不明白这如何确保写入完整性(这意味着如果多个进程同时更新同一实体,那么之后,实体会正确反映它应该处于的真实状态)。

有人可以提供一个具体的,易于理解的例子,说明如何在Java中使用乐观锁定(也许,反对MySQL DB)。假设我们有一个实体:Person

public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private Color favoriteColor;
}

并且实例将持久化到MySQL表中:Personpeople

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
);

现在,假设有 2 个软件系统,或者 1 个带有 2 个线程的系统,它们尝试同时更新同一实体:Person

  • 软件/线程 #1 正在尝试保留姓氏更改(从“John Smith”到“John Doe”")
  • 软件/线程 #2 正在尝试保留最喜爱的颜色(从红色绿色)的更改)

我的问题:

  1. 如何在和/或表上实现乐观锁定?(查找特定的 DDL 示例)peoplecolors
  2. 那么,如何在应用程序/Java层利用这种乐观锁定呢?(查找特定代码示例)
  3. 有人可以让我经历一个场景,其中DDL /代码更改(从上面的#1和#2开始)将在我的场景(或任何其他场景)中发挥作用,并会正确“乐观地锁定”/表?基本上,我希望看到乐观的锁定在行动中,并易于理解地解释为什么它有效。peoplecolors

答案 1

通常,当您查看乐观锁定时,您还会使用像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. 如果是,则以 1 递增的版本存储数据

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

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

执行语句后,检查是否更新了行。如果您这样做了,则由于您已读取数据,因此没有其他人更改数据,否则其他人会更改数据。如果其他人更改了数据,您通常会通过您正在使用的库收到 OptimisticLockException

此异常应导致所有更改被撤销,并且更改值的过程将重新启动,因为要更新实体的条件可能不再适用。

所以没有碰撞:

  1. 进程 A 读取人员
  2. 进程 A 写入人员从而增加版本
  3. 进程 B 读取人员
  4. 进程 B 写入人员从而增加版本

碰撞:

  1. 进程 A 读取人员
  2. 进程 B 读取人员
  3. 进程 A 写入人员从而增加版本
  4. 进程 B 在尝试保存为自读取人员以来更改的版本时收到异常

如果Color是另一个对象,你应该按照相同的方案在那里放一个版本。

什么是乐观锁定?

  • 乐观锁定不是合并冲突更改的魔法。乐观锁定只会防止进程意外地被另一个进程覆盖更改。
  • 乐观锁定实际上不是真正的DB锁定。它仅通过比较版本列的值来工作。您不会阻止其他进程访问任何数据,因此期望您获得OptimisticLockException

要将什么列类型用作版本?

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

version TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;

这样,实现乐观锁定的应用程序将注意到哑应用程序的变化。

如果更新实体的频率高于实体的解析或 Java 解释,则此方法可能无法检测到某些更改。此外,如果您让Java生成新的,则需要确保运行应用程序的所有机器都处于完美的时间同步状态。TIMESTAMPTIMESTAMP

如果您的所有应用程序都可以更改整数,长整型,...版本通常是一个很好的解决方案,因为它永远不会遭受不同设置的时钟的影响;-)

还有其他方案。例如,您可以使用哈希值,甚至可以在每次更改行时随机生成一个。重要的是,当任何进程保存数据以进行本地处理或在缓存中时,您都不要重复值,因为该进程将无法通过查看版本列来检测更改。String

作为最后的手段,您可以使用所有字段的值作为版本。虽然在大多数情况下,这将是最昂贵的方法,但它是在不更改表结构的情况下获得类似结果的一种方法。如果使用 Hibernate,则有@OptimisticLocking注释来强制实施此行为。在实体类上使用,以便在任何行自您读取实体后发生更改时失败,或者当另一个进程也更改了您更改的字段时失败。@OptimisticLocking(type = OptimisticLockType.ALL)@OptimisticLocking(type = OptimisticLockType.DIRTY)


答案 2

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

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


推荐