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

Adj*_*ion 29 java concurrency locking optimistic-locking

我早上花了很多时间阅读谷歌在乐观锁定方面所做的所有热门文章,而对于我的生活,我仍然没有真正理解.

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

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

public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private Color favoriteColor;
}
Run Code Online (Sandbox Code Playgroud)

并且这些Person实例会持久化到peopleMySQL表:

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
);
Run Code Online (Sandbox Code Playgroud)

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

  • 软件/线程#1试图保持姓氏改变(从" John Smith "到" John Doe ")
  • 软件/线程#2试图保持最喜欢颜色的变化(从REDGREEN)

我的问题:

  1. 如何在people和/或colors表上实现乐观锁定?(寻找特定的DDL示例)
  2. 那你怎么能在应用程序/ Java层使用这种乐观锁?(寻找具体的代码示例)
  3. 有人可以让我通过DDL /代码更改(来自上面的#1和#2)在我的场景(或任何其他场景)中发挥作用的场景,并且会" 正确地"锁定" people/ colorstable"吗?基本上,我希望看到乐观的锁定动作,并简单地解释它为何起作用.

The*_*tor 41

通常,当您研究乐观锁定时,您还可以使用像Hibernate这样的库或其他@Version支持JPA的实现.

示例可以如下所示:

public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private Color favoriteColor;
    @Version
    private Long version;
}
Run Code Online (Sandbox Code Playgroud)

显然,@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
);
Run Code Online (Sandbox Code Playgroud)

该版本会发生什么?

  1. 每次存储实体之前,都要检查存储在数据库中的版本是否仍然是您知道的版本.
  2. 如果是,则将数据存储为增加1的版本

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

UPDATE Person SET lastName = 'married', version=2 WHERE person_id = 42 AND version = 1;
Run Code Online (Sandbox Code Playgroud)

执行语句后,检查是否更新了一行.如果你这样做了,那么没有其他人在你阅读之后就改变了数据,否则其他人就改变了数据.如果其他人更改了数据,您通常会收到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以来版本已更改

如果Color是另一个对象,你应该在同一个方案中放置一个版本.

什么不乐观锁定?

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

什么列类型用作版本?

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

version TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

作为最后的手段,您可以使用所有字段的值作为版本.虽然这在大多数情况下是最昂贵的方法,但它是一种在不改变表结构的情况下获得类似结果的方法.如果您使用Hibernate,则会有@OptimisticLocking-annotation来强制执行此行为.使用@OptimisticLocking(type = OptimisticLockType.ALL)的实体类,如果任何行更改,因为您已经阅读实体或失效@OptimisticLocking(type = OptimisticLockType.DIRTY)时,另一个进程改变了你改变的字段只失败了.