跳到主要内容

事务

· 阅读需 7 分钟

参考链接:

MySql的四种事务隔离级别 - 超级小小黑 - 博客园

一口气说出 6种 @Transactional 注解失效场景

一、事务

事务的特性:

  • A(atomicity):原子性,一个事务是一个不可分割的工作单位,要么都提交,要不都不提交。
  • C(consistency):一致性,事务前后,数据状态一致。
  • I(isolation):隔离性,事务互不干扰。
  • D(durability):持久性,一旦提交,数据永久性更改。

编程式事务

在代码中手动提交事务、回滚等操作,代码侵入性比较强

Connection conn;
try {
conn.setAutoCommit(false);// 开启事务管理
// TODO something...
conn.commit();// 提交事务
} catch (Exception e) {
conn.rollback();// 回滚事务
throw new InvoiceApplyException("异常失败");
}

声明式事务

基于AOP面向切面,使用@Transactional注解,将业务与事务处理解耦,代码侵入性很低,所以是最常用的事务管理方式。

@Transactional
@GetMapping("/test")
public String test() {
int insert = cityInfoDictMapper.insert(cityInfoDict);
}

二、@Transactional介绍

1. @Transactional注解作用于哪些地方。

@Transactional作用于接口方法

  • 作用于类: 表示该类的public方法都配置相同的事务属性信息。
  • 作用于方法: 类配置了@Transactional,该类下方法也配置了@Transactional,方法的事务会覆盖类的事务。
  • 作用于接口: 不推荐这种方法,因为一旦标注在Interface上并且配置了Spring AOP使用CGlib动态代理,将会导致@Transactional注解会失效。

2.@Transactional注解属性

propagation 事务的传播属性

  • Propagation.REQUIRED(默认):

如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。(也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务)

  • Propagation.SUPPORTS:

如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。

  • Propagation.MADATORY:

如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。

  • Propagation.REQUIRES_NEW:

重新创建一个新的事务,如果当前存在事务,暂停当前的事务。(当类A中的a方法用默认Propagation.REQUIRED模式,类B中的b方法上采用Propagation.REQUIRES_NEW模式,然后在a方法中调用b方法操作数据库,然而a方法抛出异常,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停a方法的事务)

  • Propagation.NOT_SUPPORTED:

以非事务的方式运行,如果当前存在事务,暂停当前的事务。

  • Propagetion.NEVER:

以非事务的方式运行,如果当前存在事务,则抛出异常。

  • Propagetion.NESTED:

Propagetion.REQUIRED效果一样。

isolation 事务的隔离级别

  • Isolation.DEFAULT:(默认)

使用底层数据库默认的隔离级别。大多数数据库默认的事务隔离级别是Read committed,比如Sql Server,Oracle。Mysql的默认隔离级别是Repeatable read。

  • Isolation.READ_UNCOMMITTED

允许脏读,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过”排他写锁“实现。

  • Isolation.READ_COMMITTED

允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。

  • Isolation.REPEATABLE_READ

禁止不可重复读取和脏读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。

  • Isolation.SERIALIZABLE

它要求事务序列化执行,事务只能一个接着一个的执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

读数据一致性脏读不可重复读幻读
未提交读(READ_UNCOMMITTED)最低级别只能保证不读取物理上损坏的数据✔️✔️✔️
已提交读(READ_COMMITTED)语句级✔️✔️
可重复读(REPEATABLE_READ)事务级✔️
序列化(SERIALIZABLE)最高级别,事务级

对勾、叉代表是否会出现此类情况。

  1. 脏读

又称无效数据读出。一个事务读取另外一个事务还没有提交的数据叫脏读。 例如:事务T1修改了一行数据,但是还没有提交,这时候事务T2读取了被事务T1修改后的数据,之后事务T1因为某种原因Rollback了,那么事务T2读取的就是脏数据。

  1. 不可重复读

同一个事务中,多次读出的同一数据是不一致的。 例如:事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。

  1. 幻读

不好表述直接上例子吧:

在仓库管理中,管理员要给刚到的一批商品进行入库管理,当然入库之前肯定是要查一下之前有没有入库记录,确保正确性。管理员A确保库中不存在该商品之后给该商品进行入库操作,假如这时管理员B因为手快将该商品进行了入库操作。这时管理员A发现该商品已经在库中。就像刚刚发生了幻读一样,本来不存在的东西,突然之间他就有了。

注:三种问题看似不太好理解,脏读侧重的是数据的正确性。不可重复读侧重的是对数据的修改,幻读侧重于数据的新增和删除。

timeout 事务的超时时间

默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。

  • readOnly 是否为只读事务

    默认值为false;为了忽略那些不需要事务的方法,比如读取数据,可以设置成read-only为true

  • rollbackFor 触发事务回滚的异常类型

    rollbackFor用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。

  • noRollbackFor 不触发事务回滚的异常类型

    noRollbackFor:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。

三、@Transactional失效场景

仅举例常见的情况,代码问题千奇百怪,无法覆盖全部。

1.@Transactional应用在非public方法上

编译不会报错,但是自己要注意

2.@Transactional属性propagetion设置错误

若配置以下三种propagation,事务有可能不回滚。

Propagation.SUPPORT

Propagation.NOT_SUPPORTED

Propagetion.NEVER

3.@Transactional注解rollbackbakcFor设置错误

4.同一个类中方法调用,导致@Transactional失效

this.B()不会走代理,因为@Transactional基于动态代理的,不走代理无法实现其事务功能。(可以强行把this换成该对象的bean)

public class Test{

@GetMapping("/A")
public String A(){
String result = this.B();
return result;
}
@GetMapping("/B")
@Transactional(rollbackFor=Exception.class)
public String B(){
String result = "1";
result+="2";
throw new Exception("模拟报错..");
result+="3";
return result;
}
}

5.异常被catch,没有抛出给Spring

这种情况最常见!

建议使用全局异常拦截器,代码正常抛异常,这样异常会抛给spring,也不耽误给前端返回错误信息

6.数据库引擎不支持事务

mysql默认使用innodb引擎,所以基本不会出现这个问题,关于数据库引擎,都使用innodb即可。