事务的意思是原子事务,即要么完整的完成这个事务,要么在报错时使系统的状态回到执行事务之前。这么做的意义是在进行事务处理时会涉及到持久层的访问和读写,举个简单的例子,一个简单的转账操作,A转100块给B,那么对应的数据库操作是A的账户余额减100,B的账户余额加100,这两个步骤构成了简单的转账的事务。这两个步骤对数据库的读写肯定会存在时间的先后,那么万一A的账户余额减少了100后在进行B的账户余额加100时,出现了错误怎么办呢,例如网络通信断开或者数据库访问错误等,这时候这个事务就出现了错误,我们应该对整个事务进行回退将系统恢复到事务执行之前,即让A和B的账户余额都回到转账之前的状态,然后再告知用户转账操作失败。在Spring中,它为我们提供了配置事务的方法,可以实现事务的原子性。
这里用的例子是从书店购书的例子,购书这个事务分两个步骤,一是书本库存减一,二是用户余额扣去书费。当书本库存不足时,需要对这个事务进行回退,当用户余额不足时也要进行回退。类的关系如下:
数据库的内容如下:
工程的目录如下:
一、使用注解的方式进行事务管理
类的代码如下:
package tx; public interface BookShopDao { //根据书号获取书的单价 public int findBookPriceByIsbn(String isbn); //更新书的库存,使对应库存减一 public void updateBookStock(String isbn); //更新用户的账户余额,使username 的balance减去price public void updataUserAccount(String username, int price); } package tx; import javax.security.auth.login.AccountException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository("bookShopDao") public class BookShopDaoImpl implements BookShopDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public int findBookPriceByIsbn(String isbn) { // TODO Auto-generated method stub String sql = "SELECT price FROM book WHERE isbn = ?"; return jdbcTemplate.queryForObject(sql, Integer.class, isbn); } @Override public void updateBookStock(String isbn) { // TODO Auto-generated method stub //检测书的库存是否足够,若不够则抛出异常 String sql2 = "SELECT stock FROM book_stock WHERE isbn = ?"; int stock = jdbcTemplate.queryForObject(sql2, Integer.class, isbn); if (stock == 0) { throw new BookStockException("库存不足"); } String sql = "UPDATE book_stock SET stock = stock - 1 WHERE isbn = ?"; jdbcTemplate.update(sql, isbn); } @Override public void updataUserAccount(String username, int price) { // TODO Auto-generated method stub //检测余额是否足够,若不够则抛出异常 String sql2 = "SELECT balance FROM account WHERE username = ?"; int balance = jdbcTemplate.queryForObject(sql2, Integer.class, username); if (balance < price) { throw new UserAccountException("余额不足"); } String sql = "UPDATE account SET balance = balance - ? WHERE username = ?"; jdbcTemplate.update(sql, price, username); } } package tx; public interface BookShopService { public void purchase(String username, String isbn); } package tx; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.interceptor.NoRollbackRuleAttribute; @Service("bookShopService") public class BookShopServiceImpl implements BookShopService { @Autowired private BookShopDao bookShopDao; //添加事物注解 //1.使用propagation指定事务的传播行为,即当前的事务方法被另外一个事务方法调用时 //如何使用事务,默认取值为REQUIRED,即使用调用方法的事务 //REQUIRED_NEW创建新的事务执行 //2.使用isolation指定事务的隔离级别,最常用的取值为READ_COMMITTED //3.默认情况下Spring的声明式事务对所有的运行时异常进行回滚,也可以通过对应的属性进行设置 //noRollBackFor不对什么异常回滚 :noRollbackFor={UserAccountException.class} //4.使用readOnly指定事务是否为只读,优化数据库引擎可以不加锁,加快速度 //readOnly=true //5.timeout单位是秒,在强制回滚之前事务可以占用的时间 @Transactional(propagation=Propagation.REQUIRES_NEW, isolation=Isolation.READ_COMMITTED, timeout=3) @Override public void purchase(String username, String isbn) { // TODO Auto-generated method stub /*模拟事务超时 try { Thread.sleep(4000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } */ //1.获取书的单价 int price = bookShopDao.findBookPriceByIsbn(isbn); //2.更新书的库存 bookShopDao.updateBookStock(isbn); //3.更新用户余额 bookShopDao.updataUserAccount(username, price); } } 然后是两个异常类分别是库存不足和余额不足 package tx; public class BookStockException extends RuntimeException { private static final long serialVersionUID = 1L; public BookStockException() { super(); // TODO Auto-generated constructor stub } public BookStockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); // TODO Auto-generated constructor stub } public BookStockException(String message, Throwable cause) { super(message, cause); // TODO Auto-generated constructor stub } public BookStockException(String message) { super(message); // TODO Auto-generated constructor stub } public BookStockException(Throwable cause) { super(cause); // TODO Auto-generated constructor stub } } package tx; public class UserAccountException extends RuntimeException { private static final long serialVersionUID = 1L; public UserAccountException() { super(); // TODO Auto-generated constructor stub } public UserAccountException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); // TODO Auto-generated constructor stub } public UserAccountException(String message, Throwable cause) { super(message, cause); // TODO Auto-generated constructor stub } public UserAccountException(String message) { super(message); // TODO Auto-generated constructor stub } public UserAccountException(Throwable cause) { super(cause); // TODO Auto-generated constructor stub } } bean的配置文件如下: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd"> <context:component-scan base-package="tx"></context:component-scan> <!-- 导入资源文件 --> <context:property-placeholder location="classpath:db.properties"/> <!-- 配置c3p0数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.password}"></property> <property name="jdbcUrl" value="${jdbc.jdbcurl}"></property> <property name="driverClass" value="${jdbc.driverclass}"></property> <property name="initialPoolSize" value="${jdbc.initPoolSize}"></property> <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property> </bean> <!-- 配置Spring的JdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置 NameParameterJdbcTemplate,该对象可以使用具名参数,其没有无参数的构造器,所以必须为其构造器指定参数--> <bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate"> <constructor-arg ref="dataSource"></constructor-arg> </bean> <!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 启用事务注解 --> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>property文件:
jdbc.user=root jdbc.password=1234 jdbc.driverclass=com.mysql.cj.jdbc.Driver jdbc.jdbcurl=jdbc:mysql:///spring-5?useSSL=true&serverTimezone=UTC jdbc.initPoolSize=5 jdbc.maxPoolSize=10 bean的配置和property属性文件和上一篇文章的差不多,这里不多做介绍使用junit测试一下功能:
package tx; import static org.junit.Assert.*; import java.util.Arrays; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class SpringTransactionTest { private ApplicationContext ctx = null; private BookShopDao bookShopDao = null; private BookShopService bookShopService = null; private Cashier cashier = null; { ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); bookShopDao = ctx.getBean(BookShopDao.class); bookShopService = ctx.getBean(BookShopService.class); } @Test public void testBookShoService() { bookShopService.purchase("AA", "1001"); } @Test public void testBookShopDaoUpdate() { bookShopDao.updataUserAccount("AA", 100); } @Test public void testBookShopDaoUpdateBookStock() { bookShopDao.updateBookStock("1001"); } @Test public void testBookShopDapFindPriceByIsbn() { System.out.println(bookShopDao.findBookPriceByIsbn("1001")); } } 运行一下可以发现运行结果正常,再次查看数据库也会发现一切正常。读者可以自行修改数据库,产生购买失败的情况,可以看到失败的事件会被回滚。现在我们来考虑一个新的问题,当我们在一个事务中执行了一连串的事件时,当其中的一个事件出错了,需不需要将前面执行成功的事务也回滚呢?
写一个cashier类来测试一下:
package tx; import java.util.List; public interface Cashier { public void checkout(String username, List<String> isbns); } package tx; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service("cashier") public class CashierImpl implements Cashier { @Autowired private BookShopService bookShopService; //添加事务注解 @Transactional @Override public void checkout(String username, List<String> isbns) { // TODO Auto-generated method stub for (String isbn : isbns) { bookShopService.purchase(username, isbn); } } } 我们运行这段代码的话可以看到结果是只回滚了发生错误的事务,前面正常执行的事务没有被回滚,这是因为我们在前面的代码中设置了事务的传播属性 propagation=Propagation.REQUIRES_NEW,即在处理该事件时新开一个事件来处理,因此这个事件与调用它的事件不是处于同一个事件当中的,因此不会将调用其的事件也回滚。
除此之外Spring事务的事务传播行为还有6种:
REQUIRED:业务方法需要在一个容器里运行。如果方法运行时,已经处在一个事务中,那么加入到这个事务,否则自己新建一个新的事务。
NOT_SUPPORTED:声明方法不需要事务。如果方法没有关联到一个事务,容器不会为他开启事务,如果方法在一个事务中被调用,该事务会被挂起,调用结束后,原先的事务会恢复执行。
MANDATORY:该方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果在没有事务的环境下被调用,容器抛出例外。
SUPPORTS:该方法在某个事务范围内被调用,则方法成为该事务的一部分。如果方法在该事务范围外被调用,该方法就在没有事务的环境下执行。
NEVER:该方法绝对不能在事务范围内执行。如果在就抛例外。只有该方法没有关联到任何事务,才正常执行。
NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。
二、使用xml文件的方式进行事务管理
同样的,除了使用注解我们还可以通过xml文件配置的方式进行事务管理
还是使用上面的代码,但是需要将它们的注解全部去掉,然后在bean的配置文件中进行事务管理,配置文件如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd"> <context:component-scan base-package="tx"></context:component-scan> <!-- 导入资源文件 --> <context:property-placeholder location="classpath:db.properties"/> <!-- 配置c3p0数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.password}"></property> <property name="jdbcUrl" value="${jdbc.jdbcurl}"></property> <property name="driverClass" value="${jdbc.driverclass}"></property> <property name="initialPoolSize" value="${jdbc.initPoolSize}"></property> <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property> </bean> <!-- 配置Spring的JdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置bean --> <bean id="bookShopDao" class="tx.xml.BookShopDaoImpl"> <property name="jdbcTemplate" ref="jdbcTemplate"></property> </bean> <bean id="bookShopService" class="tx.xml.service.impl.BookShopServiceImpl"> <property name="bookShopDao" ref="bookShopDao"></property> </bean> <bean id="cashier" class="tx.xml.service.impl.CashierImpl"> <property name="bookShopService" ref="bookShopService"></property> </bean> <!-- 1.配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 2.配置事务属性 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- 根据方法名指定事务的属性 --> <tx:method name="purchase" propagation="REQUIRES_NEW"/> <tx:method name="get*" read-only="true"/> <tx:method name="find*" read-only="true"/> <tx:method name="*"/> </tx:attributes> </tx:advice> <!-- 这一步一定要记得添加编织器的库AspectJWeaver.jar,不然会报错 --> <!-- 配置事务切入点,以及把事务切入点和事务属性关联起来 --> <aop:config> <aop:pointcut expression="execution(* tx.xml.service.*.*(..))" id="txPointCut"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/> </aop:config> </beans> 一般来说,只要依赖的jar包加对了,版本也正确,是不会出什么问题的。以上就是Spring的事务管理。