spring中默认@Transactional以及默认丢失更新

saf*_*rJo 8 java spring hibernate jpa

春天的环境中有一个很大的现象,或者我就大错特错了。但默认的 spring @Transactional 注解不是 ACID,而是缺乏隔离性的 ACD。这意味着如果您有以下方法:

@Transactional
public TheEntity updateEntity(TheEntity ent){
  TheEntity storedEntity = loadEntity(ent.getId());
  storedEntity.setData(ent.getData);
  return saveEntity(storedEntity);
}
Run Code Online (Sandbox Code Playgroud)

如果 2 个线程以不同的计划更新进入,会发生什么情况。它们都从数据库加载实体,它们都应用自己的更改,然后保存并提交第一个,当保存并提交第二个时,第一个更新丢失。真的是这样吗?使用调试器就可以这样工作。

Bra*_*don 5

丢失数据?

您不会丢失数据。可以将其视为更改代码中的变量。

int i = 0;
i = 5;
i = 10;
Run Code Online (Sandbox Code Playgroud)

你“失去”了5吗?好吧,不,你替换了它。

现在,您提到的多线程的棘手部分是,如果这两个 SQL 更新同时发生怎么办?

从纯粹的更新角度来看(忘记读取),这没有什么不同。数据库将使用锁来序列化更新,因此一个更新仍然会先于另一个更新。第二个自然获胜。

但是,这里有一个危险......

根据当前状态更新

如果更新是基于当前状态的条件怎么办?

public void updateEntity(UUID entityId) {
    Entity blah = getCurrentState(entityId);
    blah.setNumberOfUpdates(blah.getNumberOfUpdates() + 1);
    blah.save();
}
Run Code Online (Sandbox Code Playgroud)

现在你遇到了数据丢失的问题,因为如果两个并发线程执行read( getCurrentState),它们将各自相加1,得到相同的数字,并且第二次更新将丢失前一个更新的增量。

解决它

有两种解决方案。

  1. 可串行化隔离级别 - 在大多数隔离级别中,读取select不持有任何排他锁,因此不会阻塞,无论它们是否在事务中。Serialized 实际上将为读取的每一行获取并持有一个排它锁,并且仅在事务提交或回滚时释放这些锁。
  2. 在单个语句中执行更新。- 一条UPDATE语句应该使这个对我们来说是原子的,即UPDATE entity SET number_of_updates = number_of_updates + 1 WHERE entity_id = ?

一般来说,后者更具可扩展性。持有的锁越多,持有的时间越长,阻塞就越多,吞吐量就越低。


Mat*_*ski 2

你并没有大错特错,你的问题是一个非常有趣的观察。我相信(根据您的评论)您是在非常具体的情况下考虑这个问题的,而这个主题要广泛得多。让我们一步一步来。

ACID 中的I确实代表隔离。但这并不意味着两个或多个交易需要相继执行。他们只需要在某种程度上被隔离。大多数关系数据库允许在事务上设置隔离级别,甚至允许您从其他未提交的事务中读取数据。这种情况好不好,要看具体应用。请参阅 mysql 文档示例:

https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html

您当然可以将隔离级别设置为可序列化并达到您的期望。

现在,我们也有不支持ACID的NoSQL数据库。最重要的是,如果您开始使用数据库集群,您可能需要接受数据的最终一致性,这甚至可能意味着刚刚写入某些数据的同一线程在读取时可能不会收到它。这又是一个特定于特定应用程序的问题 - 我是否可以承受暂时使用不一致的数据来换取快速写入?

您可能会倾向于在银行或某些金融系统中以可序列化的方式处理一致的数据,并且您可能会接受社交应用程序中不太一致的数据,但可以获得更高的性能。

更新丢失 - 是这样吗?

是的,情况就是这样。

我们害怕序列化吗?

是的,它可能会变得令人讨厌:-)但是了解它是如何工作的以及会产生什么后果很重要。我不知道情况是否仍然如此,但大约 10 年前我在一个使用 DB2 的项目中遇到过这种情况。由于非常特殊的场景,DB2 对整个表执行锁升级到排它锁,从而有效地阻止任何其他连接访问该表(即使是读取)。这意味着一次只能处理一个连接。

因此,如果您选择使用可序列化级别,您需要确保您的事务实际上很快并且确实是需要的。也许在您写入时其他线程正在读取数据是可以的?想象一下这样一个场景:您的文章有一个评论系统。突然,一篇病毒式的文章被发表,每个人都开始发表评论。单个评论写入事务需要 100 毫秒。100 个新评论事务排队,这将有效地阻止接下来 10 秒内的评论读取。我确信在这里使用已提交的读绝对足够了,并且可以让您实现两件事:更快地存储注释并在编写注释时读取它们。

长话短说:这完全取决于您的数据访问模式,并且没有灵丹妙药。有时需要可序列化,但它会带来性能损失,有时未提交读取也可以,但会带来不一致的损失。