介绍
所有数据库语句都在物理事务的上下文中执行,即使我们没有显式声明事务边界(BEGIN / COMMIT / ROLLBACK)也是如此。 数据完整性由数据库事务的ACID属性强制执行。
逻辑与实物交易
逻辑事务是应用程序级别的工作单元,可以跨越多个物理(数据库)事务。 在多个用户请求(包括用户思考时间)中保持数据库连接开放绝对是一种反模式。
数据库服务器只能容纳有限数量的物理连接,并且通常通过使用连接池来重用这些物理连接。 长时间保留有限的资源会影响可伸缩性。 因此,数据库事务必须简短,以确保尽快释放数据库锁和池连接。
Web应用程序需要一种读-修改-写对话模式。 Web对话包含多个用户请求,所有操作在逻辑上都连接到同一应用程序级事务。 一个典型的用例是这样的:
- 爱丽丝要求某商品进行展示
- 从数据库中获取产品并返回到浏览器
- 爱丽丝要求产品修改
- 产品必须更新并保存到数据库
所有这些操作都应封装在单个工作单元中。 因此,我们需要一个应用程序级事务,它也是ACID投诉,因为在释放共享锁很长时间之后,其他并发用户可能会修改同一实体。
在我以前的文章中,我介绍了丢失更新的危险。 数据库事务ACID属性只能在单个物理事务的边界内防止这种现象。 将事务边界推入应用程序层需要应用程序级ACID保证。
为了防止更新丢失,我们必须具有应用程序级可重复读取以及并发控制机制。
漫长的对话
HTTP是无状态协议。 无状态应用程序总是比有状态应用程序更容易扩展,但是对话并非无状态。
Hibernate提供了两种实现长时间对话的策略:
- 扩展的持久性上下文
- 分离的物体
扩展的持久性上下文
在第一个数据库事务结束后,JDBC连接关闭(通常返回到连接池),并且Hibernate会话断开连接。 新的用户请求将重新附加原始会话。 只有最后的物理事务必须发出DML操作,否则,应用程序级事务不是原子工作单元。
为了在应用程序级事务过程中禁用持久性,我们有以下选择:
- 我们可以通过将Session FlushMode切换为MANUAL来禁用自动刷新 。 在最后一次物理事务结束时,我们需要显式调用Session#flush()来传播实体状态转换 。
- 除最后一个事务外,所有事务都标记为只读 。 对于只读事务,Hibernate会禁用脏检查和默认的自动刷新功能。只读标志可能会传播到底层的JDBC Connection ,因此驱动程序可能会启用某些数据库级别的只读优化。最后一个事务必须是可写的,因此所有更改均已刷新并提交。
使用扩展的持久化上下文更加方便,因为实体可以跨多个用户请求保持连接。 缺点是内存占用量。 持久性上下文可能随每个新获取的实体而轻易增长。 Hibernate默认的脏检查机制使用深度比较策略 ,比较所有托管实体的所有属性。 持久性上下文越大,脏检查机制将变得越慢。
可以通过驱逐不需要传播到最后物理交易的实体来缓解这种情况。
Java Enterprise Edition通过使用@Stateful Session Bean和EXTENDED PersistenceContext提供了非常方便的编程模型 。
所有扩展的持久性上下文示例都将默认事务传播设置为NOT_SUPPORTED ,这使得不确定查询是否在本地事务的上下文中注册或每个查询是否在单独的数据库事务中执行。
分离的物体
另一个选择是将持久性上下文绑定到中间物理事务的生命周期。 在持久性上下文关闭时,所有实体都将分离。 为了使一个独立实体成为管理对象,我们有两个选择:
- 可以使用Hibernate特定的Session.update()方法重新连接该实体。 如果已经存在一个连接的实体(相同的实体类,并且具有相同的标识符),则Hibernate会引发异常,因为一个Session最多可以具有任何给定实体的引用。JavaPersistence API中没有这样的等效项。
- 分离的实体也可以与其等效的持久对象合并。 如果当前没有加载的持久性对象,则Hibernate将从数据库中加载一个。 分离的实体将不会被管理。现在,您应该知道这种模式闻起来像麻烦:
如果加载的数据与我们先前加载的数据不匹配怎么办?
如果实体自首次加载以来已发生更改,该怎么办?使用较旧的快照覆盖新数据会导致更新丢失。 因此,在处理长会话时,并发控制机制不是一种选择。Hibernate和JPA都提供实体合并。
分离实体存储
在给定的长时间对话的整个生命周期中,分离的实体必须始终可用。 为此,我们需要一个有状态的上下文,以确保所有对话请求都找到相同的分离实体。 因此,我们可以利用:
- 有状态会话Bean :有状态会话Bean是Java Enterprise Edition提供的最大功能之一。 它隐藏了不同用户请求之间保存/加载状态的所有复杂性。 作为一项内置功能,它会自动从群集复制中受益,因此开发人员可以专注于业务逻辑。 Seam是一个Java EE应用程序框架,具有对Web对话的内置支持。
- HttpSession :我们可以将分离的对象保存在HttpSession中。 大多数Web /应用程序服务器都提供会话复制,因此非JEE技术(例如Spring框架)可以使用此选项。 对话结束后,我们应始终丢弃所有关联状态,以确保不会浪费不必要的存储空间。您需要注意同步所有HttpSession访问(getAttribute / setAttribute),因为一个非常奇怪的原因,此网络存储空间不是线程安全的 。 Spring Web Flow是支持HttpSession Web对话的Spring MVC伴侣。
- Hazelcast :Hazelcast是内存中的群集缓存,因此它是长时间对话存储的可行解决方案。 我们应该始终设置过期策略,因为在Web应用程序中,对话可能会开始并被放弃。 到期充当Http会话无效。
无状态对话反模式
像数据库事务一样,我们需要可重复的读取,否则我们可能会加载已修改的记录而没有意识到:
- 爱丽丝要求产品展示
- 从数据库中获取产品并返回到浏览器
- 爱丽丝要求产品修改
- 由于Alice尚未保留先前显示的对象的副本,因此她不得不再次重新加载它
- 产品已更新并保存到数据库
- 批处理作业更新已丢失,爱丽丝将永远不会意识到
有状态的无版本对话反模式
如果要确保隔离性和一致性,必须保留对话状态,但是仍然会遇到丢失更新的情况:
即使我们拥有应用程序级的可重复读取,其他人仍然可以修改相同的实体。 在单个数据库事务的上下文中,行级锁可以阻止并发修改,但这对于逻辑事务是不可行的。 唯一的选择是允许其他人修改任何行,同时防止保留过时的数据。
乐观锁定救援
乐观锁定是一种通用的并发控制技术,它适用于物理和应用程序级别的事务。 使用JPA只需要在我们的域模型中添加@Version字段即可:
结论
将数据库事务边界推入应用程序层需要应用程序级并发控制。 为了确保应用程序级可重复读取,我们需要保留多个用户请求之间的状态,但是在没有数据库锁定的情况下,我们需要依赖应用程序级并发控制。
乐观锁定适用于数据库和应用程序级事务,并且不使用任何其他数据库锁定。 乐观锁定可以防止更新丢失,这就是为什么我总是建议所有实体都使用@Version属性进行注释的原因。
翻译自: https://www.javacodegeeks.com/2014/09/preventing-lost-updates-in-long-conversations.html