页面加载中...

Spring事务使用总结

| Spring Boot | 2 条评论 | 890浏览

Spring对事务的支持

首先,程序是否支持事务,取决于数据库,比如Mysql的Innodb引擎,那么就支持事务;如果是Myisam引擎,那么就根本不支持事务。

Spring支持两种方式的事务管理,一种是编程式,另一种是注解式。编程式对代码入侵太大,一般使用情况较少;基本上都是通过注解方式来进行事务管理。

事务传播行为

事务传播行为是为了解决业务层方法之间互相调用的事务问题。

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

Propagation.REQUIRED

使用的最多的一个事务传播行为,我们平时经常使用的@Transactional注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说:

  1. 如果外部方法没有开启事务的话,Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  2. 如果外部方法开启事务并且被Propagation.REQUIRED的话,所有Propagation.REQUIRED修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。

举个例子:

@Component
@EnableTransactionManagement //相当于配置文件的spring-tx配置,springboot自动包括该注解
public class A {
    @Autowired
    JdbcTemplate jdbcTemplate;
    @Autowired
    B b;
    @Transactional(propagation=Propagation.REQUIRED)
    public void requiredA(){
        b.requiredB(); //1
        jdbcTemplate.update("insert into user(id,name) value (1,'zhangsan')");
        //除于0 ,制造一个程序异常
        int s = 3/0; //2
    }

}
@Component
@EnableTransactionManagement
public class B {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Transactional(propagation=Propagation.REQUIRED)
    public void requiredB(){
        jdbcTemplate.update("insert into user(id,name) value (2,'lisi')");

    }
}

在方法A里面,制造一个异常//2处,此时方法A会回滚,那么方法B也就会回滚。

或者将异常放在方法B中,就算方法A对异常进行了try catch处理,方法B回滚了,那么方法A也一样会回滚。

@Component
@EnableTransactionManagement 
public class A {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    B b;

    @Transactional(propagation=Propagation.REQUIRED)
    public void requiredA(){
       jdbcTemplate.update("insert into user(id,name) value (1,'zhangsan')");
        try{
          //将异常放在B中,就算异常捕获了,A还是会回滚
            b.requiredB();
        }catch (Throwable e){
            System.out.println(e.getMessage());
        }
    }

}
@Component
@EnableTransactionManagement
public class B {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Transactional(propagation=Propagation.REQUIRED)
    public void requiredB(){
        jdbcTemplate.update("insert into user(id,name) value (2,'lisi')");
        //除于0 ,制造一个程序异常
        int s = 3/0; //2
    }
}

Propagation.REQUIRES_NEW

创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

比如上面,我们将方法B的注解改为:@Transactional(propagation=Propagation.REQUIRES_NEW)

@Component
@EnableTransactionManagement //相当于配置文件的spring-tx配置,springboot自动包括该注解
public class A {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    B b;

    @Transactional(propagation=Propagation.REQUIRED)
    public void requiredNewA(){
        b.requiredNewB(); //1
        jdbcTemplate.update("insert into user(id,name) value (1,'zhangsan')");
        //除于0 ,制造一个程序异常
        int s = 3/0; //2
    }

}
@Component
@EnableTransactionManagement
public class B {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void requiredNewB(){
        jdbcTemplate.update("insert into user(id,name) value (2,'lisi')");
    }
}

此时,方法A抛出了异常,方法A会回滚,但是方法B不会回滚,因为方法B是独立的事务。 同理,如果方法B回滚,同样也不会对方法A造成影响,比如:

@Component
@EnableTransactionManagement //相当于配置文件的spring-tx配置,springboot自动包括该注解
public class A {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    B b;

    @Transactional(propagation=Propagation.REQUIRED)
    public void nestedA(){
        b.nestedB();
        jdbcTemplate.update("insert into user(id,name) value (1,'zhangsan')");
        //除于0 ,制造一个程序异常
        int s = 3/0;
    }

}
@Component
@EnableTransactionManagement
public class B {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Transactional(propagation=Propagation.NESTED)
    public void nestedB(){
        jdbcTemplate.update("insert into user(id,name) value (2,'lisi')");
    }
}

此时,方法A回滚,那么方法B也就会回滚。

如果将异常放在方法B里面:

@Component
@EnableTransactionManagement //相当于配置文件的spring-tx配置,springboot自动包括该注解
public class A {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    B b;

    @Transactional(propagation=Propagation.REQUIRED)
    public void nestedA(){
        b.nestedB();
        jdbcTemplate.update("insert into user(id,name) value (1,'zhangsan')");
        
    }

}
@Component
@EnableTransactionManagement
public class B {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Transactional(propagation=Propagation.NESTED)
    public void nestedB(){
        jdbcTemplate.update("insert into user(id,name) value (2,'lisi')");
        //除于0 ,制造一个程序异常
        int s = 3/0;
    }
}

Propagation.NESTED

如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。也就是说:

它大部分情况都等价于Propagation.REQUIRS_NEW,但是: 如果主事务回滚了,那么NESTED也会回滚。比如:

@Component
@EnableTransactionManagement //相当于配置文件的spring-tx配置,springboot自动包括该注解
public class A {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    B b;

    @Transactional(propagation=Propagation.REQUIRED)
    public void nestedA(){
        b.nestedB(); //1
        jdbcTemplate.update("insert into user(id,name) value (1,'zhangsan')");
        //除于0 ,制造一个程序异常
        int s = 3/0; //2
    }

}
@Component
@EnableTransactionManagement
public class B {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Transactional(propagation=Propagation.NESTED)
    public void nestedB(){
        jdbcTemplate.update("insert into user(id,name) value (2,'lisi')");
    }
}

此时方法A产生了异常,出现了回滚,那么方法B也会回滚。

Propagation.MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性) 这个一般用得的比较少。 比如方法A有事务:

@Component
@EnableTransactionManagement //相当于配置文件的spring-tx配置,springboot自动包括该注解
public class A {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    B b;

   @Transactional(propagation=Propagation.REQUIRED) //1
    public void mandatoryA(){
        b.mandatoryB();
        jdbcTemplate.update("insert into user(id,name) value (1,'zhangsan')");

    }

}
@Component
@EnableTransactionManagement
public class B {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Transactional(propagation=Propagation.MANDATORY)
    public void mandatoryB(){
        jdbcTemplate.update("insert into user(id,name) value (3,'wangwu')");
    }
}

此时程序正常,未见异常。 如果将//1处,方法A的@Transactional注解去掉,再次运行执行方法A,就会出现如下异常:

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'

Propagation.NOT_SUPPORTED

表示不支持事务,方法不会以事务的方式进行。如果有事务存在,将它挂起,以无事务状态运行 。 无事务,就是指底层的Connection对象的autoCommit、isolation等属性与数据库有关,与dataSource设置的属性有关,不会被Spring改变。

Transactional注解

Transactional注解中常用的配置参数

属性名说明
propagation事务的传播行为,默认值为 REQUIRED,上面已说过
isolation事务的隔离级别,默认值采用 DEFAULT , 下面会说到
timeout事务的超时时间,单位秒,默认值为-1(不会超时)。
readOnly指定事务是否为只读事务,默认值为 false。
rollbackFor用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。

事务回滚规则

默认情况下,事务在遇到运行时异常(RuntimeException)及Error错误就会回滚。 在遇到检查型(checked)异常不会回滚,比如这样:

    @Transactional
    public void rollbackForA() throws IOException {
        jdbcTemplate.update("insert into user(id,name) value (2,'abc')");
        throw new IOException("123");
    }

那么该事务不会回滚。

这时可以指定事务回滚规则,在@Transcational注解中添加rollbackFor属性:

    @Transactional(rollbackFor = IOException.class)
    public void rollbackForA() throws IOException {
        jdbcTemplate.update("insert into user(id,name) value (2,'abc')");
        throw new IOException("123");
    }

此时该事务就会回滚。

事务超时时间

timeou,单位秒,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。

比如,指定事务时间为2秒,下面这个sql执行需要3秒,那么事务就会回滚。

    @Transactional(timeout = 2)
    public void timeoutA(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        jdbcTemplate.update("insert into user(id,name) value (2,'123')");
    }

此时事务超时,回滚并会抛出一个异常:

org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Wed Jul 15 10:32:14 CST 2020

事务只读属性

readOnly,对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。

如果指定了readOnly=true,但还是update语句,那么就会抛出异常并回滚。比如:

    @Transactional(readOnly=true)
    public void readOnlyA(){
        jdbcTemplate.update("insert into user(id,name) value (2,'123')");
    }

此时就会抛出异常:

org.springframework.dao.TransientDataAccessResourceException: StatementCallback; SQL [insert into user(id,name) value (2,'123')]; Connection is read-only. Queries leading to data modification are not allowed.; nested exception is java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed.
  • 只读为什么也要有事务?

如果不用事务,即不加@Tranactional,那么每条sql会单独开启一个事务。考虑这么个情况,执行n个表的查询语句,其中一些表此时正被其他事务修改,那么你读到的数据就会出现不一致的情况。

如果给方法加上了Transactional注解的话,这个方法执行的所有sql会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。

事务隔离级别

Spring有一个枚举类Isolation,专门定义事务隔离级别。

public enum Isolation {
    DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
    READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
    READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
    REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
    SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);

    private final int value;
    Isolation(int value) {
        this.value = value;
    }
    public int value() {
        return this.value;
    }

}
  • DEFAULT:使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别;
  • READ_UNCOMMITTED:最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读;
  • READ_COMMITTED : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生;
  • REPEATABLE_READ : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生;
  • SERIALIZABLE : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别;

Spring 事务失效的几种情况

@Transactional只有在public方法才起作用

@Transactional注解作用的方法,private方法直接就编译出错,protected方法注解不会生效。比如:

    @Transactional
    protected void pA(){
        jdbcTemplate.update("insert into user(id,name) value (2,'lisi')");
        int s =3/0;
    }

该方法的事务不会生效

自调用

在一个非事务方法里调用一个事务方法,此时事务方法的事务就会失效。比如:

@Component
@EnableTransactionManagement
public class TxInvalidMaster {

    @Autowired
    JdbcTemplate jdbcTemplate;

    public void a(){
        jdbcTemplate.update("insert into user(id,name) value (1,'zhangsan')");
        b();
    }

    @Transactional
    public void b(){
        jdbcTemplate.update("insert into user(id,name) value (2,'lisi')");
        int s =3/0;
    }
}

执行方法a,会发现方法b的事务并没有生效。

属性rollbackFor指定异常不正确

比如,下面方法抛出的是IoException,但是rollbackFor却是SQLException,那么此时事务也不会回滚

    @Transactional(rollbackFor = SQLException.class)
    public void rollbackForA() throws IOException {
        jdbcTemplate.update("insert into user(id,name) value (2,'lisi')");
        throw new IOException("123");
    }

参考文章

发表评论

最新评论

  1. 热心网友

    1024 up up up 节日快乐!

  2. 热心网友

    cool