13、Java EE 应用中的事务管理

Java EE 应用中的事务管理

1. 事务在 Java EE 应用中的重要性

事务是任何企业系统的重要组成部分。当 Java EE 应用执行影响底层数据的操作时,这些更改是在事务范围内完成的,以确保所处理的数据始终处于一致状态。企业应用通常会访问和操作存储在一个或多个底层数据库中的数据,这些数据可能会被其他应用同时访问。企业应用对数据执行的操作通常会被分组为逻辑工作单元,每个单元对应一个不可分割的业务操作,例如从储蓄账户向支票账户转账。

由于企业应用处理的底层数据对业务至关重要,因此必须有一个可靠的机制来确保数据的一致性、准确性和及时性,无论执行何种操作以及有多少应用同时访问数据。在企业环境中,这些问题都可以通过事务来解决。

2. JTA 事务

在大多数情况下,开发 Java EE 应用时需要处理 Java 事务 API(JTA)事务,这些事务由容器管理。JTA 为应用服务器使用的底层事务管理器提供了一组接口。实际上,可能只需要了解 javax.transaction.UserTransaction 接口,它允许以编程方式控制事务边界。若要指示容器在处理基于底层数据库定义的 DataSource 对象时使用 JTA,需要在 persistence.xml 中使用 jta-data-source 元素定义该数据源,示例如下:

<persistence ...>
    <persistence-unit name="sampleapp-pu" transaction-type="JTA">
        <jta-data-source>jdbc/mysqlpool</jta-data-source>
    </persistence-unit>
</persistence>

处理 JTA 事务时,不一定要使用 javax.transaction.UserTransaction 接口。如果选择容器管理的事务划分,容器将负责划分事务边界。作为 JTA 事务的替代方案,可以使用资源本地事务,由应用程序而不是容器控制,后续会详细讨论。

3. 事务划分类型

从应用程序开发者的角度来看,开发事务性代码时最重要的是定义事务边界。即使不需要显式的事务划分,开发者也应清楚事务的开始和结束位置。Java EE 应用中可以使用的事务划分类型如下表所示:
| 划分类型 | 描述 |
| — | — |
| 声明式事务划分 | 也称为容器管理的事务划分。容器根据可以使用 TransactionAttribute 注解应用于整个 bean 类或特定 bean 业务方法的事务属性值,以声明方式控制事务划分。也可以在部署描述符中指定事务属性。 |
| 编程式事务划分 | 通常称为 bean 管理的事务划分。使用这种划分类型时,使用 UserTransaction 接口在 bean 的业务方法中显式划分事务边界。 |
| 客户端管理的事务划分 | 与 bean 管理的事务划分类似,客户端管理的事务划分意味着使用 UserTransaction 接口方法显式划分事务。不过,在这种情况下,在调用企业 bean 业务方法的客户端程序中定义事务。 |

开发企业 bean 事务性代码时,可以在容器管理和 bean 管理的事务划分之间进行选择。开发事务性客户端应用程序代码时,可以以编程方式划分事务。

4. 使用声明式事务划分

使用声明式事务划分时,EJB 容器将控制事务,无需显式调用 begin commit 。容器负责划分事务,但可以通过将事务属性设置为适当的值或以编程方式划分事务来影响容器划分事务的方式。事务属性用于控制事务的范围,可以通过 TransactionAttribute 元数据注解或在部署描述符中设置 container-transaction 元素内的 trans-attribute 元素来设置事务属性。事务属性的可能值如下表所示:
| TransactionAttribute 注解 | trans-attribute 元素 | 描述 |
| — | — | — |
| NOT_SUPPORTED | NotSupported | 如果被调用的企业 bean 业务方法的事务属性设置为此值,容器将不会启动事务。如果客户端代码在事务上下文中运行,则客户端的事务将被挂起,直到被调用的业务方法完成。 |
| REQUIRED | Required | 容器在事务上下文中调用业务方法。如果客户端与事务上下文关联,则业务方法在客户端的事务中运行。否则,容器为该方法创建一个新事务。 |
| SUPPORTS | Supports | 如果客户端与事务上下文关联,则业务方法在客户端的事务中运行。否则,容器在没有事务上下文的情况下调用业务方法。 |
| REQUIRES_NEW | RequiresNew | 容器使用新事务调用业务方法,无论客户端是否在事务中执行。如果客户端在事务中运行,则该事务将被暂时挂起,并在被调用的业务方法结束时恢复。 |
| MANDATORY | Mandatory | 容器在客户端的事务上下文中调用业务方法。如果客户端没有事务上下文,则会抛出 TransactionRequiredException 异常。 |
| NEVER | Never | 容器在没有事务上下文的情况下调用业务方法。如果客户端在事务上下文运行,则会抛出 RemoteException 异常。 |

默认情况下,事务属性设置为 REQUIRED ,这意味着默认情况下,业务方法在事务上下文中执行,无论客户端是否在事务上下文中执行。

示例

假设有两个会话 bean,其中一个作为另一个的客户端。以下是客户端的代码片段:

@Stateless
public class ShoppingCartBean implements ShoppingCart {
    @EJB
    private OrderSession neworder;

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void placeOrder() {
        Integer pono = neworder.createOrder(cust_id);
        neworder.addOrderDetails(pono, items);
    }
}

ShoppingCartBean placeOrder 业务方法的事务属性设置为 REQUIRES_NEW ,因此该方法将始终在新事务中执行。以下是 OrderSessionBean 的代码片段:

@TransactionAttribute(TransactionAttributeType.REQUIRED)
@Stateless
public class OrderSessionBean implements OrderSession {
    public Integer createOrder() {
        // ...
        return pono;
    }
    public Integer addOrderDetails(Integer pono, List items) {
        // ...
    }
}

OrderSessionBean 为整个类使用 REQUIRED 事务属性,这意味着该属性应用于 OrderSessionBean 的所有方法。

5. 以编程方式划分事务

在某些情况下,可能需要以编程方式划分事务边界。可以利用 javax.transaction.UserTransaction 接口与控制事务的底层 JTA 事务管理器进行交互,该接口可在客户端程序和企业 bean 中使用。 javax.transaction.UserTransaction 接口的方法如下表所示:
| 方法 | 描述 |
| — | — |
| void begin() | 开始一个新事务,并将其与当前线程关联 |
| void commit() | 完成与当前线程关联的事务,使事务内执行的所有操作的效果永久化 |
| void rollback() | 回滚与当前线程关联的事务,撤销事务内所有操作的效果 |
| void setRollbackOnly() | 指示容器该事务的唯一可能结果是回滚 |
| int getStatus() | 获取与当前线程关联的事务的状态 |
| void setTransactionTimeout(int secs) | 修改当前线程启动的事务的超时时间 |

以下是修改后的 ShoppingCartBean 代码,以编程方式划分 placeOrder 业务方法的事务边界:

@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class ShoppingCartBean implements ShoppingCart {
    @Resource
    javax.transaction.UserTransaction ut;
    @EJB
    private OrderSession neworder;

    public void placeOrder() {
        try {
            ut.begin();
            Integer pono = neworder.createOrder(cust_id);
            neworder.addOrderDetails(pono, items);
            ut.commit();
        } catch (Exception e) {
            try {
                ut.rollback();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

与之前的代码相比,这里手动定义了事务边界,更加灵活,可以只将业务方法代码的一部分包含在事务中,或者在单个业务方法中定义多个事务。

6. 在客户端代码中使用事务划分

UserTransaction 接口的使用不限于企业 bean,还可以在 Web 组件(如 Servlet 和 JSP 页面)中使用它来划分 JTA 事务。以下是一个在 Servlet 中使用 UserTransaction 接口划分事务边界的示例:

public class placeOrder extends HttpServlet {
    @Resource
    UserTransaction utx;

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        try {
            utx.begin();
            Integer pono = neworder.createOrder(cust_id);
            neworder.addOrderDetails(pono, items);
            utx.commit();
        } catch (Exception ex) {
            try {
                utx.rollback();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

在这个 Servlet 中,如果 createOrder addOrderDetails 方法失败,会调用 UserTransaction 接口的 rollback 方法。

7. 处理资源本地事务

除了 JTA 事务,还有通过 EntityTransaction 接口由应用程序控制的资源本地事务。通常,在 Java SE 环境中可能需要使用资源本地事务,因为 JTA 在这种环境中不受支持。可以通过调用资源本地 EntityManager getTransaction 方法来获取 EntityTransaction 实例的引用。 EntityTransaction 接口的方法如下表所示:
| 方法 | 描述 |
| — | — |
| void begin() | 开始一个新的资源事务 |
| void commit() | 完成当前事务,使事务内执行的所有操作的效果永久化 |
| void rollback() | 回滚当前事务,撤销事务内所有操作的效果 |
| void setRollbackOnly() | 将当前事务标记为回滚 |
| boolean getRollbackOnly() | 检查当前事务是否已被标记为回滚 |
| boolean isActive() | 检查是否有事务正在进行中 |

要使用资源本地事务,需要将 persistence.xml persistence-unit 元素的 transaction-type 属性设置为 RESOURCE_LOCAL ,并使用 non-jta-data-source 元素定义应用程序中使用的非 JTA 数据源。以下是 persistence.xml 的示例:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
    <persistence-unit name="app-pu" transaction-type="RESOURCE_LOCAL">
        <non-jta-data-source>jdbc/mysqlpool</non-jta-data-source>
    </persistence-unit>
</persistence>

以下是一个在普通 Java 类中使用资源本地事务的示例:

public class OrderClass {
    private EntityManagerFactory emf;
    private EntityManager em;
    private Order order;
    private Part part;

    public OrderClass() {
        emf = Persistence.createEntityManagerFactory("app-pu");
        em = emf.createEntityManager();
    }

    public void getOrder(int pono) {
        order = em.find(Order.class, pono);
    }

    public void getPart(String partid) {
        part = em.find(Part.class, partid);
    }

    public void addLineItem(int quantity) {
        em.getTransaction().begin();
        LineItem lineItem = new LineItem(order, part, quantity);
        order.getLineItems().add(lineItem);
        em.getTransaction().commit();
    }

    public void destroy() {
        em.close();
        emf.close();
    }
}

OrderClass 的构造函数中,创建了一个实体管理器工厂并获取了一个资源本地 EntityManager 。在 addLineItem 方法中,使用 EntityManager getTransaction 方法获取 EntityTransaction 接口来创建资源本地事务。

8. 一些事务场景

涉及多个容器管理的 EntityManager 操作的业务方法的事务行为

假设有一个业务方法使用两个容器管理的实体管理器执行持久化操作。首先,需要在 persistence.xml 中定义两个持久化单元:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
    <persistence-unit name="contaneremtest1-pu" transaction-type="JTA">
        <jta-data-source>jdbc/mysqlpool</jta-data-source>
    </persistence-unit>
    <persistence-unit name="contaneremtest2-pu" transaction-type="JTA">
        <jta-data-source>jdbc/mysqlpool</jta-data-source>
    </persistence-unit>
</persistence>

以下是 OrderTestBean 会话 bean 的代码,它使用两个容器管理的实体管理器:

@Stateless
public class OrderTestBean implements OrderTest {
    @PersistenceContext(unitName="contaneremtest1-pu")
    private EntityManager em1;
    @PersistenceContext(unitName="contaneremtest2-pu")
    private EntityManager em2;

    public Integer setOrder(Integer cust_id, Integer empno) {
        Integer order_pono;
        try {
            Customer cust = (Customer) em1.find(Customer.class, cust_id);
            Employee emp = (Employee) em1.find(Employee.class, empno);
            Order order1 = new Order();
            order1.setCustomer(cust);
            order1.setEmployee(emp);
            em1.persist(order1);
            em1.flush();
            em1.refresh(order1);
            order_pono = order1.getPono();
        } catch (Exception e) {
            throw new EJBException(e.getMessage());
        }
        return order_pono;
    }

    public String changeOrderEmpTest(Integer pono, Integer empno, Integer custid) {
        String order_details;
        try {
            Order order1 = (Order) em1.find(Order.class, pono);
            order_details = "order "+ order1.getPono()+ " emp: " + 
                order1.getEmployee().getLastname()+" cust: " + 
                order1.getCustomer().getCust_name()+"<br/>";
            Employee emp = (Employee) em1.find(Employee.class, empno);
            order1.setEmployee(emp);
            em1.flush();
            em1.refresh(order1);
            order_details = order_details+"order "+ order1.getPono()+ " emp: " + 
                order1.getEmployee().getLastname()+" cust: " + 
                order1.getCustomer().getCust_name()+"<br/>";
            Order order2 = (Order) em2.find(Order.class, pono);
            Customer cust = (Customer) em1.find(Customer.class, custid);
            order2.setCustomer(cust);
            em2.flush();
            em2.refresh(order2);
            order_details = order_details+"order "+ order2.getPono()+ " emp: " + 
                order2.getEmployee().getLastname()+" cust: " + 
                order2.getCustomer().getCust_name()+"<br/>";
        } catch (Exception e) {
            throw new EJBException(e.getMessage());
        }
        return order_details;
    }
}

可以使用以下 Servlet 测试 OrderTestBean 的业务方法:

public class EmEjbTestServlet extends HttpServlet {
    @EJB private OrderTest orderTest;

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        response.setBufferSize(8192);
        PrintWriter out = response.getWriter();
        Integer custid1=1;
        Integer custid2=2;
        Integer empno1=1;
        Integer empno2=2;
        try {
            Integer pono = orderTest.setOrder(custid1,empno1);
            out.println("Created order "+ pono +"<br/>");
            out.println(orderTest.changeOrderEmpTest(pono,empno2, custid2));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,虽然两个实体管理器与两个不同的持久化上下文关联,但它们在同一个事务中操作。因此,第一个实体管理器所做的更改在同步到数据库后,可以通过另一个管理器看到。如果将事务标记为回滚,则两个管理器所做的所有更改都将在方法完成时回滚。以下是修改 changeOrderEmpTest 方法以回滚更改的代码:

@Stateless
public class OrderTestBean implements OrderTest {
    @Resource
    private SessionContext ctx;

    public String changeOrderEmpTest(Integer pono, Integer empno) {
        try {
            // ...
            ctx.setRollbackOnly();
        } catch (Exception e) {
            throw new EJBException(e.getMessage());
        }
        return order_details;
    }
}

要确保事务已回滚,可以使用命令行工具直接连接到基础数据库,并执行查询以检索 orders 表中感兴趣的记录:

SELECT o.pono, e.lastname, c.cust_name FROM orders o, employees e, customers c 
WHERE o.pono = 21 AND o.empno=e.empno AND o.cust_id=c.cust_id;

在有扩展持久化上下文的有状态会话 bean 中定义事务

以下是一个有状态会话 bean 的示例,它使用 bean 管理的事务划分:

@Stateful
@TransactionManagement(TransactionManagementType.BEAN)
public class ShoppingCartBean implements ShoppingCart {
    @Resource
    javax.Transaction.UserTransaction ut;
    @PersistenceContext(type=EXTENDED)
    EntityManager em;
    private Order order;

    public void newOrder(Integer pono) {
        ut.begin();
        order = em.find(Order.class, pono);
    }

    public void addLineItem(LineItem lineItem) {
        order.getLineItems().add(lineItem);
    }

    public void placeOrder() {
        ut.commit();
    }
}

当调用 ShoppingCartBean newOrder 业务方法时,会开始一个新事务。该事务会在所有对 addLineItem 方法的调用中保留,最后在调用 placeOrder 方法时提交。

综上所述,在 Java EE 应用中处理事务时,有多种事务划分类型可供选择,包括 JTA 事务和资源本地事务。这些事务可以在无状态和有状态会话 bean 以及 Servlet 中显式或隐式地使用。通过合理选择和使用事务划分方式,可以确保应用程序数据的一致性和可靠性。

9. 事务管理的总结与应用建议

9.1 事务管理类型总结

在 Java EE 应用中,事务管理是确保数据一致性和完整性的关键。我们已经了解了多种事务划分类型,下面通过一个流程图来更清晰地展示选择事务划分类型的决策过程:

graph TD;
    A[开始] --> B{是否需要灵活控制事务边界};
    B -- 是 --> C[编程式事务划分];
    B -- 否 --> D{是否希望容器自动管理事务};
    D -- 是 --> E[声明式事务划分];
    D -- 否 --> F{是否在客户端程序中定义事务};
    F -- 是 --> G[客户端管理的事务划分];
    F -- 否 --> H[其他情况];

9.2 不同场景下的事务管理选择

  • 简单业务逻辑且对事务控制要求不高 :可以优先考虑声明式事务划分。例如,在一个简单的订单创建系统中,使用声明式事务划分可以让容器自动处理事务的开始和提交,减少代码复杂度。示例代码如下:
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class SimpleOrderBean implements SimpleOrderService {
    public void createOrder(Order order) {
        // 业务逻辑
    }
}
  • 复杂业务逻辑需要精细控制事务 :编程式事务划分是更好的选择。比如在一个涉及多个数据库操作且需要根据不同条件决定是否提交事务的场景中,可以使用 UserTransaction 接口进行编程式控制。示例代码如下:
@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class ComplexOrderBean implements ComplexOrderService {
    @Resource
    javax.transaction.UserTransaction ut;

    public void processOrder(Order order) {
        try {
            ut.begin();
            // 多个数据库操作
            ut.commit();
        } catch (Exception e) {
            try {
                ut.rollback();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}
  • 客户端程序调用企业 bean 并需要控制事务 :客户端管理的事务划分适用。在一个 Web 应用中,Servlet 调用企业 bean 的业务方法时,可以在 Servlet 中使用 UserTransaction 接口来管理事务。示例代码如下:
public class OrderServlet extends HttpServlet {
    @Resource
    UserTransaction utx;
    @EJB
    private OrderService orderService;

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        try {
            utx.begin();
            orderService.createOrder(new Order());
            utx.commit();
        } catch (Exception ex) {
            try {
                utx.rollback();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

9.3 事务管理的注意事项

  • 事务边界的明确性 :无论是声明式还是编程式事务划分,都要确保事务边界清晰。在声明式事务划分中,要正确设置事务属性;在编程式事务划分中,要准确调用 begin commit 方法。
  • 异常处理 :在事务处理过程中,要对可能出现的异常进行妥善处理。当发生异常时,要及时回滚事务,避免数据不一致。例如在编程式事务划分中,使用 try-catch 块捕获异常并调用 rollback 方法。
  • 性能考虑 :过多的事务操作可能会影响系统性能。在设计事务时,要尽量减少事务的范围和持续时间,避免长时间占用数据库资源。

10. 事务管理的未来趋势与展望

随着 Java 技术的不断发展,事务管理也在不断演进。未来可能会出现以下趋势:

10.1 更简化的事务管理 API

为了进一步降低开发人员的学习成本和提高开发效率,可能会出现更简化的事务管理 API。这些 API 可能会隐藏底层的复杂实现,让开发人员可以更轻松地进行事务管理。

10.2 分布式事务管理的改进

在微服务架构盛行的今天,分布式事务管理变得越来越重要。未来可能会有更高效、更可靠的分布式事务管理解决方案出现,以满足大规模分布式系统的需求。

10.3 与新兴技术的融合

随着人工智能、大数据等新兴技术的发展,事务管理可能会与这些技术进行融合。例如,利用人工智能技术对事务进行智能监控和优化,提高系统的性能和可靠性。

总之,事务管理在 Java EE 应用中起着至关重要的作用。通过深入理解不同的事务划分类型和应用场景,开发人员可以根据具体需求选择合适的事务管理方式,确保应用程序的稳定性和数据的一致性。同时,关注事务管理的未来趋势,有助于开发人员更好地应对不断变化的技术环境。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值