有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

处理过时域对象引起的并发问题的java策略(Grails/GORM/Hibernate)

我喜欢将这个问题称为“可重复查找器”问题,因为它在某种意义上与“不可重复读取”相反。由于hibernate重用连接到其会话的对象,因此查找器的结果可能包括一些旧版本的对象,这些对象现在已经过时

从技术上讲,这个问题是Hibernate设计的问题,但由于Hibernate会话在Grails中是隐式的,而且Grails域对象是长期存在的(HTTP请求对我来说很长),所以我决定在Grails/GORM的上下文中问这个问题

我想问这里的专家,是否有任何共同制定的策略来处理这个问题

考虑如下:

    class BankAccount {
      String name
      Float amount

      static constraints = {
        name unique: true
      }
    }

和“组件A”代码:

    BankAccount.findByName('a1')

'组件B代码:

    def result = BankAccount.findAll()

假设componentA先执行,然后是其他一些逻辑,然后是componentB,组件B的结果由视图呈现。组件A和组件B不想彼此了解太多

这样一来,componentB结果包含旧版本的BankAccount“a1”

可能会发生很多非常尴尬的事情。例如,如果同时修改了银行账户,则显示的列表可以包含两个名为“a1”的项目(用户看起来没有唯一性!)或者,账户之间的资金转账可以显示为部分应用的交易(如果资金从a2转账到a1,则会显示从a2中扣除,但a1尚未扣除)。 这些问题令人尴尬,可能会降低用户对应用程序的信心

新增于2014年9月24日:以下是一个令人大开眼界的例子,这个断言可能会失败:

  BankAccount.findAllByName('a1').every{ it.name == 'a1' }

这种情况的例子可以在任何链接的JIRA门票或我的博客中找到。 )

新增于2014年9月24日:注:在实现equals()方法时使用数据库强制的唯一键似乎是一个合理的建议,但它并不保证并发安全。您可能会得到两个具有相同“业务密钥”值且不同的对象。)

可能的解决方案似乎是添加大量discard()调用或大量withNewSession()调用,并处理LazyinitializationExeption和DuplicateKeyException等问题。
但如果我这么做了,为什么我要使用hibernate/GORM?对每个查询返回的每个对象调用refresh似乎很荒谬

我目前的想法是,在某些关键领域使用短会话/withNewSession是最好的方法,但它并不能解决所有情况下的问题,只是解决了一些关键应用领域的问题

这是Grails应用程序必须接受的吗? 你能给我指一下关于这个问题的任何文件/讨论吗

2014年9月24日编辑: 相关Grails JIRA票:https://jira.grails.org/browse/GRAILS-11645, Hibernate JIRA:https://hibernate.atlassian.net/browse/HHH-9367(不幸被拒绝), 我的博客有更详细的例子:http://rpeszek.blogspot.com/2014/08/i-dont-like-hibernategrails-part-2.html

新增2014年10月17日:我收到了几封回复,称这是任何DB应用程序/任何ORM问题。这是不对的

的确,通过使用长事务(Hibernate会话长度/HTTP请求长度)+设置高于可重复读取的正常DB隔离级别,可以避免这个问题。这种解决方案是完全不可接受的(如果要让应用程序正常工作,我们需要HTTP请求长事务,为什么我们有跨国服务呢!)

DB应用程序和其他ORM不会出现这个问题。它们不需要长时间的事务来工作,问题是使用just READ COMMITTED来防止

我在这里发布这个问题已经两个月了,还没有得到有意义的回答。这仅仅是因为这个问题没有答案。这是Hibernate可以修复的,而不是Grails应用程序可以修复的新增2014年10月17日至2014年底


共 (4) 个答案

  1. # 1 楼答案

    就我所能理解的问题而言,问题归结为数据库事务隔离不足

    我还建议,这个问题可能存在于任何应用程序中,以及任何数据库访问框架中

    在数据库事务中,您必须假设自己是数据库的唯一访问者,并且对该事务中的数据库具有一致的视图

    提交后,您可能会发现状态发生了与您所做的更改不兼容的更改,您的事务将回滚

    如果您只进行了只读访问,那么您仍然必须在事务范围内保持一致性,并且数据库保护您免受其他并发修改

    Hibernate的二级缓存跨越事务,因此在同时进行修改时,应该清除该缓存,而且在任何情况下,数据库都可以被其他应用程序修改,因此应该谨慎使用二级缓存

    但是你说二级缓存已经不是你的问题了。我同意。您的问题听起来像是数据库中事务隔离程度不足。这个问题可以解决吗

  2. # 2 楼答案

    下面是我自己回答这个问题的尝试

    添加于2014年9月24日对于这个问题根本没有好的解决方案。遗憾的是,Hibernate拒绝了HHH-9367 JIRA票证,认为它“不是一个bug”。该票证中建议的唯一解决方案是使用刷新(我认为这需要将所有查询更改为如下内容):

    BankAccount.findAllBy...(...).each{ it.refresh() }
    

    就我个人而言,我不同意这是一个有意义的解决方案。)

    如上所述,如果Hibernate/GORM查询返回一组DomainObjects和其中一些 对象已经在hibernate会话中(由以前的查询填充)。查询将返回这些旧对象,并且这些对象不会自动刷新。这可能会导致一些难以发现的并发问题。我称之为可重复查找问题

    这与二级缓存无关。这个问题是由hibernate在没有配置二级缓存的情况下的工作方式引起的。(2014年9月24日编辑:而且,这不是任何ORM、任何DB应用程序问题,该问题专门针对Hibernate的使用)

    对应用程序的影响:

    我只能解释我所知道的影响,我并不是说这些是唯一的影响

    域对象通常有一组关联的约束/逻辑规则,这些约束/逻辑规则通常需要跨多个记录保存,并由应用程序或数据库本身强制执行。我将借用FP和testing的一个术语,并将其称为“属性”

    示例属性: 在上面的BankAccount示例中,名称唯一性(由DB强制)是一个属性(例如,您可以在定义equals()方法时使用它),如果在帐户之间转账, 这些账户中的总金额必须是一个常数——这是一笔财产
    如果我修改了我的BankAccount类并将“branch”关联添加到其中:

    BankBranch branch
    

    那么这也是一个属性:

    assert BankAccount.findAllByBranch(b).every{it.branch == b}.
    

    (经过编辑,从技术上讲,这个属性应该由DB强制执行,finder方法的实现和开发者可能会认为它是“安全的”且不可破坏的。事实上,你的应用程序在hibernate下面某处使用的大多数“where”条件和“join”定义了类似性质的属性。)

    可重复查找器的问题可能会导致大多数属性在并发使用时崩溃(可怕的事情!)。例如,我在这里重申了我写的一段代码 问题中链接的相关JIRA罚单:

    ... a1 has branch b1
    BankAccount.findByName('a1')
    
    ... concurrently a1 is moved to branch b2
    //fails because stale a1.branch == b1
    assert BankAccount.findAllByBranch(b2).every{it.branch == b2} 
    

    您的应用程序可能使用显式和隐式属性,并且可能具有执行它们的逻辑。 例如,应用程序可能依赖于名称的唯一性,如果名称不唯一,则会出现异常或返回错误的结果(可能名称本身用于定义equals()。这是明确的用法。 应用程序可能会提供列表视图,如果列表显示违反了属性,这将非常尴尬(分支b2下的帐户列表显示了分支b1下的一些帐户-这是隐式用法)。任何此类情况都会受到“可重复查找器”的影响

    如果使用Grails代码(而不是DB约束)来强制执行属性,那么除了“可重复查找器”之外,还需要解决更明显的并发问题。(我不是在这里讨论这些。)

    发现问题:

    这仅适用于损坏的属性。我不知道可重复查找程序是否会导致其他问题。

    因此,我认为第一步是识别应用程序中的所有属性(经过编辑:将有许多属性,可能太多而无法检查-因此,关注可能同时更改的域对象可能是关键所在),第二步是确定应用程序(隐式或显式)在何处以及如何使用这些属性,以及它们是如何强制执行的。需要检查其中每一个的代码,以验证可重复查找器不是问题所在

    简单地启用SQL跟踪(以及查看每个HTTP请求的开始和结束位置),并检查SQL“from”部分中确定的关注区域中的任何表名的日志跟踪。如果这样的表格出现不止一次 根据请求,这可能是问题的良好迹象。良好的功能测试覆盖率有助于生成此类日志文件

    这显然是一个不平凡的过程,这里没有防弹的解决方案

    解决问题:

    对以前查询中的对象使用discard()或在单独的hibernate会话中运行依赖于特定应用程序属性的查询应该可以解决这个问题。使用新的会话方法应该更加防弹。我不建议在这里使用refresh()。 (注意,hibernate没有提供公共API来查询连接到其会话的对象。)
    使用新会话将使应用程序暴露于 一些新问题,如LazyInitalizationException或DupicateKeyException。相比之下,这些都微不足道

    旁注:我个人认为框架设计决定会导致代码在添加附加查询时中断:一个可怕的设计缺陷。p>

    比较Hibernate和Active Record(我对它知之甚少)是很有趣的。Hibernate采用ORM纯粹主义的方法,试图将RDBMS变成OO,而Active Record则采用“不共享”的方法,即与DB和 让数据库处理更复杂的并发问题
    当然,在活动记录节点中。儿童首先()。家长!=但这是件坏事吗
    我承认我不理解hibernate在执行新查询时不刷新缓存中对象的决定背后的原因。 他们是否担心副作用?可以游说Hibernate和Grails来改变这一点吗?因为这似乎是最好的长期解决方案。(2014年9月24日编辑:我让Hibernate解决这个问题的努力失败了。)

    新增(2014/08/12): 重新考虑Grails应用程序的设计,将GORM/Hibernate用作非常薄的持久层,也可能会有所帮助。在设计这样一个层时,对每个请求期间发出的查询进行严格控制,可以最大限度地减少这个问题。这显然不是Grails框架所倡导的(2014年9月24日编辑,它只会减少而不会消除这个问题)

    经过很多思考,我觉得这可能是Grails/Hibernate技术堆栈中的一个主要逻辑漏洞。如果您关心并发性,那么就没有好的解决方案,您应该关心

  3. # 3 楼答案

    马克·帕尔默关于这些问题的一篇好文章。我觉得很有趣。在文章的最后,他给出了一些“解决方案”,可以满足你们中一些人的需求

    The false optimism of GORM and Hibernate

  4. # 4 楼答案

    可重复读取是在数据库事务中执行preventing lost updates的一种方式。大多数应用程序采用读-修改-写数据访问模式,打破了数据库事务边界和pushing transactions to the application-layer

    Hibernate使用transactional write-behind policy,因此实体状态转换会尽可能延迟,以减少与DML语句相关联的数据库锁定

    在应用程序级事务中,第一级缓存充当应用程序级可重复读取机制。但是,虽然在使用物理事务时,数据库锁定可以确保可重复读取的一致性,但对于应用程序级事务,您需要应用程序级锁定机制。这就是为什么你应该首先使用乐观锁定

    乐观锁定允许其他人修改您以前加载的数据,同时防止您更新过时的数据

    打破的不是平等。无论如何,数据库约束应该始终强制执行唯一的业务密钥

    对于与帐户更新有关的操作,您应该使用单个数据库事务,通过锁定获取(选择更新)确保安全,或者使用乐观锁定,因此当其他人更新您的数据时,您将得到一个过时的实体异常

    我可以。该实体将从一级缓存中重新使用。对于SQL查询,您可以自由加载并发更改。只要加载实体以便以后进行更新,就可以了,因为乐观锁定机制将防止保存过时的数据

    如果您使用HQL/JPQL只是为了查看,那么您可能希望使用投影