• 如何防止长时间对话中的更新丢失


    介绍

    所有数据库语句都在物理事务的上下文中执行,即使我们没有显式声明事务边界(BEGIN/COMMIT/ROLLBACK)。数据完整性由数据库事务的ACID 属性强制执行。

    逻辑事务与物理事务

    逻辑事务是应用程序级工作单元,可以跨越多个物理(数据库)事务。在多个用户请求(包括用户思考时间)中保持数据库连接打开绝对是一种反模式。

    数据库服务器可以容纳有限数量的物理连接,并且通常使用连接池重用这些连接。长时间持有有限的资源会阻碍可扩展性。因此,数据库事务必须很短,以确保尽快释放数据库锁和池连接。

    Web 应用程序需要读取-修改-写入对话模式。Web 对话由多个用户请求组成,所有操作在逻辑上都连接到同一应用程序级事务。一个典型的用例是这样的:

    1. 爱丽丝请求展示某种产品
    2. 从数据库中获取产品并返回到浏览器
    3. 爱丽丝请求修改产品
    4. 产品必须更新并保存到数据库

    所有这些操作都应封装在单个工作单元中。因此,我们需要一个也符合 ACID 的应用程序级事务,因为其他并发用户可能会在共享锁释放很久之后修改相同的实体。

    我之前的帖子中,我介绍了丢失更新的危险。数据库事务 ACID 属性只能在单个物理事务的边界内防止这种现象。将事务边界推送到应用层需要应用级 ACID 保证。

    为了防止丢失更新,我们必须具有应用程序级可重复读取以及并发控制机制。

    长时间的对话

    HTTP是一种无状态协议。无状态应用程序总是比有状态应用程序更容易扩展,但对话不能是无状态的。

    Hibernate提供了两种实现长对话的策略:

    • 扩展持久性上下文
    • 分离的对象

    扩展持久性上下文

    在第一个数据库事务结束后,JDBC 连接将关闭(通常返回到连接池),休眠会话将断开连接。新用户请求将重新附加原始会话。只有最后一个物理事务必须发出 DML 操作,否则,应用程序级事务不是原子工作单元。

    为了在应用程序级事务过程中禁用持久性,我们有以下选项:

    • 我们可以通过将会话刷新模式切换到手动来禁用自动刷新。在最后一个物理事务结束时,我们需要显式调用Session#flush() 来传播实体状态转换
    • 除最后一个事务外,所有事务都标记为只读。对于只读事务,休眠会禁用脏检查和默认自动刷新。

      只读标志可能会传播到基础JDBC 连接,因此驱动程序可能会启用某些数据库级只读优化。

      最后一个事务必须是可写的,以便刷新和提交所有更改。

    使用扩展持久性上下文更方便,因为实体在多个用户请求中保持附加状态。缺点是内存占用。持久性上下文可能很容易随着每个新提取的实体而增长。休眠默认脏检查机制使用深度比较策略,比较所有托管实体的所有属性。持久性上下文越大,脏检查机制的速度就越慢。

    可以通过逐出不需要传播到最后一个物理事务的实体来缓解此问题。

    Java Enterprise Edition通过使用Session Beans和EXTENDED PersistenceContext提供了一个非常方便的编程模型@Stateful

    所有扩展持久性上下文示例都将默认事务传播设置为NOT_SUPPORTED这使得不确定查询是在本地事务的上下文中注册还是每个查询是在单独的数据库事务中执行的。

    分离的对象

    另一种选择是将持久性上下文绑定到中间物理事务的生命周期。持久性上下文关闭后,所有实体都将分离。要使分离的实体成为托管实体,我们有两个选择:

    • 可以使用Hibernate特定的Session.update()方法重新附加实体。如果存在已附加的实体(相同的实体类和相同的标识符),则 Hibernate 会引发异常,因为会话最多可以有一个对任何给定实体的引用。

      Java Persistence API 中没有这样的等价物。

    • 分离的实体也可以与其等效的持久对象合并。如果当前没有加载的持久性对象,Hibernate将从数据库中加载一个。分离的实体将不会成为托管实体。

      到现在为止,您应该知道这种模式闻起来像麻烦:

      如果加载的数据与我们之前加载的数据不匹配怎么办?
      如果实体自我们首次加载以来发生了变化怎么办?

      使用较旧的快照覆盖新数据会导致更新丢失。因此,在处理长会话时,并发控制机制不是一种选择。

      HibernateJPA都提供实体合并。

    分离实体存储

    分离的实体必须在给定的长会话的整个生存期内可用。为此,我们需要一个有状态的上下文来确保所有会话请求都找到相同的分离实体。因此,我们可以利用:

    • 有状态会话 Bean

      有状态会话 bean 是 Java 企业版提供的最大功能之一。它隐藏了不同用户请求之间保存/加载状态的所有复杂性。作为一项内置功能,它会自动从群集复制中受益,因此开发人员可以专注于业务逻辑。

      Seam是一个Java EE应用程序框架,内置了对Web对话的支持。

    • HttpSession

      我们可以将分离的对象保存在 HttpSession 中。大多数Web/应用程序服务器都提供会话复制,因此非JEE技术(如Spring框架)可以使用此选项。对话结束后,我们应该始终丢弃所有相关状态,以确保我们不会因不必要的存储空间而使会话膨胀。

      您需要小心同步所有 HttpSession 访问(getAttribute/setAttribute),因为出于一个非常奇怪的原因,此 Web 存储不是线程安全的

      Spring Web Flow是一个 Spring MVC 伴侣,支持 HttpSession Web 对话。

    • 榛子

      Hazelcast 是一种内存中群集缓存,因此它是长对话存储的可行解决方案。我们应该始终设置过期策略,因为在 Web 应用程序中,对话可能会启动和放弃。过期充当 Http 会话失效。

    无状态对话反模式

    与数据库事务一样,我们需要可重复的读取,否则我们可能会加载已经修改的记录而没有意识到这一点:

    1. 爱丽丝请求展示产品
    2. 从数据库中获取产品并返回到浏览器
    3. 爱丽丝请求修改产品
    4. 由于 Alice 没有保留之前显示的对象的副本,因此她必须再次重新加载它
    5. 产品已更新并保存到数据库中
    6. 批处理作业更新已丢失,爱丽丝永远不会意识到

    有状态无版本对话反模式

    如果我们想确保隔离和一致性,则必须保留会话状态,但我们仍然会遇到丢失更新的情况:

    即使我们有应用程序级可重复读取,其他人仍然可以修改相同的实体。在单个数据库事务的上下文中,行级锁可以阻止并发修改,但这对于逻辑事务是不可行的。唯一的选择是允许其他人修改任何行,同时防止保留过时的数据。

    乐观锁定救援

    乐观锁定是一种通用用途的并发控制技术,它适用于物理和应用程序级事务。使用 JPA 只需向我们的域模型添加一个@Version字段:

    结论

    将数据库事务边界推送到应用程序层需要应用程序级并发控制。为了确保应用程序级可重复读取,我们需要在多个用户请求之间保留状态,但在没有数据库锁定的情况下,我们需要依赖应用程序级并发控制。

    乐观锁定适用于数据库级和应用程序级事务,并且不使用任何其他数据库锁定。乐观锁定可以防止丢失更新,这就是为什么我总是建议使用 @Version 属性注释所有实体。

  • 相关阅读:
    基数排序.
    MFC Windows 程序设计[219]之磁盘目录检索(附源码)
    Linux安装GCC(最新版)
    玩转webpack(02):webpack基础使用
    快速搭建Jenkins自动化集成cicd工具
    Java--嵌套类
    Flutter 下载篇 - 叁 | 网络库切换实践与思考
    K8S的pod创建过程
    linux系统shell脚本开发之循环的使用
    TikTok选品有什么技巧?
  • 原文地址:https://blog.csdn.net/allway2/article/details/127673858