3.5 Spring事务嵌套最佳实践
3.4节简单介绍了Spring事务传播机制的理论,本节以案例的形式介绍Spring事务传播机制的使用方法。
3.5.1 环境准备
电商场景中一个典型的操作就是下单减库存。从本节开始,以下单减库存的场景为例,说明Spring事务传播机制的使用方法。先准备环境。
第一步:创建Maven项目spring-tx,并在pom.xml文件中添加Maven依赖。
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.21.RELEASE</version> </dependency> <!--加入lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.1</version> </dependency> <!--加入日志包--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.3.21.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.8</version> </dependency> </dependencies>
第二步:创建用于测试的实体类,在io.transaction.spring.entity包下分别创建订单类Order和商品类Product,如下所示。
创建订单类Order代码如下。
public class Order { /** * 数据id */ private Long id; /** * 订单编号 */ private String orderNo; #########省略get/set方法############# }
创建商品类Product代码如下。
public class Product { /** * 数据id */ private Long id; /** * 商品名称 */ private String productName; /** * 商品价格 */ private BigDecimal productPrice; /** * 库存数量 */ private Integer stockCount; ##########省略get/set方法############## }
注意,这里为了方便展示,简写了订单类和商品类的实体类,在实际开发过程中,订单类和商品类的设计远比本节描述的复杂。
第三步:创建操作数据库的Dao类。在io.transaction.spring.dao包下分别创建OrderDao类和ProductDao类,如下所示。
OrderDao类主要用于操作数据库中的订单数据并提供保存订单的方法。创建OrderDao类代码如下。
@Repository public class OrderDao { @Autowired private JdbcTemplate jdbcTemplate; public int saveOrder(Order order){ String sql = "insert into order_info (id, order_no) values (?, ?)"; return jdbcTemplate.update(sql, order.getId(), order.getOrderNo()); } }
ProductDao类主要用于操作数据库中的商品信息并提供扣减库存的方法。创建Product-Dao类代码如下。
@Repository public class ProductDao { @Autowired private JdbcTemplate jdbcTemplate; public int updateProductStockCountById(Integer stockCount, Long id){ String sql = "update product_info set stock_count = stock_count - ? where id = ?"; return jdbcTemplate.update(sql, stockCount, id); } }
第四步:创建Service类。在io.transaction.spring.service包下分别创建OrderServcie类和ProductService类,如下所示。
OrderService类调用OrderDao类,实现保存订单的操作,同时会调用ProductService的方法实现减库存的操作。创建OrderService类代码如下。
@Service public class OrderService { @Autowired private OrderDao orderDao; @Autowired private ProductService productService; public void submitOrder(){ //生成订单 Order order = new Order(); long number = Math.abs(new Random().nextInt(500)); order.setId(number); order.setOrderNo("order_" + number); orderDao.saveOrder(order); //减库存 productService.updateProductStockCountById(1, 1L); } }
ProductServcie类的主要作用是扣减库存,创建ProductService类代码如下。
@Service public class ProductService { @Autowired private ProductDao productDao; public void updateProductStockCountById(Integer stockCount, Long id){ productDao.updateProductStockCountById(stockCount, id); int i = 1 / 0; } }
注意在ProductService类的updateProductStockCountById()方法中,有一行代码为int i=1/0,说明这个方法会抛出异常。
第五步:创建配置类。在io.transaction.spring.config包下创建配置类MainConfig,如下所示。
@EnableTransactionManagement @Configuration @ComponentScan(basePackages = {"io.transaction.spring"}) public class MainConfig { @Bean public DataSource dataSource(){ DruidDataSource dataSource = new DruidDataSource(); dataSource.setUsername("root"); dataSource.setPassword("root"); dataSource.setUrl("jdbc:mysql://localhost:3306/spring-tx"); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); return dataSource; } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource){ return new JdbcTemplate(dataSource); } @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
MainConfig类的作用是开始Spring事务管理,扫描io.transaction.spring包下的类,将DataSource、JdbcTemplate和PlatformTransactionManager对象加载到IOC容器中。
第六步:创建系统启动类,也是整个程序的运行入口类。在io.transaction.spring包下创建Main类,用于启动应用程序,如下所示。
public class Main { public static void main(String[] args){ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class); OrderService orderService = context.getBean(OrderService.class); orderService.submitOrder(); } }
第七步:创建数据库spring-tx,并在spring-tx数据库中创建order_info数据表和product_info数据表,如下所示。
create database if not exists spring-tx; CREATE TABLE IF NOT EXISTS order_info ( `id` bigint(20) NOT NULL, `order_no` varchar(50) DEFAULT '', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS product_info ( `id` bigint(20) NOT NULL, `product_name` varchar(50) DEFAULT NULL, `product_price` decimal(10,2) DEFAULT NULL, `stock_count` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
向product_info数据表中插入基础数据,如下所示。
INSERT INTO `spring-tx`.`product_info`(`id`, `product_name`, `product_price`, `stock_count`) VALUES (1, '笔记本电脑', 10000.00, 100);
此时查询order_info数据表和product_info数据表中的数据,如下所示。
mysql> select * from order_info; Empty set (0.00 sec) mysql> select * from product_info; +----+-----------------+---------------+-------------+ | id | product_name | product_price | stock_count | +----+-----------------+---------------+-------------+ | 1 | 笔记本电脑 | 10000.00 | 100 | +----+-----------------+---------------+-------------+ 1 row in set (0.00 sec)
至此,准备工作就完成了。接下来验证Spring中的各个事务传播机制的类型。
3.5.2 最佳实践场景一
场景一为外部方法无事务注解,内部方法添加REQUIRED事务传播类型。
第一步:在OrderService类的submitOrder()方法上不添加注解,如下所示。
public void submitOrder(){ //生成订单 Order order = new Order(); long number = Math.abs(new Random().nextInt(500)); order.setId(number); order.setOrderNo("order_" + number); orderDao.saveOrder(order); //减库存 productService.updateProductStockCountById(1, 1L); }
第二步:在ProductService类的updateProductStockCountById()方法中添加@Transac-tional(propagation=Propagation.REQUIRED)注解,如下所示。
@Transactional(propagation = Propagation.REQUIRED) public void updateProductStockCountById(Integer stockCount, Long id){ productDao.updateProductStockCountById(stockCount, id); int i = 1 / 0; }
第三步:运行Main类中的main()方法,抛出了如下异常。
Exception in thread "main" java.lang.ArithmeticException: / by zero
这是因ProductService类的updateProductStockCountById()方法中存在如下代码而引起的。
int i = 1 / 0;
第四步:查询order_info表和product_info表中的数据,如下所示。
mysql> select * from order_info; +-----+-----------+ | id | order_no | +-----+-----------+ | 172 | order_172 | +-----+-----------+ 1 row in set (0.00 sec) mysql> select * from product_info; +----+-----------------+---------------+-------------+ | id | product_name | product_price | stock_count | +----+-----------------+---------------+-------------+ | 1 | 笔记本电脑 | 10000.00 | 100 | +----+-----------------+---------------+-------------+ 1 row in set (0.00 sec)
可以看到,当OrderService类的submitOrder()方法上不添加注解,而ProductService类的updateProductStockCountById()方法中添加@Transactional(propagation=Propagation.REQUIRED)注解,并且ProductService类的updateProductStockCountById()方法抛出异常时,OrderService类的submitOrder()方法执行成功,向数据库保存订单信息。ProductService类的updateProductStockCountById()方法执行失败抛出异常,并没有扣减库存。
总结:外部方法无事务注解,内部方法添加REQUIRED事务传播类型时,内部方法抛出异常。内部方法执行失败,不会影响外部方法的执行,外部方法执行成功。
3.5.3 最佳实践场景二
场景二为外部方法添加REQUIRED事务传播类型,内部方法无事务注解。
第一步:在OrderService类的submitOrder()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,如下所示。
@Transactional(propagation = Propagation.REQUIRED) public void submitOrder(){ //生成订单 Order order = new Order(); long number = Math.abs(new Random().nextInt(500)); order.setId(number); order.setOrderNo("order_" + number); orderDao.saveOrder(order); //减库存 productService.updateProductStockCountById(1, 1L); }
第二步:ProductService类的updateProductStockCountById()方法上不添加注解,如下所示。
public void updateProductStockCountById(Integer stockCount, Long id){ productDao.updateProductStockCountById(stockCount, id); int i = 1 / 0; }
第三步:运行Main类中的main()方法,抛出了如下异常。
Exception in thread "main" java.lang.ArithmeticException: / by zero
第四步:查询order_info表和product_info表中的数据,如下所示。
mysql> select * from order_info; Empty set (0.00 sec) mysql> select * from product_info; +----+-----------------+---------------+-------------+ | id | product_name | product_price | stock_count | +----+-----------------+---------------+-------------+ | 1 | 笔记本电脑 | 10000.00 | 100 | +----+-----------------+---------------+-------------+ 1 row in set (0.00 sec)
可以看到,当OrderService类的submitOrder()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,而ProductService类的updateProductStockCountById()方法不添加事务注解,并且ProductService类的updateProductStockCountById()方法抛出异常时,OrderService类的submitOrder()方法和ProductService类的updateProductStockCountById()方法都执行失败。
总结:外部方法添加REQUIRED事务传播类型,内部方法无事务注解时,内部方法抛出异常,会影响外部方法的执行,导致外部方法的事务回滚。
3.5.4 最佳实践场景三
场景三为外部方法添加REQUIRED事务传播类型,内部方法添加REQUIRED事务传播类型。
第一步:在OrderService类的submitOrder()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,如下所示。
@Transactional(propagation = Propagation.REQUIRED) public void submitOrder(){ //生成订单 Order order = new Order(); long number = Math.abs(new Random().nextInt(500)); order.setId(number); order.setOrderNo("order_" + number); orderDao.saveOrder(order); //减库存 productService.updateProductStockCountById(1, 1L); }
第二步:在ProductService类的updateProductStockCountById()方法上添加@Transac-tional(propagation=Propagation.REQUIRED)注解,如下所示。
@Transactional(propagation = Propagation.REQUIRED) public void updateProductStockCountById(Integer stockCount, Long id){ productDao.updateProductStockCountById(stockCount, id); int i = 1 / 0; }
第三步:运行Main类中的main()方法,抛出了如下异常。
Exception in thread "main" java.lang.ArithmeticException: / by zero
第四步:查询order_info表和product_info表中的数据,如下所示。
mysql> select * from order_info; Empty set (0.00 sec) mysql> select * from product_info; +----+-----------------+---------------+-------------+ | id | product_name | product_price | stock_count | +----+-----------------+---------------+-------------+ | 1 | 笔记本电脑 | 10000.00 | 100 | +----+-----------------+---------------+-------------+ 1 row in set (0.00 sec)
可以看到,当OrderService类的submitOrder()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,ProductService类的updateProductStockCountById()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,并且ProductService类的updateProductStockCountById()方法抛出异常时,OrderService类的submitOrder()方法和ProductService类的updateProductStockCountById()方法都执行失败。
总结:外部方法添加REQUIRED事务传播类型,内部方法添加REQUIRED事务传播类型时,内部方法抛出异常,会影响外部方法的执行,事务会回滚。
3.5.5 最佳实践场景四
场景四为外部方法添加REQUIRED事务传播类型,内部方法添加NOT_SUPPORTED事务传播类型。
第一步:在OrderService类的submitOrder()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,如下所示。
@Transactional(propagation = Propagation.REQUIRED) public void submitOrder(){ //生成订单 Order order = new Order(); long number = Math.abs(new Random().nextInt(500)); order.setId(number); order.setOrderNo("order_" + number); orderDao.saveOrder(order); //减库存 productService.updateProductStockCountById(1, 1L); }
第二步:在ProductService类的updateProductStockCountById()方法上添加@Transac-tional(propagation=Propagation.NOT_SUPPORTED)注解,如下所示。
@Transactional(propagation = Propagation. NOT_SUPPORTED) public void updateProductStockCountById(Integer stockCount, Long id){ productDao.updateProductStockCountById(stockCount, id); int i = 1 / 0; }
第三步:运行Main类中的main()方法,抛出了如下异常。
Exception in thread "main" java.lang.ArithmeticException: / by zero
第四步:查询order_info表和product_info表中的数据,如下所示。
mysql> select * from order_info; Empty set (0.00 sec) mysql> select * from product_info; +----+-----------------+---------------+-------------+ | id | product_name | product_price | stock_count | +----+-----------------+---------------+-------------+ | 1 | 笔记本电脑 | 10000.00 | 99 | +----+-----------------+---------------+-------------+ 1 row in set (0.00 sec)
可以看到,当OrderService类的submitOrder()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,ProductService类的updateProductStockCountById()方法上添加@Transactional(propagation=Propagation.NOT_SUPPORTED)注解,并且ProductService类的updateProductStockCountById()方法抛出异常时,OrderService类的submitOrder()方法执行失败,ProductService类的updateProductStockCountById()方法执行成功。
总结:外部方法添加REQUIRED事务传播类型,内部方法添加NOT_SUPPORTED事务传播类型时,内部方法抛异常,如果外部方法执行成功,事务会提交,如果外部方法执行失败,事务会回滚。
3.5.6 最佳实践场景五
场景五为外部方法添加REQUIRED事务传播类型,内部方法添加REQUIRES_NEW事务传播类型。
第一步:在OrderService类的submitOrder()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,如下所示。
@Transactional(propagation = Propagation.REQUIRED) public void submitOrder(){ //生成订单 Order order = new Order(); long number = Math.abs(new Random().nextInt(500)); order.setId(number); order.setOrderNo("order_" + number); orderDao.saveOrder(order); //减库存 productService.updateProductStockCountById(1, 1L); }
第二步:在ProductService类的updateProductStockCountById()方法上添加@Transac-tional(propagation=Propagation.REQUIRES_NEW)注解,如下所示。
@Transactional(propagation = Propagation.REQUIRES_NEW) public void updateProductStockCountById(Integer stockCount, Long id){ productDao.updateProductStockCountById(stockCount, id); int i = 1 / 0; }
第三步:运行Main类中的main()方法,抛出了如下异常。
Exception in thread "main" java.lang.ArithmeticException: / by zero
第四步:查询order_info表和product_info表中的数据,如下所示。
mysql> select * from order_info; Empty set (0.00 sec) mysql> select * from product_info; +----+-----------------+---------------+-------------+ | id | product_name | product_price | stock_count | +----+-----------------+---------------+-------------+ | 1 | 笔记本电脑 | 10000.00 | 100 | +----+-----------------+---------------+-------------+ 1 row in set (0.00 sec)
可以看出,当OrderService类的submitOrder()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,ProductService类的updateProductStockCountById()方法上添加@Transactional(propagation=Propagation.REQUIRES_NEW)注解,并且ProductService类的updateProductStockCountById()方法抛出异常时,OrderService类的submitOrder()方法和ProductService类的updateProductStockCountById()方法都会执行失败,事务回滚。
总结:外部方法添加REQUIRED事务传播类型,内部方法添加REQUIRES_NEW事务传播类型,内部方法抛出异常时,内部方法和外部方法都会执行失败,事务回滚。
3.5.7 最佳实践场景六
场景六为外部方法添加REQUIRED事务传播类型,内部方法添加REQUIRES_NEW事务传播类型,并且把异常代码移动到外部方法的末尾。
第一步:在OrderService类的submitOrder()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,并且在该方法末尾添加int i=1/0,代码如下所示。
@Transactional(propagation = Propagation.REQUIRED) public void submitOrder(){ //生成订单 Order order = new Order(); long number = Math.abs(new Random().nextInt(500)); order.setId(number); order.setOrderNo("order_" + number); orderDao.saveOrder(order); //减库存 productService.updateProductStockCountById(1, 1L); int i = 1 / 0; }
第二步:在ProductService类的updateProductStockCountById()方法上添加@Transac-tional(propagation=Propagation.REQUIRES_NEW)注解,去除int i=1/0,代码如下所示。
@Transactional(propagation = Propagation.REQUIRES_NEW) public void updateProductStockCountById(Integer stockCount, Long id){ productDao.updateProductStockCountById(stockCount, id); }
第三步:运行Main类中的main()方法,抛出了如下异常。
Exception in thread "main" java.lang.ArithmeticException: / by zero
第四步;查询order_info表和product_info表中的数据,如下所示。
mysql> select * from order_info; Empty set (0.00 sec) mysql> select * from product_info; +----+-----------------+---------------+-------------+ | id | product_name | product_price | stock_count | +----+-----------------+---------------+-------------+ | 1 | 笔记本电脑 | 10000.00 | 99 | +----+-----------------+---------------+-------------+ 1 row in set (0.00 sec)
可以看出,OrderService类的submitOrder()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,并且在该方法末尾添加int i=1/0,在ProductService类的updateProductStockCountById()方法上添加@Transactional(propagation=Propagation.REQUIRES_NEW)注解,去除int i=1/0。updateProductStockCountById()方法抛出异常时,OrderService类的submitOrder()方法执行失败,事务回滚。ProductService类的updateProductStockCountById()方法执行成功,事务提交。
总结:外部方法添加REQUIRED事务传播类型,内部方法添加REQUIRES_NEW事务传播类型,并且把异常代码移动到外部方法的末尾,内部方法抛异常时,外部方法执行失败,事务回滚;内部方法执行成功时,事务提交。
3.5.8 最佳实践场景七
场景七为外部方法添加REQUIRED事务传播类型,内部方法添加REQUIRES_NEW事务传播类型,并且把异常代码移动到外部方法的末尾,同时外部方法和内部方法在同一个类中。
第一步:在OrderService类的submitOrder()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,并且在OrderService类的submitOrder()方法末尾添加int i=1/0,如下所示。
@Transactional(propagation = Propagation.REQUIRED) public void submitOrder(){ //生成订单 Order order = new Order(); long number = Math.abs(new Random().nextInt(500)); order.setId(number); order.setOrderNo("order_" + number); orderDao.saveOrder(order); //减库存 this.updateProductStockCountById(1, 1L); int i = 1 / 0; }
这里需要注意productService.updateProductStockCountById(1,1L)这行代码已经变成了this.updateProductStockCountById(1,1L)。
第二步:在OrderService类中添加updateProductStockCountById()方法,如下所示。
@Transactional(propagation = Propagation.REQUIRES_NEW) public void updateProductStockCountById(Integer stockCount, Long id){ productDao.updateProductStockCountById(stockCount, id); }
第三步:运行Main类中的main()方法,抛出了如下异常。
Exception in thread "main" java.lang.ArithmeticException: / by zero
第四步:查询order_info表和product_info表中的数据,如下所示。
mysql> select * from order_info; Empty set (0.00 sec) mysql> select * from product_info; +----+-----------------+---------------+-------------+ | id | product_name | product_price | stock_count | +----+-----------------+---------------+-------------+ | 1 | 笔记本电脑 | 10000.00 | 100 | +----+-----------------+---------------+-------------+ 1 row in set (0.00 sec)
可以看出,在OrderService类的submitOrder()方法上添加@Transactional(propagation=Propagation.REQUIRED)注解,在OrderService类的submitOrder()方法末尾添加int i=1/0代码,同时在OrderService类中添加updateProductStockCountById()方法,update-ProductStockCountById()方法抛出异常时,OrderService类的submitOrder()方法和update-ProductStockCountById()方法执行失败,事务回滚。
总结:外部方法添加REQUIRED事务传播类型,内部方法添加REQUIRES_NEW事务传播类型,并且把异常代码移动到外部方法的末尾,同时外部方法和内部方法在同一个类中,内部方法抛出异常,外部方法和内部方法都会执行失败,事务回滚。