虽然网上关于Spring的事务传播行为的博客已经很多了,但是符合通俗易懂的却不多。我写这篇博客就是试图通俗地描述Spring事务的传播行为是什么,Spring事务的传播行为有哪些类型以及它们之间有什么不同。
为什么需要使用到事务?
使用事务是为了确保事务中的所有操作在数据库中要么全部正确反映,要么全部不反映。举个例子来说,就是当你网上买商品时,减少你账户余额和减少商品库存这两个动作应该是要全部发生或者全部都不发生。怎么才能保证这一点?答案是,使用事务。
如果不使用事务,
则执行下面的 purchaseBook 方法后,就可能会出现商品库存减少了但是用户余额却没有减少的情况,这对于商家来说是不能接受的。
public void purchaseBook(Integer userId, String isbn, int num) {
//1. 获取书的单价
double price = bookShopDao.findBookPriceByIsbn(isbn);
double amounts = AmountTransUtil.mul(price, num);
//2. 更新数的库存
bookShopDao.reduceBookStock(isbn, num);
//3. 更新用户余额
bookShopDao.reduceUserAccount(userId, amounts);
}
/**
* 减少isbn对应的书籍的数量
* @param isbn
*/
public void reduceBookStock(String isbn, int num) {
String sql2 = "select stock from book_stock where isbn = ?";
Integer stock = jdbcTemplate.queryForObject(sql2, Integer.class, isbn);
if (stock == null)
throw new BookStockException("找不到该书本库存的信息,请确认输入的isbn是否准确");
if (stock.intValue() <= 0)
throw new BookStockException("库存不足");
String sql = "update book_stock set stock = stock - ? where isbn = ? and stock >= ?";
int updateCount = jdbcTemplate.update(sql, num, isbn, num);
if (updateCount != 1) {
throw new BookStockException("库存不足");
}
}
/**
* 更新用户余额(将其余额减去花费的钱)
* @param id
* @param amounts 必须为大于0的数字
*/
public void reduceUserAccount(int id, double amounts) {
String sql2 = "select balance from account where id = ?";
double balance = jdbcTemplate.queryForObject(sql2, Double.class, id);
if (balance < amounts)
throw new UserAccountException("余额不足");
String sql = "update account set balance = balance - ? where id = ? and balance >= ?";
int updateCount = jdbcTemplate.update(sql, amounts, id, amounts);
if (updateCount != 1) {
throw new UserAccountException("余额不足或者账户不存在");
}
}
如何使用事务?
这里只介绍基于注解的Spring事务。在Spring配置文件上配置aop、transactionManager,并开启事务注解,然后只需在purchaseBook方法上面一行添加”@Transactional”。
<!-- 使用 AspectJ 注解起作用:自动为匹配的类生成代理对象 -->
<!--基于接口的代理,使用jdk实现 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!-- jdbc start -->
<!-- 导入资源文件 -->
<context:property-placeholder location="classpath:app.properties" />
<!-- 配置 C3P0 数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}" />
<property name="password" value="${jdbc.password}" />
<property name="jdbcUrl" value="${jdbc.jdbcUrl}" />
<property name="driverClass" value="${jdbc.driverClass}" />
<property name="initialPoolSize" value="${jdbc.initPoolSize}" />
<property name="maxPoolSize" value="${jdbc.maxPoolSize}" />
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource" />
</bean>
<!-- jdbc end -->
<!-- tx start -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
<!-- tx end -->
@Transactional
public void purchaseBook(Integer userId, String isbn, int num) {
//省略内容
}
注意,上面的 reduceBookStock 和 reduceUserAccount 方法抛出的异常必须是
RuntimeException的子类。因为Spring事务的处理机制就是当事务方法中抛出RuntimeException 后,就会回滚事务,回滚后,将原异常重新抛出。
Spring事务的传播行为是什么呢?
Spring事务的传播行为定义了外层方法[注1]调用带有@Transactional的当前方法时,事务如何进行传播的规则,共有7种类型:
(1)PROPAGATION.REQUIRED(默认)
如果外层方法有事务,则当前方法加入外层事务;如果外层方法没有事务,则当前方法新建一个事务。
(2)PROPAGATION.REQUIRES_NEW
当前方法总是开启一个新的事务,如果外层方法有事务,则将外层事务挂起,先执行当前方法的事务(外层事务和当前方法的事务是两个不同的事务)。
当当前方法发生回滚并抛出RuntimeException时,如果该异常被捕获,则外层方法的事务不会因此回滚;如果该异常没有被捕获,则外层方法的事务就会因此而回滚。
当外层方法发生回滚时,如果其回滚发生在当前方法前,则当前方法得不到执行;如果其回滚发生在当前方法之后,则当前方法不会因此而回滚。
(3)PROPAGATION.NESTED
如果外层方法没事务,则当前方法新建一个事务;如果外层方法有事务,则把当前方法当成外层事务的一部分(使用savepoint实现),外层方法事务的rolback或者commit都会影响当前方法[注2],而当前方法的rolback不会导致外层事务回滚,除非rollback过程抛出了RuntimeException且该异常没有被捕获。
(4)PROPAGATION.SUPPORTS
如果外层方法没事务,那当前方法就按普通方法执行;如果外层方法有事务,则使用外层方法的事务。
(5)PROPAGATION.NOT_SUPPORTED
当前方法总是非事务地执行,如果外层方法有事务则把事务挂起,当前方法还是以普通方法执行。
(6)PROPAGATION.NEVER
如果外层方法没事务,那当前方法就按普通方法执行;如果外层方法有事务,则当前方法抛出异常。
(7)PROPAGATION.MANDATORY
如果外层方法没事务,则当前方法就会抛出异常;如果外层方法有事务,则当前方法使用外层事务。
下面是这几种传播行为的使用方式。其中purchaseBook1方法和purchaseBook2方法是等效的,因为propagation的默认值就是Propagation.REQUIRED。
@Transactional
public void purchaseBook1(Integer userId, String isbn, int num) {
//省略内容
}
@Transactional(propagation=Propagation.REQUIRED)
public void purchaseBook2(Integer userId, String isbn, int num) {
//省略内容
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void purchaseBook3(Integer userId, String isbn, int num) {
//省略内容
}
// ...
其他的一些注意事项:
- @Transactional 注解应该只被应用到 public 可见度的方法上,否则不会生效。
- 当@Transactional 注解在接口方法上,只有代理类是基于接口的代理[注3]时,Transactional 注解才起作用的。通常,建议将@Transactional 注解在具体类的方法上。
注1:外层方法并不单指当前方法的上一级方法。如果方法X调用方法A,方法A调用当前方法,那么方法X和A都是外层方法。
注2:如果外层事务rollback了,当前方法中的数据库操作就不会反映在数据库中;如果外层事务commit了,当前方法中的数据库操作就会反映在数据库中。
注3:
当你在Spring中配置了 <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
时,那么代理类就是基于接口的代理。
当你在Spring中配置了 <aop:aspectj-autoproxy proxy-target-class="true"/>
时,那么代理类就是基于类的代理。