重点来了:事务一致性的深入研究&EJB的全生命周期 | 从开发角度看应用架构5

一、前言

本文仅代表作者的个人观点;

本文的内容仅限于技术探讨,不能作为指导生产环境的素材;

本文素材是红帽公司产品技术和手册;

本文分为系列文章,将会有多篇,初步预计将会有9篇。

二、EJB的生命周期

应用程序中的EJB组件在应用程序服务器内的容器上下文(也就是EJB container)中运行。 EJB容器负责管理EJB的生命周期(创建,执行和销毁)。 每种不同类型的EJB(无状态、有状态、单例、MDB)都有其自己的生命周期。

  1. 有状态session bean

三种不同的状态:

  • Does Not Exist: 有状态EJB还未创建、并且不存在于应用程序服务器内存中。
  • Ready:有状态的EJB准备就绪有状态的EJB(对象)是通过JNDI调用或CDI注入在应用程序服务器内存中创建的,并且已准备好让其客户端调用其业务方法。
  • Passivated:由于有状态的EJB,具有在多个客户端调用中持久化的对象状态,因此应用程序服务器可能会将EJB钝化(停用)到辅助存储以优化内存消耗。 当客户端调用EJB上的任何方法时,它将激活EJB回到就绪状态。 开发人员不具有激活和钝化的任何直接控制权,并且应用程序服务器根据某些算法对其进行透明处理。

2.无状态session Bean生命周期

两种不同的状态(因为是无状态的,所以不会有Passivated的状态):

Does Not Exist: 无状态EJB未创建,并且不存在于应用程序服务器内存中。

Ready: 无状态EJB(对象)通过JNDI调用或CDI注入在应用程序服务器内存中创建,并准备好让客户端调用其业务方法。

3. Singleton会话Bean生命周期

两种不同的状态:

Does Not Exist:单例未被创建,并且不存在于应用程序服务器内存中。

Ready:单启动EJB(单个对象)在启动时或在CDI注入时在应用程序服务器内存中创建,并准备好让其客户端调用其业务方法。

由于在其生命周期内只有一个EJB实例,因此没有概念池。 对bean的并发访问策略可以通过部署描述符或代码级别注释来控制。

因此,EJB的全生命周期描述如下:

Bean Type

Annotation

Description

Stateful Session Bean

@PostConstruct

method is invoked when a bean is created for the first time.

@PreDestroy

method is invoked when a bean is destroyed.

@PostActivate

method is invoked when a bean is loaded to be used after activation.

@PrePassivate

method is invoked when a bean is about to be passivated.

Stateless Session Bean

@PostConstruct

method is invoked when a bean is created for the first time.

@PreDestroy

method is invoked when a bean is removed from the bean pool or is destroyed.

Singleton Session Bean

@PostConstruct

method is invoked when a bean is created for the first time.

@PreDestroy

method is invoked when a bean is destroyed.

@Startup

The application server instantiates the singleton at startup.

三、隐式和显式事务

典型的Java EE企业应用程序,通常会访问、操作一个或多个持久数据存储中的数据,这些数据通常在关系数据库里(RDBMS)。 存储在这些数据库中的关键业务数据通常由多个应用程序同时访问。因此确保数据完整性至关重要。 事务通过控制对数据的并发访问来确保数据的完整性,并确保失败的业务事务不会使系统处于不一致或无效的状态。

事务是一系列必须作为单个原子单位执行的动作。所谓原子性,说白了就是要么事务执行成功,要么失败,不存在成功一半的情况。

接下来,我们举个订单管理系统的例子:

客户下订单(PlaceOrder)时必须执行一系列操作。

  1. 交易从执行saveOrder()方法开始,该方法将订单存储在订单数据库中。
  2. saveOrder()方法调用raisePurchaseOrder()方法,该方法在财务部门维护的另一个数据库中引发采购订单。
  3. 流程转到updateInventory()方法,该方法更新库存数据库,然后使用sendEmail()方法向客户发送电子邮件。
  4. 如果事务中的所有方法都没有任何错误或失败地执行,那么事务将被提交。例如,如果updateInventory()方法失败,则应用程序必须确保参与事务的以前方法(即raisePurchaseOrder()和saveOrder())的操作被逆转,并且整体状态的系统恢复到交易开始时的状态。这种逆转被称为事务回滚。

Java EE标准定义了Java Transaction API(JTA),它为运行在Java EE兼容应用程序服务器上的应用程序提供事务管理。此API为应用程序中的提交和回滚事务提供了一个方便的高级界面。例如,如果Java持久性API(JPA)与JTA一起使用,则开发人员不必在应用程序源码中编写跟踪SQL提交和回滚语句。 JTA API以独立于数据库的方式处理这些操作。

JTA有两种不同的方式来管理Java EE中的事务:

  • 隐式/容器管理事务(Implicit or Container Managed Transaction:CMT):应用程序服务器管理事务边界并自动提交和回滚事务,而开发人员不需要编写代码来管理事务。 这是默认的方式。
  • 显式/Bean管理事务(Explicit or Bean Managed Transaction:BMT):事务由开发人员在Bean级别(EJB中)的代码中进行管理。 开发人员负责明确控制交易范围和边界。

四、隐式事务管理,又称容器管理事务(CMT)

在CMT中,应用程序服务器隐式地在EJB方法开始时开始事务、并在方法结束时提交事务,除非出现错误或异常。在出现错误或者异常的情况下,会自动触发应用程序服务器的回滚。CMT中,不允许在单个bean方法中嵌套事务。开发人员可以使用称为“事务属性”的注释来覆盖方法级别的默认事务行为。

使用CMT的EJB不得使用任何与应用程序服务器的事务范围和边界冲突的JTA API方法。例如,不能使用诸如commit(),setAutoCommit()和rollback()之类的JTA方法,或者java.sql.Connection之类的JDBC类以及来自javax.jms.Session的commit()和rollback()方法。如果需要显式控制事务流,则必须使用Bean管理的事务。另外,使用CMT的EJB不能使用javax.transaction.UserTransaction接口。

设置交易属性

在CMT中,事务属性控制事务的范围,并允许开发人员在EJB中的各个方法级别声明性地管理事务。

例如,我们查看下面的代码,它描述的是一个无状态EJB从另一个无状态EJB调用方法:

@Stateless
public class TodoService {  @Inject
  UserService user;

  public void login(String user, String password) {
    user.authenticate(user,password);
  }
  ...
  }

@Stateless
public class UserService {

  public boolean authenticate(String user, String password) {
    ...
  }
  ...
}

@Inject可以注入包含EJB的任何bean,而@EJB只能注入EJB。

事务属性可以用来控制执行UserService类方法的范围和上下文。

Java EE规范定义了六个事务属性。下面参考上面的代码片段来说明它们:

@TransactionAttribute(TransactionAttributeType.REQUIRED)

如果login()方法在事务中运行并调用UserService类中的authenticate()方法,则authenticate()将在同一事务中执行。如果在调用authenticate()时没有事务,则应用程序服务器在执行authenticate()之前启动新的事务。这是默认的事务属性,除非用其他事务属性注释明确覆盖。

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)

如果login()方法在事务中运行,并调用UserService类中的authenticate()方法,则应用程序服务器在执行authenticate()之前挂起事务并启动新的事务。一旦authenticate()完成执行,控制就移回到login()并且暂停的事务恢复。如果在调用authenticate()时没有事务,则应用程序服务器在执行authenticate()之前启动新的事务。该属性确保我们的方法始终以新事务运行。

@TransactionAttribute(TransactionAttributeType.MANDATORY)

如果login()方法在事务中运行并调用UserService类中的authenticate()方法,则authenticate()将在同一事务中执行。如果在调用authenticate()时没有事务,则应用程序服务器将引发TransactionRequiredException。如果我们希望方法始终在调用客户端的事务上下文中执行,请使用此属性。

@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)

如果login()方法在事务中运行并调用UserService类中的authenticate()方法,则应用程序服务器挂起事务并在没有任何事务上下文的情况下运行authenticate()。一旦authenticate()完成执行,控制就移回到login()并且暂停的事务恢复。如果在调用authenticate()时没有事务,则应用程序服务器在执行authenticate()之前不会启动新的事务。将此属性用于不需要事务的方法。

@TransactionAttribute(TransactionAttributeType.SUPPORTS)

如果login()方法在事务中运行并调用authenticate(),则authenticate()将在同一事务中执行。如果在调用authenticate()时没有事务,则应用程序服务器在执行authenticate()之前不会启动新的事务。

@TransactionAttribute(TransactionAttributeType.NEVER)

如果login()方法在事务中运行并调用UserService类中的authenticate()方法,则应用程序服务器将引发RemoteException。如果在调用authenticate()时没有事务,则应用程序服务器在执行authenticate()之前不会启动新的事务。

设置交易属性

通过使用javax.ejb.TransactionAttribute注释注释EJB类或方法并将其设置为javax.ejb.TransactionAttributeType枚举常量之一来声明事务属性。 如果使用@TransactionAttribute在类级别注释EJB,则指定的属性适用于EJB中的所有方法。 使用@TransactionAttribute注解特定方法仅将该属性应用于该方法。 方法级事务属性声明覆盖类级声明。

考虑以下具有多种方法的EJB类:

@Stateless
public class TodoEJB {

  @TransactionAttribute(TransactionAttributeType.REQUIRED)
  public void createTodo(TodoItem item) {
    ...
  }

  @TransactionAttribute(TransactionAttributeType.NEVER)
  public List<TodoItem> listTodos() {
    ...
  }

  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void logCreateTodo(TodoItem item) {
    ...
  }

createTodo()方法使用@TransactionAttribute(TransactionAttributeType.REQUIRED)事务属性进行注释,而listTodos()方法使用

@TransactionAttribute(TransactionAttributeType.NEVER)事务属性进行注释,因为此方法只执行一个只读操作 该数据库并不插入,删除或更新任何数据。

logCreateTodo()方法使用@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)事务属性进行注释,该属性确保始终使用新事务执行此方法,即使它是从使用其自己的事务上下文执行的另一个方法调用的。

五、显式事务管理,又Bean管理事务(BMT)

在需要对事务的开始和结束时间进行细粒度控制并控制何时执行和回滚的情况下,可以使用Bean Managed Transactions(BMT)。 我们可以使用javax.transaction.UserTransaction接口中的begin(),commit()和rollback()方法来显式地控制事务边界和范围。

在使用BMT的应用程序中,不应使用javax.ejb.EJBContext接口的getRollbackOnly()和setRollbackOnly()方法。 改为使用javax.transaction.UserTransaction接口的getStatus()和rollback()方法。

具有bean管理事务的示例EJB如下所示:

1表示这个类是一个无状态的EJB。

2将此EJB标记为使用@TransactionManagement(TransactionManagementType.BEAN)批注进行管理的Bean。

3注入UserTransaction对象。 这用于在此EJB中开始,提交和回滚事务。

4Begin开始交易。

5如果所有方法都没有任何错误地成功执行,请提交事务。

6如果由于某种故障而出现异常,请执行回滚事务。

六、实验验证:配置Bean管理事务(BMT)

在本实验中,我们使用无状态EJB并将其更新为使用Bean管理的事务。

首先,用IDE导入一个已经存在的maven项目:

查看调用EJB的JSF页面:bean-transactions→src→main→webapp文件夹,然后双击index.xhtml文件。

下面这段源码表示:在Web form提交(在web界面点击subbmit)时调用表达式语言(EL)值#{hello.sayHello()}。

接下来,查看JSF页面使用的请求范围的Hello backing bean。

src/main/java→ com.redhat.training.ui参看: Hello.java,这个源码定义的是一个叫hello的public class:

而该源码中定义对htmlaction bean的定义位置如下:sayhello:

此EJB使用CDI注入PersonService EJB,这是添加事务逻辑的地方。

我们接下来,查看Hello.java CDI注入的bean的class源码:PersonService.java

我们看到,此EJB已标记为@Stateless,但目前不包含任何事务管理。

hello()方法为每个在UI中输入名称并返回包含当前日期和时间的问候的人员在数据库中创建一个新条目(下面标黄部分,显示返回值是hello + 输入值的拼接,以及当前的时间)。

public String hello(String name) {
		try {
			// TODO start a new transaction

			// let's grab the current date and time on the server
			LocalDateTime today = LocalDateTime.now();

			// format it nicely for on-screen display
			DateTimeFormatter format = DateTimeFormatter.ofPattern("MMM dd yyyy hh:mm:ss a");
			String fdate = today.format(format);

			// Create a new Person object and persist to database
			Person p = new Person();
			p.setName(name);
			entityManager.persist(p);

			// respond back with Hello and convert the name to UPPERCASE. Also, send the
			// current time on the server.			
return "Hello " + name.toUpperCase() + "!. " + "Time on the server is: " + fdate;
		} catch(Exception e) {
			//TODO roll-back the transaction

			throw new EJBException(e);
		}
	}
}

接下来,我们把PersonService修改成使用Bean管理的事务,在源码中增加如下一行:

刚增加的注释会阻止容器管理事务并允许EJB手动管理事务。

添加以下代码,以使用资源注入将UserTransaction类的实例注入到EJB中以进行手动事务管理:

@Resource告诉容器分配一个新的事务对象并在运行时将其注入到这个EJB中。

添加以下代码以提交事务:

添加以下代码以在发生异常时回滚事务:

接下来,启动EAP:

使用Maven使用以下命令在JBoss EAP上部署应用程序:

通过浏览器访问刚部署的应用:

输入david,点击submit,输出为Hello DAVID:

而上图中蓝色部分的输出值,其在业务逻辑实现就是:

// current time on the server. return "Hello " + name.toUpperCase() + "!. " + "Time on the server is: " + fdate;

因此,这个实验的整体代码调用关系如下:

魏新宇

  • "大魏分享"运营者、红帽资深解决方案架构师
  • 专注开源云计算、容器及自动化运维在金融行业的推广
  • 拥有MBA、ITIL V3、Cobit5、C-STAR、TOGAF9.1(鉴定级)等管理认证。
  • 拥有红帽RHCE/RHCA、VMware VCP-DCV、VCP-DT、VCP-Network、VCP-Cloud、AIX、HPUX等技术认证。

原文发布于微信公众号 - 大魏分享(david-share)

原文发表时间:2018-06-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏walterlv - 吕毅的博客

将 async/await 异步代码转换为安全的不会死锁的同步代码

发布于 2018-03-16 03:58 更新于 2018-08...

731
来自专栏一名合格java开发的自我修养

javaOOM该分析dump文件而不是看异常log日志原因

应用程序出现OOM异常,你是否仍然通过看日志的方式去排查问题(该方式定位解决问题是大概率的巧合而已)?正确的排查方案是进行dump文件分析,你知道为什么吗?

1424
来自专栏前端杂货铺

node中子进程同步输出

管道 通过“child_process”模块fork出来的子进程都是返回一个ChildProcess对象实例,ChildProcess类比较特殊无法手动创建该对...

2636
来自专栏bluesummer

使用FluentScheduler和IIS预加载在asp.net中实现定时任务管理

1688
来自专栏SpringBoot 核心技术

第四十五章:基于SpringBoot 设计业务逻辑异常统一处理

2754
来自专栏H2Cloud

FFLIB C++ 异步&类型安全&printf风格的日志库

摘要       C++程序的调试一般有调试器、printf、日志文件三种。Linux下的调试器为gdb,关于gdb的使用甚至可以单独用一本书来说明,但是本章并...

3678
来自专栏扎心了老铁

python codis集群客户端(二) - 基于zookeeper对实例创建与摘除

 在这一篇中我们实现了不通过zk来编写codis集群proxys的api, 如果codis集群暴露zk给你的话,那么就方便了,探活和故障摘除与恢复codis集群...

4165
来自专栏电光石火

MySQL按天,按周,按月,按时间段统计

自己做过MySQL按天,按周,按月,按时间段统计,但是不怎么满意,后来找到这位大神的博客,转载一下,谢谢这位博主的分享

875
来自专栏韩东吉的Unity杂货铺

零基础入门 42:更新Unity2017快捷键清除日志

Hello,之前在零基础入门系列里,有发过关于快捷键清除日志的文章,但是当时的Unity版本是Unity5.5,很多人和我说用起来都还蛮方便,但是随着2017的...

562
来自专栏简单聊聊Spark

Spark性能调优篇四之使用Kryo进行序列化操作

        接着上一篇文章,今天介绍一下通过使用Kryo这个东东来进一步降低网络IO的传输量和内存的占用率。在介绍Kryo之前,接下来我们先来对比一下默认的...

893

扫码关注云+社区