写在前面

之前遇到一些自以为是的面试官问了一大推关于A方法内部调用B方法的事务问题,换着法的问,AB方法分别设置不同的事务传播属性时,如果AB方法中某个方法出错了,AB方法数据会怎样回滚的问题?

首先这种问题很不负责任,前提条件不是很清晰,问题没说清楚,容易成了绕口令游戏:

1、A和B方法有没有对异常进行try catch ?

2、AB方法可设置的事务传播属性组合那么多,怎么能口头上给你一一列举?

3、报错的位置是在数据库操作方法之前还是之后?

4、AB方法是否是来自同一个类的两个方法,还是来自两个不同类的方法?

5、事务的超时时间怎么设置的?

这样的问题,前提条件没说清楚,如果把注意力放在什么AB方法,异常位置的判断上,就跳进了坑里去

其实决定事务是否回滚的本质很简单,不需要考虑什么AB方法:

事务超时时间内,方法所属事务内是否抛出可以触发回滚的异常,有则回滚。没有就不回滚

如果你认同我的观点,后面的内容就没必要继续看了

1、spring中事务控制API介绍

PlatformTransactionManager接口

它是一个接口,是spring的事务管理器核心接口,spring并不实现具体的事务,而是负责包装底层事务,提供规范。应用底层支持什么样的事务策略,spring就支持什么样的事务策略。该接口提供了我们操作事务的常用方法。如下:

    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

    void commit(TransactionStatus var1) throws TransactionException;

    void rollback(TransactionStatus var1) throws TransactionException;

在这里插入图片描述

我们在开发中都是直接使用它的实现类,有如下常用实现类:

在这里插入图片描述

TransactionDefinition接口()

它是一个接口,是定义事务的信息对象,提供了如下常用的方法:

1、事务隔离级别

2、事务隔离级别

3、事务超时时间

public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;    //事务传播属性
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
    
    
    int ISOLATION_DEFAULT = -1;   //事务隔离级别
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;
    
    
    int TIMEOUT_DEFAULT = -1;   //事务超时时间

    default int getPropagationBehavior() {
        return 0;
    }

    default int getIsolationLevel() {
        return -1;
    }

    default int getTimeout() {
        return -1;
    }

    default boolean isReadOnly() {
        return false;
    }

    @Nullable
    default String getName() {
        return null;
    }

    static TransactionDefinition withDefaults() {
        return StaticTransactionDefinition.INSTANCE;
    }
}

事务传播属性

propagation说明
PROPAGATION_REQUIRED支持当前事务,如果当前没有事务,则新建一个事务执行
PROPAGATION_SUPPORTS支持当前事务,如果没有当前事务,则以非事务的方式执行
PROPAGATION_MANDATORY支持当前事务,如果当前没有事务,则抛出异常
PROPAGATION_REQUIRES_NEW创建一个新的事务,如果当前已经有事务了,则将当前事务挂起
PROPAGATION_NOT_SUPPORTED不支持当前事务,而且总是以非事务方式执行
PROPAGATION_NEVER不支持当前事务,如果存在事务,则抛出异常
PROPAGATION_NESTED如果当前事务存在,则在嵌套事务中执行,否则行为类似于PROPAGATION_REQUIRED。 EJB中没有类似的功能。

默认是PROPAGATION_REQUIRED的事务传播。

事务隔离级别

说明:事务隔离级别,反应了事务在并发访问时的处理态度。

ISOLATION_DEFAULT:

默认级别,归属于下列某一种隔离级别。在项目中使用默认值即可。

ISOLATION_READ_UNCOMMITTED:

可以读取其他事务未提交的数据(脏读)

ISOLATION_READ_COMMITTED:

只读取其他事务已经提交的数据,解决脏读的问题。有可能两次读取不一致的问题,不可重复读(oracle数据库默认级别

ISOLATION_REPEATABLE_READ:

是否读取其他事务提交修改后的数据,解决不可重复读的问题。保证两次读取一致,可重复读(mysql数据库默认级别

**ISOLATION_SERIALIZABLE:**是否读取其他事务添加后的数据,解决幻影读的问题

细节:

1.事务级别从低到高:脏读->不可重复读->可重复读->解决幻读

2.事务级别越高,数据越安全,消耗的资源越多,数据库操作性能越低

3.在企业项目中,使用哪一种级别的事务,需要根据业务需求来确定

事务超时时间

以秒为单位进行设置。如果设置为-1(默认值),表示没有超时限制。在企业项目中使用默认值即可。

2、决定事务回滚的本质

1、不是一个方法就是一个事务,事务和方法不是对等关系。一个事务可以管理多个方法的数据库操作
2、如何判断多个方法的数据库操作被同一个事务管理,需要看事务的传播属性
3、事务超时时间内,方法所属事务内是否抛出可以触发回滚的异常,有则回滚。没有就不回滚

测试事务回滚的本质

注意本文测试代码

实体类–成本表和收入表

初始值如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o5VLI31j-1621321052811)(C:\Users\22051\Desktop\spring\test.assets\image-20210518143438545.png)]

/**
 *  成本实体类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "tb_cost")
@ToString(callSuper = true)
public class Cost  {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;


    @Column(name = "cost")
    private BigDecimal cost;

}



/**
 * 收入实体类
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
@Entity
@Table(name = "tb_revenue")
@ToString(callSuper = true)
public class Revenue {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;


    @Column(name = "revenue")
    private BigDecimal revenue;


}

Dao数据库访问类
@Repository
public interface RevenueDao extends JpaRepository<Revenue, Integer> {

}


@Repository
public interface CostDao extends JpaRepository<Cost, Integer> {

}
service类

RevenueService的addRevenue内部调用costService.addCost方法

@Service
public class RevenueService {

    @Resource
    private RevenueDao revenueDao;

    @Resource
    private PlatformTransactionManager transactionManager;
    
    @Resource
    private CostService costService;
    
    /**
     *
     * @param id  查询的id
     * @param addNum  增加的数字
     * @param needRevenueServiceThrowExp  是否需要在RevenueService抛出异常
     * @param needCostServiceThrowExp  是否需要在CostService抛出异常
     * @param REVENUE_PROPAGATION   RevenueService的事务传播属性
     * @param COST_PROPAGATION  CostService的事务传播属性
     */
    public void addRevenue(Integer id, BigDecimal addNum, Boolean needRevenueServiceThrowExp, Boolean needCostServiceThrowExp, int REVENUE_PROPAGATION, int COST_PROPAGATION) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.setPropagationBehavior(REVENUE_PROPAGATION);
        transactionTemplate.execute(status -> {
            Revenue revenue = revenueDao.findById(id).get();
            revenue.setRevenue(revenue.getRevenue().add(addNum));
            revenueDao.save(revenue);
            costService.addCost(id, addNum,needCostServiceThrowExp, COST_PROPAGATION);
            if (needRevenueServiceThrowExp) {
                throw new RuntimeException("RevenueService手动报错");
            }
            return true;
        });
    }
    
}

CostService类

@Service
public class CostService {

    @Resource
    private CostDao costDao;


    @Resource
    private PlatformTransactionManager transactionManager;


    @Async
    public void addCost(Integer id, BigDecimal addNum, Boolean needThrowExp, int COST_PROPAGATION) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.setPropagationBehavior(COST_PROPAGATION);
        transactionTemplate.execute(status -> {
            Cost cost = costDao.findById(id).get();
            cost.setCost(cost.getCost().add(addNum));
            costDao.save(cost);
            if (needThrowExp) {
                throw new RuntimeException("CostService手动报错");
            }
            return true;
        });
    }
    
}
论证1:不是一个方法就是一个事务,事务和方法不是对等关系。一个事务可以管理多个方法的数据库操作

测试方法:

    @Test
    void contextLoads() {
        BigDecimal addNum = BigDecimal.ONE;
        revenueService.addRevenue(1, addNum, true,false, TransactionDefinition.PROPAGATION_REQUIRED,TransactionDefinition.PROPAGATION_REQUIRED);
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eSiSyAxm-1621321052817)(C:\Users\22051\Desktop\spring\test.assets\image-20210518143735154.png)]

costService.addCost和revenueService.addRevenue被同一个事务管理。即时costService.addCost运行正常,但revenueService.addRevenue报错时,一样会回滚

论证2、如何判断多个方法的数据库操作被同一个事务管理,需要看事务的传播属性

测试方法

    @Test
    void contextLoads() {
        BigDecimal addNum = BigDecimal.ONE;
        revenueService.addRevenue(1, addNum, true,false, TransactionDefinition.PROPAGATION_REQUIRED,TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    }

在这里插入图片描述

costService.addCost和revenueService.addRevenuede的事务传播属性不一样。costService.addCost是PROPAGATION_REQUIRES_NEW,新起一个事务,不会报错就提交事务成功。revenueService.addRevenue报错时,会回滚。因为他们被不同的事务管理

论证3、方法所属事务内是否出现触发回滚的异常,有则回滚。没有就不回滚

测试方法

    @Test
    void contextLoads() {
        BigDecimal addNum = BigDecimal.ONE;
        revenueService.addRevenue(1, addNum, true,true, TransactionDefinition.PROPAGATION_REQUIRED,TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
    }

在这里插入图片描述

同时结合论证2的例子

costService.addCost是PROPAGATION_REQUIRES_NEW,新起一个事务,不会报错就提交事务成功。

    @Test
    void contextLoads() {
        BigDecimal addNum = BigDecimal.ONE;
        revenueService.addRevenue(1, addNum, true,false, TransactionDefinition.PROPAGATION_REQUIRED,TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    }
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐