rob*_*zek 20 java grails spring hibernate transactions
我喜欢将此问题称为"可重复查找器"问题,因为它在某种意义上与"不可重复读取"相反.因为hibernate重用附加到其会话的对象,所以查找器的结果可能包括一些现在过时的旧版本的对象.
问题在技术上是一个Hibernate设计问题,但由于Hibernate会话隐含在Grails和Grails中,域对象是长期存在的(HTTP请求对我来说很长)我决定在Grails/GORM的上下文中提出这个问题.
我想问一下这里的专家是否有任何常用的处理这个问题的策略.
考虑一下:
class BankAccount {
String name
Float amount
static constraints = {
name unique: true
}
}
Run Code Online (Sandbox Code Playgroud)
和'componentA'代码:
BankAccount.findByName('a1')
Run Code Online (Sandbox Code Playgroud)
'componentB代码:
def result = BankAccount.findAll()
Run Code Online (Sandbox Code Playgroud)
假设componentA首先执行,后跟一些其他逻辑,然后是componentB,组件B的结果由视图呈现.组件A和B不希望彼此了解太多.
这样,componentB结果包含旧版本的BankAccount'a1'.
许多非常令人尴尬的事情都可能发生.如果同时修改了BankAccounts,则所呈现的列表可以包含2个名称为"a1"的项目(唯一性看起来是用户!)或者帐户之间的转帐可以显示为部分应用的交易(如果资金从a2转移到a1那么它将显示从a2中扣除但是还没有a1).这些问题令人尴尬,并且会降低用户对应用程序的信心.
(ADDED 9/24/2014:这是一个开眼界的例子,这个断言可能会失败:
BankAccount.findAllByName('a1').every{ it.name == 'a1' }
Run Code Online (Sandbox Code Playgroud)
如何发生这种情况的示例可以在任何链接的JIRA门票或我的博客中找到.)
(2014年9月24日增加: 注意:在实现equals()方法时使用数据库强制唯一键的看似合理的建议并不安全.您可能会获得具有相同"业务键"值的2个对象,这些对象是不同的. )
可能的解决方案似乎是添加了很多discard()调用或很多withNewSession()调用并处理LazyIntializationExeption和DuplicateKeyException等.
但是如果我这样做,为什么我使用hibernate/GORM?从每个查询返回的每个对象上调用刷新看起来简直太荒谬了.
我目前的想法是,在某些关键领域使用短会话/ 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
ADDED 2014年10月17日: 我收到几条回复,声明这是任何数据库应用程序/任何ORM问题.这不正确.
确实可以通过使用长事务(Hibernate会话长度/ HTTP请求长度)+设置高于REPEATABLE READ的正常DB隔离级别来避免此问题.这个解决方案根本不可接受(为什么我们有跨国服务,如果应用程序正常工作,我们需要HTTP请求长事务!?)
数据库应用程序和其他ORM不会出现此问题.他们不需要长事务来工作,只需READ COMMITTED即可防止问题.
现在已经2个月了,因为我在这里发布了这个问题,并没有收到有意义的答案.这只是因为这个问题没有答案.这是Hibernate可以修复的东西,而不是Grails应用程序可以修复的东西.新增10/17/2014-END
这是我自己尝试回答这个问题.
(ADDED 9/24/2014这个问题根本就没有很好的解决方案.可悲的是,HHHH-9367 JIRA票被Hibernate拒绝为"不是错误".该票证中唯一的解决方案是使用刷新(我假设这需要将所有查询更改为看起来像这样的内容:
BankAccount.findAllBy...(...).each{ it.refresh() }
Run Code Online (Sandbox Code Playgroud)
就个人而言,我不同意这是一个有意义的解决方案.)
正如我上面所解释的,如果Hibernate/GORM查询返回一组DomainObjects,并且其中一些对象已经处于休眠会话中(由先前的查询填充),则查询将返回这些旧对象,并且这些对象将不会自动刷新.这可能会导致一些难以发现的并发问题.我称之为可重复查找器问题.
这与二级缓存无关.这个问题是由于即使没有配置第二级缓存,hibernate的工作原理也是如此.(2014年9月24日编辑:而且,这不是任何ORM,任何数据库应用程序问题,该问题特定于使用Hibernate).
对您的申请的影响:
(我只能解释我所知道的影响,我并不是说这些是唯一的影响).
域对象通常具有一组关联的约束/逻辑规则,这些约束/逻辑规则需要通常包含多个记录,并由应用程序或数据库本身强制执行.我将从FP和测试中借用一个术语,并称之为"属性".
示例属性:在上面的BankAccount示例中,名称唯一性(由DB强制执行)是一个属性(例如,您可以在定义equals()方法时使用它),如果在帐户之间转移资金,则这些帐户中的总金额需要是一个常数 - 这是一个属性.
如果我修改我的BankAccount类并添加'branch'关联:
BankBranch branch
Run Code Online (Sandbox Code Playgroud)
那么这也是一个属性:
assert BankAccount.findAllByBranch(b).every{it.branch == b}.
Run Code Online (Sandbox Code Playgroud)
(编辑,这个属性在技术上应该由DB强制执行并且finder方法的实现和开发人员可能认为它是'安全的'并且不易破碎.事实上,你的app在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}
Run Code Online (Sandbox Code Playgroud)
您的应用程序可能使用显式和隐式属性,并且可能具有强制执行它们的逻辑.例如,应用程序可能依赖于唯一的名称,如果它们不是唯一的,则会异常或返回错误的结果(可能名称本身用于定义equals()).这是明确的用法.应用程序可能会提供列表视图,如果列表显示违反了属性,则会非常尴尬(分支b2下的帐户列表显示某些具有分支b1的帐户 - 这是隐式用法).任何这种情况都会受到"可重复的发现者"的影响.
如果Grails代码(不是DB约束)用于强制执行属性,那么除了'可重复查找器'之外,还需要解决更明显的并发问题.(我这里不讨论这些.)
发现问题:
(这仅适用于破碎的属性.我不知道可重复的查找器是否会导致其他问题.)
所以,我认为第一步是识别应用程序中的所有属性(EDITED:会有很多属性,可能需要检查太多 - 因此,关注可能会同时更改的域对象可能是关键点.),第二步是确定应用程序(隐式或显式)使用这些属性的位置和方式以及它们是如何实施的.需要检查每个这些需求的代码,以验证可重复的查找器不是问题.
简单地启用SQL跟踪(以及跟踪每个HTTP请求的开始和结束位置)并检查来自SQL的"from"部分中任何表名的已识别区域的日志跟踪可能是一个好主意.如果这样的表每次请求出现多次,这可能是问题的良好指示.良好的功能测试覆盖率可以帮助生成此类日志文件.
这显然是一个不重要的过程,这里没有防弹解决方案.
修复问题:
对先前查询的对象使用discard()或在单独的hibernate会话中运行依赖于某些应用程序属性/属性的查询应该可以解决问题.使用新的会话方法应该更加防弹.我不建议在这里使用refresh().(注意,hibernate不提供公共API来查询附加到其会话的对象.)
使用新会话将使应用程序暴露于一些新问题,如LazyInitalizationException或DupicateKeyException.相比之下,这些都是微不足道的.
侧面注意:我个人认为框架设计决策导致代码在添加额外查询时中断:一个可怕的设计缺陷.
将Hibernate与Active Record(我不太了解)进行比较是很有趣的.Hibernate采用ORM纯粹主义方法尝试将RDBMS转换为OO,Active Record采用了"无共享"的方法来保持更接近数据库并让DB处理更复杂的并发问题.
当然,在Active Record node.children.first().parent!= parent但是那么糟糕吗?
我承认不理解hibernate决定在执行新查询时不刷新其缓存中的对象的原因.他们一直担心副作用吗?Hibernate和Grails可以游说改变吗?因为这似乎是最好的长期解决方案.(2014年9月24日编辑:我让Hibernate解决问题的努力失败了.)
ADDED(2014/08/12):重新考虑Grails应用程序的设计并将GORM/Hibernate仅用作非常薄的持久层也可能有所帮助.通过严格控制在每个请求期间发出的查询来设计此类层应该最小化此问题.这显然不是Grails框架所倡导的,(2014年9月24日编辑,它只会减少并不能解决问题.)
经过深思熟虑之后,我觉得这可能是Grails/Hibernate技术堆栈中的一个重要逻辑漏洞.如果你关心并发,真的没有好的解决方案,你应该担心.
归档时间: |
|
查看次数: |
2363 次 |
最近记录: |