spring事务传播深入解析

由于最近做项目的过程中,事务控制有点问题,所以决定对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`)

之后搭建测试环境,项目结构如下图



下面是配置事务的xml

<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的内部机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值