由于最近做项目的过程中,事务控制有点问题,所以决定对spring的事务传播特性进行研究一番,如有不对的地方还请指正。
我们先来回顾下spring事务传播的7种行为。
PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
PROPAGATION_NESTED--支持当前事务,如果当前有事务,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。
PROPAGATION_SUPPORTS--支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY--支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW--新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED--以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER--以非事务方式执行,如果当前存在事务,则抛出异常。
此次主要测试传播方式是REQUIRED的情况,因为REQUIRED方式在实际中应用比较多。隔离级别就用mysql默认的隔离级别,暂时不去探讨。测试环境是spring+hibernate+mysql,
事务的控制方式是声明式事务,控制粒度较粗。首先建立三张表,sql如下,
CREATE TABLE `book` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
)
CREATE TABLE `express` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
)
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`age` int(11) DEFAULT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
)
之后搭建测试环境,项目结构如下图
<tx:adviceid="transactionAdvice"transaction-manager="transactionManager">
<tx:attributes>
<tx:methodname="add*"propagation="REQUIRESD"/>
<tx:methodname="delete*"propagation="REQUIRED"/>
<tx:methodname="update*"propagation="REQUIRED"/>
<tx:methodname="select*"propagation="REQUIRED"read-only="true"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:advisorpointcut="execution(* com.jhw.service..*Impl.*(..))"advice-ref="transactionAdvice"/>
</aop:config>
此时测试项目搭建完毕,开始进行测试。我们探讨的主要是事务的传播行为,事务控制在service层,只有service层相互调用才有意义。
首先我们来看propagation="REQUIRED"下的添加数据的表现。BookServiceImpl代码,此时mysql的表类型是innoDB.
public void addBook(Book book) {
bookDao.add(book);
thrownew RuntimeException("throw runtime exception by xiaoge");
}
UserServiceImpl的代码如下,
publicvoid addUser(Useruser) {
Book book =new Book();
book.setName("菜根谭");
bookService.addBook(book);
System.out.println("已经执行到这里了");
userDao.add(user);
}
JUnit的代码,
@org.junit.Test
publicvoid test3(){
User user =new User();
user.setAge(11);
user.setName("xiaoge");
userService.addUser(user);
}
下面执行测试代码,控制台输出如下,
可以看到有插入到book表的一条sql语句,由于在BookServiceImpl中抛出了RuntimeException异常,可以看见事物紧接着回滚了,我们再去数据库中看看。
发现user和book中都没有数据。原因是
bookService.addBook(book);这句虽然执行了,但是内部发生RuntimeException异常导致事物回滚,所以book这张表没有插入数据。由于红框处没有捕获异常,所以程序中断,导致后面两句没有执行,所以我们在控制台也没有看见已经执行到这里了这句话,Usre自然也不会插入数据库。如果我们此时把数据库的表类型改成MyLSAM,看看情况是不是一样的,再次执行JUnit,控制台输出如下,
看起来和之前控制台的输出没什么不一样的地方,我们看看数据库,
我们看到User表中依然没数据,但是book表居然有了一条数据。我们来分析一下为什么,user表中没数据是因为userserviceImpl中的addUser()方法发生异常之后userDao.add(user);这句话执行不到,这个比较好理解。但是为什么明明发生异常了book表中还会有数据呢,目前只有一种解释,就是表类型如果是MyLSAM类型的话不支持事物的回滚。我们把数据删除并且把表类型换成innoDB继续下面的测试。
那我们在异常处捕获异常看看发生什么,
publicvoid addBook(Bookbook) {
bookDao.add(book);
try {
thrownew RuntimeException("throw runtime exception by xiaoge");
} catch (Exceptione) {
// TODO: handle exception
e.printStackTrace();
}
}
此时进数据库看发现两张表都已经有数据,异常被捕获事物不会回滚。删除数据继续进行测试,我们把代码稍微调整下,BookserviceImpl代码变成
publicvoid addBook(Bookbook) {
bookDao.add(book);
}
UserServiceImpl的代码变成
public void addUser(User user) {
Book book =new Book();
book.setName("菜根谭");
bookService.addBook(book);
System.out.println("已经执行到这里了");
userDao.add(user);
thrownew RuntimeException("throw runtime exception by xiaoge");
}
JUnit的测试代码不变,我们运行一下,控制台输出如下,数据库中book和user两张表也是没有数据的,这个比较好理解,代码运行到最后抛出异常导致事物回滚。那我们看下propagation="REQUIRES_NEW"的传播方式在这种情况下如何表现,此处主要是为了对比。事物的配置调整为
<tx:adviceid="transactionAdvice"transaction-manager="transactionManager">
<tx:attributes>
<tx:methodname="add*"propagation="REQUIRES_NEW"/>
<tx:methodname="delete*"propagation="REQUIRED"/>
<tx:methodname="update*"propagation="REQUIRED"/>
<tx:methodname="select*"propagation="REQUIRED"read-only="true"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:advisorpointcut="execution(* com.jhw.service..*Impl.*(..))"advice-ref="transactionAdvice"/>
</aop:config>
其他代码不变运行JUnit,进入数据库查看数据,发现user表中没有数据,book表中存在一条数据,我们来分析一下造成这种现象的原因。总结:REQUIRED的传播行为下,JUnit首先调用的是UserServiceImpl的addUser()方法,此时并没有事物,所以会生成一个事物,紧接着addUser()方法内会调用BookServiceImpl的addBook()方法,由于之前生成了事物,事物传播给了addBook()方法,addBook()方法运行完之后进入addUser()方法,最后抛出异常,其实整个运行过程中都只存在一个事物,所以事物回滚之后两个表的数据都不会插入进去。
REQUIRES_NEW的传播行为下,JUnit首先调用的是UserServiceImpl的addUser()方法,此时并没有事物,所以会生成一个事物,紧接着addUser()方法内会调用BookServiceImpl的addBook()方法,由于传播行为是REQUIRES_NEW,所以之前的事物会挂起,新建一个事物。此时运行过程中会产生两个事物,我们把addUser()方法的事物叫做事物1,addBook()方法产生的事物叫做事物2.addBook()方法在运行过程中并没有产生异常,事物2不会回滚,所以book表中会产生数据。addUser()方法最后抛出异常,造成事物1回滚,所以user表中没有产生数据。
好了,上面主要说的是spring事物传播,下面我们看下删除数据时同一个事物和不同的事物会发生什么。首先在express表中加数据
ExpressServiceImpl的代码如下
publicvoid deleteExpress(longid) {
expressDao.delete(id);
Express express =expressDao.getById(id);
if (express!=null) {
System.out.println("找到你小子了");
}
else{
System.out.println("在当前事务中这小子不见了");
}
thrownew RuntimeException("new runtime exception by xiaoge");
}
Junit的测试代码
@org.junit.Test
publicvoid test4()throws Exception{
expressService.deleteExpress(1L);
}
运行一下,控制台输出如下
最后事物回滚,id为1的这条数据还是存在数据库中的。此时我们把deleteExpress()方法上打上断点,进入debug模式,如图,
注意此时expressDao.delete(id);这句话已经运行过了,我们用另外一个测试类运行junit,
@Test
publicvoid test1(){
Express express =expressService.selectExpress(1L);
System.out.println(express.getCode()+"*********************");
}
去查找id为1的这条数据,控制台输出
这条数据还在。
总结:通过测试结果表明,hibernate查询数据的时候并不是简单的从数据库中查询数据。删除一条数据的情况下,如果此时事务没有提交,在当前事务下是查询不到此条数据的。但是在其他事务中或者说是线程中是可以查询到此条数据的。由于hibernate的session是getCurrentSesson()方式获得的,所以session是与当前线程绑定的,事务也是与当前线程绑定的。删除数据的时候,虽然事务没有提交,也就是数据库中还存在此条数据,但是在hibernate中的session已经把此条数据删除了,所以当前事务中是查询不到的。更新数据的时候与上述情况也是一样的。如果对上述情况感兴趣的可以深入的去了解hibernate的内部机制。