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

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

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

有人可以提供一个具体的,易于理解的例子,说明如何在Java中使用乐观锁定(可能是MySQL DB)。 假设我们有一个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个线程,它们试图同时更新同一个Person实体:

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

我的问题:

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

通常,当您研究乐观锁定时,您还可以使用像Hibernate这样的库或其他具有@Version支持的JPA-Implementation。

示例可以如下所示:

 public class Person { private String firstName; private String lastName; private int age; private Color favoriteColor; @Version private Long version; } 

显然,如果您没有使用支持此function的框架,则无需添加@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

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

所以没有碰撞:

  1. 进程A读取Person
  2. 进程A编写Person,从而增加版本
  3. 进程B读取Person
  4. 进程B编写Person,从而增加版本

碰撞:

  1. 进程A读取Person
  2. 进程B读取Person
  3. 进程A编写Person,从而增加版本
  4. 尝试保存时,进程B收到exception,因为自读取Person以来版本已更改

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

什么不乐观锁定?

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

什么列类型用作版本?

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

 version TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; 

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

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

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

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

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

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

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