Spring事务

一、简介

1、介绍

事务,就是一组操作数据库的动作集合。事务是现代数据库理论中的核心概念之一。如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事务。当所有的步骤像一个操作一样被完整地执行,我们称该事务被提交。由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态。

2、事务特点(ACID)

  • 原子性(Atomicity):整个事务是一个整体,不可分割的最小工作单位。一个事务中的所有操作要么全部执行成功,要么全部都不执行。其中任何一条语句执行失败,都会导致事务回滚;

  • 一致性(Consistency):数据库的记录总是从一个一致性状态转变成另一个一致性状态;

  • 隔离性(Isolation):一个事物的执行,不受其他事务的干扰,即并发执行的事物之间互不干扰;

  • 持久性(Durability):数据一旦提交,结果就是永久性的。并不应为宕机等情况丢失。一般理解就是写入硬盘保存成功。

3、事务实现方式

3.1 MySql事务实现方式
  • 原子性和持久性利用redo log(重做日志) 实现

  • 一致性利用undo log(回滚日志)实现

  • 隔离性利用锁来实现

3.2 SpringBoot实现机制

Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编码式声明式的两种方式。

编程式事务管理: 编程式事务管理使用 TransactionTemplate 或者直接使用底层的 PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate

声明式事务管理: 建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务

声明式事务管理不需要入侵代码,更快捷而且简单,推荐使用!!!

声明式事务有两种方式:

  • 一种是在配置文件(xml)中做相关的事务规则声明

  • 另一种是基于 @Transactional 注解的方式。注释配置是目前流行的使用方式,推荐使用

在应用系统调用声明了 @Transactional 的目标方法时,Spring Framework默认使用 AOP 代理,在代码运行时生成一个代理对象,根据 @Transactional 的属性配置信息,这个代理对象决定该声明 @Transactional 的目标方法是否由拦截器 TransactionInterceptor来使用拦截,在 TransactionInterceptor拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑,最后根据执行情况是否出现异常,利用抽象事务管理器 AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务

Spring AOP 代理有 CglibAopProxy和 JdkDynamicAopProxy两种,以 CglibAopProxy 为例,对于 CglibAopProxy,需要调用其内部类的 DynamicAdvisedInterceptor的 intercept方法。对于 JdkDynamicAopProxy,需要调用其 invoke方法。

二、@Transactional详解

1、@Transactional常用配置

2、事务传播行为

事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何运行。

例如:methodA方法调用methodB方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。

Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。Propagation枚举则引用了这些类型,开发过程中我们一般直接用Propagation枚举。例如@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true),常用的三项已经加粗。

说明:加入该事务,指的是父、子方法共用一个事务(无论父、子方法报错,整体回滚)

数据准备

准备t_a数据表:

准备t_b数据表:

实体类TableA:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TableA {
    private Integer id;
    private String word;
}

实体类TableB:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TableB {
    private Integer id;
    private String word;
}

TableAMapper接口:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TableB {
    private Integer id;
    private String word;
}

TableBMapper接口:

@Mapper
public interface TableBMapper {
​
    /**
     * 向t_b中插入一条数据
     * @param tableB
     */
    void add(TableB tableB);
​
}

TableAMapper.xml文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dw.transactional.mapper.TableAMapper">
​
    <!-- 向t_a中插入一条数据 -->
    <insert id="add">
        insert into t_a values(#{id}, #{word})
    </insert>
​
</mapper>

TableBMapper.xml文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dw.transactional.mapper.TableBMapper">
​
    <!-- 向t_b中插入一条数据 -->
    <insert id="add">
        insert into t_b values(#{id}, #{word})
    </insert>
​
</mapper>

2.1、REQUIRED案例

Propagation.REQUIRED:需要事务(默认)。若当前无事务,新建一个事务;若当前有事务,加入此事务中。

代码准备:

@Service
public class TableBService {
​
    @Resource
    TableBMapper mapper;
​
    @Transactional
    public void insertTableBData(TableB tableB){
        mapper.add(tableB);
        // 抛出异常
        // throw new RuntimeException("子方法抛出异常~~~~~");
    }
​
}


@Service
public class TableAService {
​
    @Resource
    TableAMapper mapper;
​
    @Resource
    TableBService bService;
​
    @Transactional
    public void insertTableAData(TableA tableA){
        mapper.add(tableA);
        // 调用 TableBService 中带有事务的方法
        bService.insertTableBData(new TableB(1, "bbb"));
        // 抛出异常
        // throw new RuntimeException("父方法抛出异常!!!");
    }
​
}

各个情况的结果:

  • 父方法无事务,子方法开启事务,子方法抛出异常,则t_a数据插入成功,t_b数据回滚;

  • 父方法开启事务,子方法开启事务,父方法抛出异常,则t_a和t_b数据回滚;

  • 父方法开启事务,子方法开启事务,子方法抛出异常,则t_a和t_b数据回滚。

总结:

父方法无事务,则子方法开启新事务;

若父方法有事务,则子方法和父方法共用一个事务(无论父、子方法抛出异常,整体回滚)

2.2、SUPPORTS案例

Propagation.SUPPORTS:支持事务。若当前没有事务以非事务方式执行;若当前有事务,加入此事务中。

代码准备:

@Service
public class TableBService {
​
    @Resource
    TableBMapper mapper;
​
    @Transactional
    public void insertTableBData(TableB tableB){
        mapper.add(tableB);
        // 抛出异常
        // throw new RuntimeException("子方法抛出异常~~~~~");
    }
​
}


@Service
public class TableAService {
​
    @Resource
    TableAMapper mapper;
​
    @Resource
    TableBService bService;
​
    @Transactional
    public void insertTableAData(TableA tableA){
        mapper.add(tableA);
        // 调用 TableBService 中带有事务的方法
        bService.insertTableBData(new TableB(1, "bbb"));
        // 抛出异常
        // throw new RuntimeException("父方法抛出异常!!!");
    }
​
}

各个情况的结果:

  • 父方法无事务,子方法开启事务,子方法抛出异常,则t_a和t_b数据插入成功,子方法抛出异常;

  • 父方法开启事务,子方法开启事务,父方法抛出异常,则t_a和t_b数据回滚;

  • 父方法开启事务,子方法开启事务,子方法抛出异常,则t_a和t_b数据回滚。

总结:

父方法无事务,则子方法则以非事物的方式执行(未开启事务);

若父方法有事务,则子方法和父方法共用一个事务(无论父、子方法抛出异常,整体回滚)

2.3、MANDATORY案例

Propagation.MANDATORY:强制使用事务。若当前有事务,就使用当前事务;若当前没有事务,抛出IllegalTransactionStateException异常。

代码准备:

@Service
public class TableBService {

    @Resource
    TableBMapper bMapper;

    @Transactional(propagation = Propagation.MANDATORY)
    public void insertTableBData(TableB tableB){
        bMapper.add(tableB);
        // 抛出异常
        // throw new RuntimeException("子方法抛出异常~~~~~");
    }

}
@Service
public class TableAService {

    @Resource
    TableAMapper aMapper;

    @Resource
    TableBService bService;

    @Transactional
    public void insertTableAData(TableA tableA){
        aMapper.add(tableA);
        // 调用 TableBService 中带有事务的方法
        bService.insertTableBData(new TableB(1, "bbb"));
        // 抛出异常
        // throw new RuntimeException("父方法抛出异常!!!");
    }

}

各个情况的结果:

  • 父方法无事务,子方法开启事务,子方法抛出异常,则父方法以非事务方式执行,当执行到子方法时就会立即抛出IllegalTransactionStateException异常;

  • 父方法开启事务,子方法开启事务,父方法抛出异常,则t_a和t_b数据回滚;

  • 父方法开启事务,子方法开启事务,子方法抛出异常,则t_a和t_b数据回滚。

总结:

父方法无事务,则父方法以非事务方式执行,并会抛出异常;

若父方法有事务,则子方法和父方法共用一个事务(无论父、子方法抛出异常,整体回滚)

2.4、REQUIRES_NEW案例

Propagation.REQUIRES_NEW:新建事务。无论当前是否有事务,都新建事务运行。

代码准备:

@Service
public class TableBService {

    @Resource
    TableBMapper bMapper;

    @Transactional(propagation = Propagation.MANDATORY)
    public void insertTableBData(TableB tableB){
        bMapper.add(tableB);
        // 抛出异常
        // throw new RuntimeException("子方法抛出异常~~~~~");
    }

}
@Service
public class TableAService {

    @Resource
    TableAMapper aMapper;

    @Resource
    TableBService bService;

    @Transactional
    public void insertTableAData(TableA tableA){
        aMapper.add(tableA);
        // 调用 TableBService 中带有事务的方法
        bService.insertTableBData(new TableB(1, "bbb"));
        // 抛出异常
        // throw new RuntimeException("父方法抛出异常!!!");
    }

}

各个情况的结果:

  • 父方法无事务,子方法开启事务,子方法抛出异常,则父方法以非事务方式执行,t_b数据回滚;

  • 父方法开启事务,子方法开启事务,父方法抛出异常,则t_a数据回滚,t_b插入成功;

  • 父方法开启事务,子方法开启事务,子方法抛出异常,则t_a和t_b数据回滚。

总结:

无论父方法有没有事务,子方法都创建新事务

2.5、NOT_SUPPORTED案例

Propagation.NOT_SUPPORTED:不支持事务。若当前存在事务,把当前事务挂起,然后运行方法

代码准备:

@Service
public class TableBService {

    @Resource
    TableBMapper bMapper;

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void insertTableBData(TableB tableB){
        bMapper.add(tableB);
        // 抛出异常
        // throw new RuntimeException("子方法抛出异常~~~~~");
    }

}
@Service
public class TableAService {

    @Resource
    TableAMapper aMapper;

    @Resource
    TableBService bService;

    @Transactional
    public void insertTableAData(TableA tableA){
        aMapper.add(tableA);
        // 调用 TableBService 中带有事务的方法
        bService.insertTableBData(new TableB(1, "bbb"));
        // 抛出异常
        // throw new RuntimeException("父方法抛出异常!!!");
    }

}

各个情况的结果:

  • 父方法无事务,子方法开启事务,子方法抛出异常,则t_a和t_b插入成功;

  • 父方法开启事务,子方法开启事务,父方法抛出异常,则t_a数据回滚,t_b插入成功;

  • 父方法开启事务,子方法开启事务,子方法抛出异常,则t_a数据回滚,t_b插入成功;

总结:

若父方法无事务,则子方法以非事务方式执行;

若父方法有事务,父方法以事务方式执行,子方法以非事务方式执行

2.6、NEVER案例

Propagation.NEVER:不使用事务。若当前方法存在事务,则抛出IllegalTransactionStateException异常,否则继续使用无事务机制运行

2.7、NESTED案例

Propagation.NESTED:嵌套。如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则执行与REQUIRED类似的操作

代码准备:

@Service
public class TableBService {

    @Resource
    TableBMapper bMapper;

    @Transactional(propagation = Propagation.NESTED)
    public void insertTableBData(TableB tableB){
        bMapper.add(tableB);
        // 抛出异常
        // throw new RuntimeException("子方法抛出异常~~~~~");
    }

}
@Service
public class TableAService {

    @Resource
    TableAMapper aMapper;

    @Resource
    TableBService bService;

    @Transactional
    public void insertTableAData(TableA tableA){
        aMapper.add(tableA);
        // 调用 TableBService 中带有事务的方法
        bService.insertTableBData(new TableB(1, "bbb"));
        // 抛出异常
        // throw new RuntimeException("父方法抛出异常!!!");
    }

}

各个情况的结果:

  • 父方法无事务,子方法开启事务,子方法抛出异常,则t_a插入成功,t_b数据回滚;

  • 父方法开启事务,子方法开启事务,子方法抛出异常,则t_a和t_b数据回滚;

  • 父方法开启事务,子方法开启事务,父方法抛出异常,则t_a和t_b数据回滚;

  • 父方法开启事务,子方法开启事务,子方法报错,父方法并捕获,则t_a插入成功,t_b数据回滚

总结:

如果父方法没有事务,则子方法新开事务执行 如果父方法存在事务,则子方法在嵌套事务内执行

区别总结

1、NESTED和REQUIRES区别

区别的场景:当前存在事务,子方法抛异常

NESTED和REQUIRES_NEW在父方法可以选择捕获子方法,父方法数据不会回滚
REQUIRES无论捕不捕获,父方法数据都回滚

2、NESTED和REQUIRES_NEW区别

区别的场景:当前存在事务,父方法抛异常时

NESTED数据回滚,REQUIRES也是如此

REQUIRES_NEW数据不回滚

3、事务5种隔离级别

例如:@Transactional(isolation = Isolation.READ_COMMITTED)

三、事务使用事项与场景

1、事务使用注意事项

① 在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上

② @Transactional 注解应该只被应用在 public 修饰的方法上(注意)。 如果在 protected、private 或者 package-visible 的方法上使用 该注解,它也不会抛出异常 (IDEA会有提示), 但事务并没有生效

③ @Transactional是基于动态代理的(注意),需要一个类调用另一个类,类内调用会失效

④ 被外部调用的公共方法A有两个进行了数据操作的子方法B和子方法C的事务注解说明:

  • 被外部调用的公共方法A声明事务@Transactional,无论子方法B和C是不是本类的方法,无论子方法B和C是否声明事务,事务均由公共方法A控制

  • 被外部调用的公共方法A未声明事务@Transactional,子方法B和C若是其他类的方法且各自声明事务:事务由子方法B和C各自控制

  • 被外部调用的公共方法A未声明事务@Transactional,子方法B和C若是本类的方法,则即使子方法B和C各自声明事务,事务也不会生效,并且会抛出异常(没有可用的transactional)

  • 被外部调用的公共方法A声明事务@Transactional,子方法运行异常,但运行异常被子方法自己 try-catch 处理了,则事务回滚是不会生效的!

如果想要事务回滚生效,需要将子方法的事务控制交给调用的方法来处理:

方案1:子方法中不用 try-catch 处理运行异常

方案2:子方法的catch里面将运行异常抛出throw new RuntimeException();

⑤ 默认情况下,Spring会对unchecked异常进行事务回滚,也就是默认对 RuntimeException() 异常或是其子类进行事务回滚;如果是checked异常则不回滚,例 如空指针异常、算数异常等会被回滚;文件读写、网络问题Spring就没法回滚。若想对所有异常(包括自定义异常)都起作用,注解上面需配置异常类型: @Transactional(rollbackFor = Exception.class)

⑥ 数据库要支持事务,如果是mysql,要使用innodb引擎,myisam不支持事务

⑦ 事务@Transactional由spring控制时,它会在抛出异常的时候进行回滚。如果自己使用try-catch捕获处理了,是不生效的。如果想事务生效可以进行手动回滚 或者在catch里面将异常抛出throw new RuntimeException();有两种方案:

方案一:手动抛出运行时异常(缺陷是不能在catch代码块自定义返回值)

try{
      ....  
  }catch(Exception e){
      logger.error("fail",e);
      throw new RuntimeException;
}

方案二:手动进行回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

try{
      ...
  }catch(Exception e){
      log.error("fail",e);
      TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
      return false;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值