深入理解分布式事务:原理与实战
上QQ阅读APP看书,第一时间看更新

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事务传播类型,并且把异常代码移动到外部方法的末尾,同时外部方法和内部方法在同一个类中,内部方法抛出异常,外部方法和内部方法都会执行失败,事务回滚。