通过具体(Java)示例进行乐观锁定

时间:2014-01-14 17:31:55

标签: java concurrency locking optimistic-locking

我早上都在阅读Google churns up on optimistic locking的所有热门文章,而对于我的生活,我仍然没有真正理解它。

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

有人可以提供一个具体,易于理解的示例,说明如何在Java中使用乐观锁定(可能是MySQL数据库)。假设我们有一个Person实体:

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

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

  • 软件/线程#1试图保持姓氏更改(从“ John Smith ”到“ John Doe ”)
  • 软件/线程#2试图保持对喜欢的颜色的更改(从 RED GREEN

我的问题:

  1. 如何在people和/或colors表上实施乐观锁定? (寻找特定的DDL示例)
  2. 您如何在应用程序/ Java层使用此乐观锁定? (寻找具体的代码示例)
  3. 有人可以让我通过DDL /代码更改(来自上面的#1和#2)将在我的方案(或任何其他方案)中发挥作用并且“乐观地锁定”people的情况/ colors表是否正确?基本上,我希望看到乐观锁定在行动中,并简单地解释它为何起作用。

2 个答案:

答案 0 :(得分:37)

通常,当您考虑乐观锁定时,您还可以使用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. 如果是,请将数据存储为增加一个
  3. 的版本

    要完成这两个步骤而不冒其他进程在两个步骤之间更改数据的风险,通常会通过类似

    的语句来处理
    UPDATE Person SET lastName = 'married', version=2 WHERE person_id = 42 AND version = 1;
    

    执行语句后,检查是否更新了一行。如果你这样做了,那么没有其他人在你阅读之后就改变了数据,否则其他人就改变了数据。如果其他人更改了数据,您通常会收到正在使用的库的OptimisticLockException

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

    所以没有碰撞:

    1. 流程A读取人员
    2. 进程A编写Person,从而递增版本
    3. 流程B读取人
    4. 进程B编写Person,从而递增版本
    5. 碰撞:

      1. 流程A读取人员
      2. 流程B读取人
      3. 进程A编写Person,从而递增版本
      4. 当尝试保存为自“读取人员”以来更改的版本时,进程B收到异常
      5. 如果Color是另一个对象,你应该用相同的方案放置一个版本。

        什么不乐观锁定?

        • 乐观锁定对于合并冲突的更改并不神奇。乐观锁定只会阻止进程意外地覆盖另一个进程的更改。
        • 乐观锁定实际上并不是真正的DB-Lock。它只是通过比较版本列的值来工作。您不会阻止其他进程访问任何数据,因此您希望得到OptimisticLockException s

        什么列类型用作版本?

        如果许多不同的应用程序访问您的数据,您最好使用数据库自​​动更新的列。例如对于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)只会失败。

答案 1 :(得分:3)

@TheConstructor:很好地解释它是什么,不是什么。当你说"乐观锁定并不是合并冲突变化的魔力。我想发表评论。我曾经管理过DataFlex应用程序,它允许用户编辑表单上的记录。当他们推动" Save"按钮,该应用程序将执行所谓的" 多用户重读"数据 - 拉动当前值 - 并与用户修改的内容进行比较。如果用户修改的字段在此期间没有被更改,那么只有那些字段将被写回到记录(仅在重新读取+写入操作期间被锁定),因此,2个用户可以透明地修改不同的字段。同样的记录没有问题。它不需要版本标记,只需要知道修改了哪些字段。

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