4 Spring中的事务管理
1 Spring中的事务管理
- 事务就是一系列的动作,它们被当做一个单独的工作单元,这些动作要么全部完成,要么全部不起作用。
- Spring在不同的事物管理API之上定义了一个抽象层。应用程序开发人员不必了解底层的事物管理API,就可以使用Spring的事物管理机制。
- Spring既支持编程式事务管理,也支持声明式事务管理。
- 编程式事务管理:将事务管理代码嵌入到业务中来控制事务的提交和回滚。
- 声明式事务管理:将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
- 事务管理作为一种横切关注点,可以通过AOP方法模块化。
- Spring通过Spring AOP框架支持声明式事务管理。
- 事务管理器以普通的Bean形式声明在Spring IOC容器中。
2 使用案例
数据库表:
create table book ( isbn varchar(50) primary key, book_name varchar(100), price int ); create table book_stock( isbn varchar(50) primary key, stock int, check(stock>0) ); create table account( username varchar(50) primary key, balance int, check(balance > 0) ); INSERT INTO `spring_test`.`book_stock` (`isbn`, `stock`) VALUES ('1001', '10'); INSERT INTO `spring_test`.`book_stock` (`isbn`, `stock`) VALUES ('1002', '10'); INSERT INTO `spring_test`.`book` (`isbn`, `book_name`, `price`) VALUES ('1001', '《Java从入门到放弃》', '100'); INSERT INTO `spring_test`.`book` (`isbn`, `book_name`, `price`) VALUES ('1002', '《活着》', '70'); INSERT INTO `spring_test`.`account` (`username`, `balance`) VALUES ('Tom', '300');
项目目录:
db.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/spring_test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC jdbc.username=root jdbc.password=root
applicationContext.xml:
<?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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 注解扫描 --> <context:component-scan base-package="com.xianhuii"></context:component-scan> <!-- 引入外部化的配置文件 --> <context:property-placeholder location="classpath:db.properties"/> <!-- 配置数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="${jdbc.driver}"></property> <property name="jdbcUrl" value="${jdbc.url}"></property> <property name="user" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <!-- 配置jdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 注入数据源 --> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务管理器 --> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 基于注解使用事务,需要开启事务注解 --> <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/> </beans>
BookShopDao:
package com.xianhuii.dao; public interface BookShopDao { // 1、根据书号获取对应的价格 public int findBookPriceByIsbn(String isbn); // 2、根据书号更新库存 public void updateBookStock(String isbn); // 3、根据书的价格更新用户的余额 public void updateUserAccount(String username, int price); }
BookShopDaoImpl:
package com.xianhuii.dao; import com.xianhuii.exception.BookStockException; import com.xianhuii.exception.UserAccountException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository public class BookShopDaoImpl implements BookShopDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public int findBookPriceByIsbn(String isbn) { String sql = "select price from book where isbn = ?"; return jdbcTemplate.queryForObject(sql, Integer.class, isbn); } @Override public void updateBookStock(String isbn) { // 判断库存是否足够 String sql = "select stock from book_stock where isbn = ?"; int stock = jdbcTemplate.queryForObject(sql, Integer.class, isbn); if (stock <= 0) { // 抛出库存不足的异常 throw new BookStockException("库存不足!"); } sql = "update book_stock set stock = stock -1 where isbn = ?"; jdbcTemplate.update(sql, isbn); } @Override public void updateUserAccount(String username, int price) { // 判断余额是否足够 String sql = "select balance from account where username = ?"; int balance = jdbcTemplate.queryForObject(sql, Integer.class, username); if (balance < price) { // 抛出余额不足的异常 throw new UserAccountException("余额不足!"); } sql = "update account set balance = balance - ? where username = ?"; jdbcTemplate.update(sql, price, username); } }
BookStockException:
package com.xianhuii.exception; public class BookStockException extends RuntimeException { public BookStockException() { super(); } public BookStockException(String message) { super(message); } public BookStockException(String message, Throwable cause) { super(message, cause); } public BookStockException(Throwable cause) { super(cause); } protected BookStockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }
UserAccountException:
package com.xianhuii.exception; public class UserAccountException extends RuntimeException { public UserAccountException() { super(); } public UserAccountException(String message) { super(message); } public UserAccountException(String message, Throwable cause) { super(message, cause); } public UserAccountException(Throwable cause) { super(cause); } protected UserAccountException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }
BookShopService:
package com.xianhuii.service; public interface BookShopService { public void buyBook(String username, String isbn); }
BookShopServiceImpl:
package com.xianhuii.service; import com.xianhuii.dao.BookShopDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service //@Transactional // 对该类中所有的方法都起作用 public class BookShopServiceImpl implements BookShopService { @Autowired private BookShopDao bookShopDao; @Transactional // 对指定方法起作用 @Override public void buyBook(String username, String isbn) { // 1、查询书的价格 int price = bookShopDao.findBookPriceByIsbn(isbn); // 2、更新数的库存 bookShopDao.updateBookStock(isbn); // 3、更新用户的余额 bookShopDao.updateUserAccount(username, price); } }
TransactionTest:
package com.xianhuii.test; import com.xianhuii.service.BookShopService; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class TransactionTest { private ApplicationContext context; private BookShopService bookShopService; { context = new ClassPathXmlApplicationContext("applicationContext.xml"); bookShopService = (BookShopService) context.getBean("bookShopServiceImpl"); } @Test public void buyBookTest() { bookShopService.buyBook("Tom", "1001"); } }
3 总结
①在applicationContext.xml配置:
tx:annotation-driven中,transaction-manager的默认值是transactionManager。
如果当前事务管理器的id值就是transactionManager,那么transaction-manager的配置可以省略。
如果当前事务管理器的id值不是transactionManager,那么就必须在transaction-manager中指定当前的事务管理器的id值。
<?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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 注解扫描 --> <context:component-scan base-package="com.xianhuii"></context:component-scan> <!-- 引入外部化的配置文件 --> <context:property-placeholder location="classpath:db.properties"/> <!-- 配置数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="${jdbc.driver}"></property> <property name="jdbcUrl" value="${jdbc.url}"></property> <property name="user" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <!-- 配置jdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 注入数据源 --> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务管理器 --> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 基于注解使用事务,需要开启事务注解 --> <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/> </beans>
②使用注解方式开启事务:
package com.xianhuii.service; import com.xianhuii.dao.BookShopDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service //@Transactional // 对该类中所有的方法都起作用 public class BookShopServiceImpl implements BookShopService { @Autowired private BookShopDao bookShopDao; @Transactional // 对指定方法起作用 @Override public void buyBook(String username, String isbn) { // 1、查询书的价格 int price = bookShopDao.findBookPriceByIsbn(isbn); // 2、更新数的库存 bookShopDao.updateBookStock(isbn); // 3、更新用户的余额 bookShopDao.updateUserAccount(username, price); } }
4 事务的属性
1、propagation
- propagation:事务的传播行为。
- Propagation.REQUIRED:使用调用者的事务,默认值。
- Propagation.REQUIRES_NEW:将调用者的事务挂起,使用自己的新事务。
2、isolation
- 事务的隔离级别,最常用的是:READ_COMMITTED。
3、readOnly
- 指定事务是否为只读。如果是只读事务,代表这个事务只读取数据库,而不进行修改操作。
- 若一个事务真的是只读取数据,就有必要设置readOnly=true,可以帮助数据库引擎进行优化。
4、(不)回滚条件
- rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName。
5、timeout
- 指定强制回滚前事务可以占用的时间。为了避免一个事务占用过长时间。
6、案例
Cashier:
package com.xianhuii.service; import java.util.List; public interface Cashier { /** * 模拟用户结账行为,同时买多本书 */ public void checkOut(String username, List<String> isbns); }
CashierImpl:
package com.xianhuii.service; 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 java.util.List; @Service public class CashierImpl implements Cashier { @Autowired private BookShopService bookShopService; /** * 事务的属性: * propagation:事务的传播行为。 * Propagation.REQUIRED:使用调用者的事务,默认值。 * Propagation.REQUIRES_NEW:将调用者的事务挂起,使用自己的新事务。 * isolation:事务的隔离级别,最常用的是:READ_COMMITTED。 * readOnly:指定事务是否为只读。如果是只读事务,代表这个事务只读取数据库,而不进行修改操作。 * 若一个事务真的是只读取数据,就有必要设置readOnly=true,可以帮助数据库引擎进行优化。 * rollbackFor * rollbackForClassName * noRollbackFor * noRollbackForClassName * timeout:指定强制回滚前事务可以占用的时间。为了避免一个事务占用过长时间。 */ @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED) public void checkOut(String username, List<String> isbns) { for (String isbn : isbns) { bookShopService.buyBook(username, isbn); } } }
测试:
@Test public void checkOutTest () { List<String> isbns = new ArrayList<>(); isbns.add("1001"); isbns.add("1002"); cashier.checkOut("Tom", isbns); }
5 基于XML配置事务
<!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务属性 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="byBook" propagation="REQUIRES_NEW" isolation="READ_COMMITTED" read-only="false"/> <tx:method name="checkOut"/> <tx:method name="update*" propagation="REQUIRES_NEW"/> <tx:method name="insert*" propagation="REQUIRED"/> <tx:method name="delete*" propagation="REQUIRED"/> <tx:method name="get*" read-only="true"/> <!-- *代表除了上述指定的方法之外的方法 --> <tx:method name="*"/> </tx:attributes> </tx:advice> <!-- 配置事务切入点,以及事务切入点和事务属性关联起来 --> <aop:config> <aop:pointcut id="txPointCut" expression="execution(* com.xianhuii.service.*.*(..))"></aop:pointcut> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/> </aop:config>