春季默认@Transactional和默认的丢失更新

时间:2018-03-19 14:58:09

标签: 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);
}

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

3 个答案:

答案 0 :(得分:2)

你不是非常错,你的问题是一个非常有趣的观察。我相信(基于你的评论)你在非常具体的情况下考虑它,而这个主题更广泛。让我们一步一步来。

<强> ACID

ACID中的

I 确实代表了孤立。但这并不意味着需要一个接一个地执行两个或更多个事务。他们只需要被隔离到某种程度。大多数关系数据库允许在事务上设置隔离级别,甚至允许您从其他未提交的事务中读取数据。如果这种情况好或不合适,则由具体应用决定。请参阅mysql文档:

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

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

现在,我们还有NoSQL数据库不支持ACID。最重要的是,如果您开始使用数据库集群,您可能需要拥抱数据的最终一致性,这甚至可能意味着在执行读取时,刚写入某些数据的相同线程可能无法接收它。同样,这是一个非常具体的特定应用程序的问题 - 我能否承受一段时间不一致的数据以换取快速写入?

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

更新失败 - 就是这种情况?

是的,情况确实如此。

我们害怕可序列化吗?

是的,它可能会变得讨厌:-)但重要的是要了解它是如何工作的以及后果是什么。我不知道是否仍然如此,但我在大约10年前在使用DB2的项目中遇到过这种情况。由于非常具体的情况,DB2在整个表上执行锁升级到独占锁,有效阻止任何其他连接访问表甚至读取。这意味着一次只能处理一个连接。

因此,如果您选择使用可序列化级别,则需要确保您的事务实际上很快并且实际上需要它。也许在你写作的时候,其他一些线程正在读取数据呢?想象一下你的文章有一个评论系统的场景。突然,一篇病毒式文章发布,每个人都开始发表评论。用于注释的单个写事务需要100ms。 100个新的评论交易排队,这将有效地阻止阅读未来10年的评论。我确信在这里继续读取是绝对足够的,并允许你实现两件事:更快地存储注释并在编写时读取它们。

长话短说: 这一切都取决于您的数据访问模式,并没有银弹。有时可序列化是必需的,但它有性能损失,有时读取未提交将没有问题,但会带来不一致的处罚。

答案 1 :(得分:1)

丢失数据?

您不会丢失数据。可以把它想象成改变代码中的变量。

int i = 0;
i = 5;
i = 10;

你有没有&#34;失去&#34; 5?好吧,不,你替换了它。

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

从纯粹的更新角度(忘记阅读),它没有什么不同。数据库将使用锁来序列化更新,因此一个仍将在另一个之前。第二个自然而然地获胜。

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

根据当前状态更新

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

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

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

解决它

有两种解决方案。

  1. 可序列化隔离级别 - 在大多数隔离级别中,读取(select s)不保留任何排它锁,因此不会阻塞,无论它们是否在事务中。 Serializable实际上会为每个读取的行获取并保持一个独占锁,并且只在事务提交或回滚时释放这些锁。
  2. 在单个语句中执行更新。 - 单个UPDATE语句应该为我们提供原子,即UPDATE entity SET number_of_updates = number_of_updates + 1 WHERE entity_id = ?
  3. 一般来说,后者更具可扩展性。您持有的锁越多,持有的时间越长,获得的阻塞就越多,因此吞吐量就越小。

答案 2 :(得分:0)

要补充说明,使用@Transactional和“丢失更新”的情况并没有错,但是,这似乎令人困惑,因为它不符合我们对@Transactional可以防止“丢失的更新”。

READ_COMMITED隔离级别可能会发生“丢失更新”问题,这也是大多数DB和JPA提供程序的默认级别。

为防止这种情况,需要使用@Transactional(isolation = isolation.REPEATABLE_READ)。不需要SERIALIZABLE,那会导致过度杀伤。

著名的JPA冠军弗拉德·米哈尔恰(Vlad Mihalcea)在他的文章中给出了很好的解释:https://vladmihalcea.com/a-beginners-guide-to-database-locking-and-the-lost-update-phenomena/

值得一提的是,更好的解决方案是使用@Version,它也可以使用乐观锁定方法来防止更新丢失。

该问题可能来自维基页面https://en.wikipedia.org/wiki/Isolation_(database_systems),其中显示“丢失更新”比“脏读”是一个“较弱”的问题,并且从来没有这样的情况,但是,以下文字矛盾: >

enter image description here