以下转自blog.csdn.net/jnqqls/的系列博客EJB,写的很好!
在介绍EJB的事务之前,我们很有必要再对事务的基本概念再做一个回顾和了解,通过对基础概念的掌握能够帮助我们深一步的去理解EJB中的事务.
事务是访问数据库的一个操作序列,数据库应用系统通过事务集来完成对数据库的存取。简单点说就是事务的正确执行使得数据库从一种状态转换成另一种状态。
事务本身有着自己的原则,而且必须遵循ACID原则.ACID是原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)的缩写。
原子性:
即不可分割性,事务要么全部被执行,要么就全部不被执行。如果事务的所有子事务全部提交成功,则所有的数据库操作被提交,数据库状态发生转换;如果有子事务失败,则其他子事务的数据库操作被回滚,即数据库回到事务执行前的状态,不会发生状态转换。
一致性:
事务的执行使得数据库从一种正确状态转换成另一种正确状态。
隔离性:
在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务,即在事务正确提交之前,它可能的结果不应显示给任何其他事务。
持久性:
事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故障,事务的处理结果也会得到保存。
事务在什么时候开始运行呢? 当我们执行一条SQL语句的时候,事务就会自己启动. 启动之后必须让启动事务的用户或者应用 程序来显示的终止此事务,除非你用了自动提交 Automatic commit.这种情况是每个SQL语句就是一个单独的事务.这里涉及到数据库系统所支撑的两种事务模式,一种事务模式就是我们刚才所说的自动提交模式.当数据库执行完一个Sql语句之后,会自动提交事务,另一种是手动提交模式,也就是事务必须由数据库客户端程序制定事务开始 边界和结束边界.
我们通过执行Commit或RollBack语句来终止事务。
Commit表示事务正确的提交,RollBack表示事务被回滚,具体来解释就是当执行Commit语句时,自从事务启动以来对数据库所做的一切更改就成为永久性的了--即它们被写到磁盘。当执行RollBack语句时,自从事务启动以来对数据库所做的一切更改都被撤销,并且数据库返回到事务开始之前所处的状态。不管是哪种情况,数据库在事务完成时都保证能回到一致状态。
一般运行一个事务的时候基本上没有什么问题,但是当运行多个事务的时候,并且这些事务同时访问数据库中相同的数据,便会产生各种并发问题.如何解决这些问题,我们需要采取一定的隔离机制.
~~~~~~~~~~~~~~
在上篇文章我们提到,当运行多个事务的时候,并且这些事务方式数据库中的相同数据会出现一系列的并发问题,这些问题汇总起来总共有以下几类.
丢失更新:撤销一个事务时,把其他事务已提交的更新数据覆盖。
脏读:一个事务读到另一个事务为提交的更新数据。
不可重复读:一个事务读到另一个事务已提交的更新数据。
幻像读:一个事务读到另一个事务已提交的新插入的数据。
丢失更新:
当多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。每个事务都不知道其它事务的存在。最后的更新将重写由其它事务所做的更新,这将导致数据丢失。
举一个小例子来帮助进一步理解:当事务A和事务B同时修改数据库表的某行的值,
1.事务A将数值改为10000并提交
2.事务B将数值改为20000并提交。这时数据的值为20000,事务A所做的更新将会丢失。
解决办法:对行加锁,只允许并发一个更新事务。
脏读:
当第二个事务选择其它事务正在更新的行时,会发生未确认的相关性问题。第二个事务正在读取的数据还没有确认并且可能由更新此行的事务所更改。
1.甲的原工资为10000, 乙将甲的工资改为了15000,此时但未提交事务.
2.甲读取自己的工资 ,发现自己的工资变为了15000!
3.而乙发现操作有误,回滚了事务,甲的工资又变为了10000 像这样,甲记取的工资数15000是一个脏数据。
解决办法:如果在第一个事务提交前,任何其他事务不可读取其修改过的值,则可以避免该问题。
非重复读:
当第二个事务多次访问同一行而且每次读取不同的数据时,会发生不一致的分析问题。不一致的分析与未确认的相关性类似,因为其它事务也是正在更改第二个事务正在读取的数据。然而,在不一致的分析中,第二个事务读取的数据是由已进行了更改的事务提交的。而且,不一致的分析涉及多次(两次或更多)读取同一行,而且每次信息都由其它事务更改;因而该行被非重复读取。
在一个事务中前后两次读取的结果并不致,导致了不可重复读
1.在事务1中,甲读取了自己的工资为10000,操作并没有完成
2.在事务2中,这时乙修改了甲的工资为50000,并提交了事务.
3.在事务1中,甲再次读取自己的工资时,工资变为了50000
解决办法:如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。
幻像读 :
当对某行执行插入或删除操作,而该行属于某个事务正在读取的行的范围时,会发生幻像读问题。事务第一次读的行范围显示出其中一行已不复存在于第二次读或后续读中,因为该行已被其它事务删除。同样,由于其它事务的插入操作,事务的第二次或后续读显示有一行已不存在于原始读中。
目前工资为10000的员工有5人。
1.事务1,读取所有工资为10000的员工。
2.这时事务2向表插入了一条员工记录,工资也为10000
3.事务1再次读取所有工资为10000的员工共读取到了6条记录,
解决办法:如果在操作事务完成数据处理之前,任何其他事务都不可以添加新数据,则可避免该问题。
上面所产生的并发问题我们都基本上给出了解决的方案或者建议,也就是用锁.锁机制能有效地解决并发事务时的各种问题,但是也会影响到并发的性能。数据库系统提供了相应的解决方案,我们将在下一篇文章中来了解隔离级别.
~~~~~~~~~~~~
在上文中我们提到,锁机制能有效地解决并发事务时的各种问题,但是也会影响到并发的性能。数据库系统提供了4种可选的事务隔离级别,它们是
1.Read Uncommited:读未提交的数据
2.Read commited:读已提交的数据
3.Repeateble Read:可重复读
4.Serialable:序列化
ReadUncommited: 该隔离级别读取数据时不使用任何锁。可能会出现脏读,不可重复读,和虚读的问题。
Readcommited:返回的是读取时间点之前已提交的数据,因此可以避免脏读。但重复读数据时,返回的数据和读取时间点有关,因此会重现不可重复读,另外还会出现虚读现象。
RepeatableRead:该隔离级别能够保证重复读,可以避免脏读和不可重复读问题。
Serializable:该隔离级别能够避免脏读,不可重复读和虚读现象,是最严格的隔离级别。
上面四种隔离级别,从1-4隔离级别越来越严格,数据安全和真实性越来越高,但并发性能越来越低。所以选择什么样的隔离级别应根据应用的具体要求而定。对于多数应用程序,可以有优先考虑把数据库系统的隔离级别设为ReadCommited,它能够避免脏读,而且具有较好的并发性能。尽管它会导致不可重复读、幻像读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
A.悲观锁:指在应用程序中显示的为数据资源加锁。尽管能防止丢失更新和不可重复读这类并发问题,但是它会影响并发性能,因此应该谨慎地使用。
B.乐观锁:乐观锁假定当前事务操作数据资源时,不会有其他事务同时访问该数据资源,因此完全依靠数据库的隔离级别来自动管理锁的工作。应用程序采用版本控制手段来避免可能出现的并发问题。
~~~~~~~~~~
在我们对事务的基本概念以及出现的问题和隔离级别有进一步的了解之后,接下来看看EJB是如何进行事务管理.
在EJB中有两种使用事务的方式。第一种方式通过容器管理的事务,叫CMT(Container-Managed Transaction),另一种通过Bean管理的事务叫BMT(Bean-Managed Transaction)。
如果使用容器来管理事务,那么EJB组件就不需要显式地给出begin 、commit 、abort 语句,EJB 容器会替我们考虑这些内容。EJB 容器会依据EJB组件提供者指定的事务行为来界定相应的事务边界。
在使用容器管理事务时,EJB 容器会拦截客户请求,并自动为EJB组建启动新的事务,也就是说,容器会通过begin 语句调用底层事务系统,从而启动事务。随后,容器会将业务请求委派给EJB组件,组件中的业务操作将运行在这一事务中。处于事务中的EJB 组件能够执行任何业务逻辑,如写入数据库、发送异步信息、调用其他的EJB组件等。一旦在处理业务过程中出现问题,则EJB 组建需要通知EJB 容器去回滚事务。当EJB 组建完成业务处理后,会将控制权交回给EJB 容器。随后,EJB容器能够通过commit 或abort 语句调用底层事务系统。
我们可以使用@TransactionAttribute注释或部署描述符来指定事务属性。EJB 容器通过分析事务属性便能够知道如何处理EJB 组件的事务需求。
如果用简短的话总结上面的内容就是,用CMT管理事务,事务都是被容器管理的,开发人员不需要对事务进行管理,需要做的就是配置事务属性.
EJB 事务属性的取值有以下几种:
(1 )Required ,如果EJB组件必须总是运行在事务中,则应该使用Required 模式。如果已经有事务在运行,则EJB 组件参与其中;如果没有事务运行,则EJB 容器会为EJB组件启动新的事务。
Required 是默认和最常使用的事务属性值。这个值指定必须在事务之内调用EJB方法。如果从非事务性客户端调用方法,那么容器会在调用方法之前开始事务,并且在方法返回时结束事务。另一方面,如果调用者从事务性上下文调用方法,那么方法会联结已有事务。在从客户段传播事务的情况下,如果我们的方法表示应该回滚事务,那么容器不仅回回滚整个事务,而且会向客户端抛出异常,从而让客户端知道它开始的事务已经被另一个方法回滚了。
(2 )Requires_New,当客户调用EJB 时,如果总是希望启动新的事务,则应该使用RequiresNew 事务属性,如果客户在调用EJB组件时已经存在事务,则当前事务会被挂起,进而容器启动新的事务,并将调用请求委派给EJB组件。也就是说,如果客户端已经有了事务,那么它暂停该事务,知道方法返回位置,新事务是成功还是失败都不会影响客户端已有的事务。EJB组件执行相应的业务操作,容器会提交或回滚事务,最终容器将恢复原有的事务,当然,如果客户在调用EJB 组件时不存在事务,则不需要执行事务的挂起或恢复操作。
RequiresNew 事务属性非常有用。如果EJB 组件需要事务的ACID属性,并且将EJB 组件运行在单个独立的工作单元中,从而不会将其他外部逻辑也包括在当前的事务中,则必须使用RequiredNew事务属性。如果需要事务,但是不希望事务的回滚影响客户端,就应该使用它。另外,当不希望客户端的回滚影响你的时候,也应该使用这个值。
(3 )Supports ,如果某个EJB组件使用了Supports 事务属性,则只有调用它的客户已经启用了事务时,这一EJB 组件才会运行在事务中。如果客户并没有运行在事务中,则EJB组建也不会运行在事务中。Supports 同Required 事务属性很相似,但是,Required 要求EJB 组件必须运行在事务中。如果使用Support事务属性,EJB 组建很可能没有运行在事务中。
(4 )Mandatory ,Mandatory事务属性要求调用EJB 组件的客户必须已经运行在事务中。如果从非事务性客户端调用使用Mandatory 属性的EJB方法,那么客户将接受到系统抛出的javax.ejb.EJBTransactionRequiredException 异常。EJB 组件使用Mandatory事务属性是非常安全的,它能够保证EJB 组建运行在事务中。如果客户没有运行在事务中,则不能够调用到应用了Mandatory 事务属性的EJB组件。但是,Mandatory 事务属性要求第3 方(及客户)在调用EJB 组件前必须启动了事务。EJB 容器并不会为Mandatory事务属性自动启动新事务,这是同Support 事务属性的最主要区别。
(5 )NotSupported ,如果EJB组件使用了NotSupport事务属性,它根本不会参与到事务中。如果调用者使用相关联的事务调用方法,容器就会暂停事务,调用方法,然后再方法返回时恢复事务。通常,此属性只用于非实物性的自动确认模式中,支持JMS提供者的MDB 。
(6 )Never ,如果EJB组件使用Never 事务属性,它就不能够参与到事务中,而且,如果调用它的客户已经处于事务中,则容器会将javax.ejb.EJBException异常抛给客户。
当然在上面所列出的属性中我们最常用的还是Required具体可以看下面的一段片段代码:
- import javax.ejb.Remote;
- import javax.ejb.Stateless;
- import javax.ejb.TransactionAttribute;
- import javax.ejb.TransactionAttributeType;
- import javax.ejb.TransactionManagement;
- import javax.ejb.TransactionManagementType;
- import javax.persistence.EntityManager;
- import javax.persistence.PersistenceContext;
- @Stateless(name = "UserManager")
- @Remote
- @TransactionManagement(TransactionManagementType.CONTAINER)
- public class UserManagerBean implements UserManager {
- @PersistenceContext
- private EntityManager em ;
- @TransactionAttribute(TransactionAttributeType.REQUIRED)
- public void addUser(String name) {
- User s = new User();
- s.setName(name);
- em.persist(s);
- System.out.println("服务器端执行成功:保存姓名" + name);
- }
- }
上面的例子中
@TransactionManagement(TransactionManagementType.CONTAINER)
表示指定事务的类型。如果省略,默认为CMT方式。
@TransactionAttribute(TransactionAttributeType.REQUIRED)
通知容器如何管理事务,事务的属性控制了事务的使用范围,因为事务之间的关系非常的复杂,这个属性主要是用来处理事务与事务之间怎样来处理的的问题。
以上便是EJB用容器来进行事务管理,下一篇我们将来介绍BMT.
~~~~~~~~~~~~~~~~~
在上一篇文章中我们对EJB的事物管理有了一个整体的认识,同时对EJB的容器事务管理用了一个例子进行展示.接下来是EJB的另一种管理事务的方式Bean管理.也用一个例子来进行展示和说明.
Bean本身来管理事务
建立实体类
- package com.tgb.jpa;
- import javax.persistence.Entity;
- import javax.persistence.GeneratedValue;
- import javax.persistence.Id;
- import javax.persistence.Table;
- @Entity
- @Table(name="T_Student")
- public class Student {
- @Id
- @GeneratedValue
- private int id ;
- public int getId() {
- return id;
- }
- public void setId(int id) {
- this.id = id;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- private String name;
- }
建立接口
- package com.tgb.jpa;
- public interface StudentManager {
- public void addStudent(String name);
建立EJB,此处需要注意的一些是增加注解:
@TransactionManagement(TransactionManagementType.BEAN)表示本EJB的事务管理方式是通过Bean的方式来进行自身的管理,脱离了容器的管理,需要在Bean内自己进行事务的管理.如下:
- package com.tgb.jpa;
- import javax.annotation.Resource;
- import javax.ejb.Remote;
- import javax.ejb.Stateless;
- import javax.ejb.TransactionManagement;
- import javax.ejb.TransactionManagementType;
- import javax.persistence.EntityManager;
- import javax.persistence.PersistenceContext;
- import javax.transaction.UserTransaction;
- @Stateless(name = "StudentManager")
- @Remote
- @TransactionManagement(TransactionManagementType.BEAN)
- public class StudentManagerBean implements StudentManager {
- @PersistenceContext
- private EntityManager em;
- @Resource
- private UserTransaction ut;
- @Override
- public void addStudent(String name) {
- try {
- ut.begin();
- Student s = new Student();
- s.setName(name);
- em.persist(s);
- System.out.println("服务器端执行成功:保存姓名"+name);
- ut.commit();
- } catch (Exception e) {
- try {
- e.printStackTrace();
- ut.rollback();
- } catch (Exception e1) {
- e1.printStackTrace();
- }
- }
- }
- }
建立客户端进行测试EJB,增加Jndi.properties文件;
- package com.tgb.jpa;
- import javax.naming.InitialContext;
- import javax.naming.NamingException;
- public class StudentManagerClient {
- /**
- * @param args
- * @throws NamingException
- */
- public static void main(String[] args) throws NamingException {
- InitialContext context = new InitialContext();
- StudentManager sm =(StudentManager) context.lookup("StudentManager/remote");
- sm.addStudent("jiqing");
- System.out.println("客户端执行成功");
- }
- }
小结:
用Bean管理事物我们需要要自己确定事务界限,可以调用javax.transaction.UserTransaction接口的begin、commit和rollback方法来确定事务界限,该接口只能在SessionBean中使用,实体Bean不允许使用用户自定义的。上面的例子示范了UserTransaction的用法。begin和commit方法确定了数据库操作的事务界限,如果操作失败则调用rollback回滚事务并抛出EJBException异常。
容器管理事务不支持嵌套事务,Bean管理事务支持嵌套事务,所以需要嵌套事务时可以采用Bean管理事务,使用Bean管理事务需要我们显式的设置事务的属性,需要我们自己编码管理事务.