【问题标题】:Default @Transactional in spring and the default lost update春季默认@Transactional和默认丢失更新
【发布时间】:2018-03-19 14:58:08
【问题描述】:

spring 环境有一个大现象 不然我大错特错了。 但是默认的spring @Transactional 注释不是ACID,而只是缺乏隔离的ACD。这意味着如果你有方法:

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

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

【问题讨论】:

  • 这取决于数据库和数据库的默认事务行为。使用REPEATABLE_READSERIALIZED 会起作用,但这会对性能产生很大影响。这也是为什么 JPA 提供程序(如 Hibernate)具有乐观锁定功能的原因(基本上是更新中包含的 versiontimestamp 列)。当 2 个线程执行更新时,1 会因为版本号不匹配而失败。
  • saveEntity 是不必要的。如果TheEntity 是一个托管实体(因为它应该是,考虑到该方法是事务性的),对它所做的任何更改都将在事务结束时刷新到数据库中。正如@M.Deinum 所说,乐观锁定机制应该可以防止任何丢失的更新。
  • yeye 我知道持久分离等状态我只是为了可读性而添加它,但这与问题无关。问题是......为什么 sprint @Transactional 的行为不像每个人所期望的那样 - ACID。我周围所有使用它多年的开发人员都不敢相信它是这样工作的,但确实如此。丢失的更新正在发生。不是吗?
  • 一种避免的方法是隔离 = ISOLATION.SERIALIZABLE,但它会抛出异常,所以它不是那么直接,另一种是乐观锁定,但也需要一些更改。
  • 您不会丢失数据。你有两个更新。您的实体不能处于两种状态。我认为您真正想知道的是并发是如何工作的。

标签: java spring hibernate jpa


【解决方案1】:

丢失数据?

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

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

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

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

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

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

根据当前状态更新

如果更新是基于当前状态的条件呢?

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

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

解决它

有两种解决方案。

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

一般来说,后者的可扩展性要高得多。你持有的锁越多,持有的时间越长,你得到的阻塞就越多,因此吞吐量就越低。

【讨论】:

    【解决方案2】:

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

    ACID 中的

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

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

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

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

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

    更新丢失 - 是这样吗?

    是的,就是这样。

    我们害怕可序列化吗?

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

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

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

    【讨论】:

      【解决方案3】:

      补充一下上面的 cmets,@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,它还可以通过乐观锁定方法防止丢失更新。

      问题可能来自 wiki 页面https://en.wikipedia.org/wiki/Isolation_(database_systems),其中显示“丢失更新”是比“脏读”“更弱”的问题,而且从来不是这样,但是,下面的文字矛盾:

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-03-19
        • 1970-01-01
        • 2010-12-16
        • 2016-04-02
        • 2012-11-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多