Spring事务@Transactional的说明和使用举例

一、事务简单介绍

事务具有四大特性ACID:分别指:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。

1.1 事务ACID
  • 原子性(Atomicity):指同一个事务中的操作要么都执行,要么都回滚,回滚后就像事情没有发生过一样。

  • 一致性(Consistency):数据库数据在执行前处于正确状态,在执行后也要处于正确状态,即不能破坏数据的完整性约束。比如A转账100给B,不能出现A的钱最后减100,B的钱却没有加100。一致性还可分为强一致性和最终一致性。一般来说,关系型数据库需要保证强一致性,而NoSQL数据库需要保证数据最终一致性。

  • 隔离性(Isolation):指并发事务执行互不干扰,sql规范定义了4种隔离级别,分别为脏读、读写提交、可重复读、序列化。不同隔离级别对事务影响不同。

  • 持久性(Durability):指事务对数据的修改是永久不可改变的。

1.2 Spring事务属性

来看看Spring事务定义:

public interface TransactionDefinition {
	/**
     * 返回传播行为
     */
	int getPropagationBehavior();
	/**
     * 返回事务隔离级别
     */
	int getIsolationLevel();
	/**
     * 返回事务超时时间
     */
	int getTimeout();
	/**
     * 返回是否优化为只读事务。
     */
	boolean isReadOnly();
}

除此之外还有rollbackFor、noRollbackFor等配置,这些配置属性组成了事务执行策略,包含以下五个策略:传播行为,隔离规则,回滚规则,超时时间,是否只读。

1.2.1 传播行为

事务的传播行为指的是当一个事务方法被另一个事务方法调用,存在多个事务操作时,如何处理这些事务行为,比如是用同一个事务还是开启新的事务。TransactionDefinition中定义了七种传播行为:

传播行为说明
PROPAGATION_REQUIRED支持当前事务,如果当前不存在事务则新建一个事务,通常这是Spring默认的传播行为
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,则以非事务方式执行
PROPAGATION_MANDATORY支持当前事务,如果当前事务不存在则会抛出一个异常
PROPAGATION_REQUIRES_NEW新建一个事务,如果当前存在一个事务,则将当前事务挂起。新建的事务和当前事务是两个独立的事务,可通过捕获新建事务执行异常判断当前事务是否需要回滚操作
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务中执行。如果当前不存在事务,则执行类似PROPAGATION_REQUIRED操作
1.2.2 隔离规则

事务的隔离级别控制一个事务在并发情况下可能受到的影响程度。并发情况下,事务可能出现以下几个问题:

  • 脏读(Dirty reads):A事务更新了数据,但未提交事务,B读取了A更新的数据,当A事务更新的数据回滚了,B读取到的数据就是脏数据。
  • 不可重复读(Nonrepeatable read):A事务多次读取同一个数据,在读取过程中B事务将数据更新了,导致A事务多次读取的同一个数据不一致。
  • 幻读(Phantom read):A事务对一定范围内数据进行批量修改,在过程中B事务在这个范围内插入了一条新的数据,最后导致批量修改操作完成后再次查询相同范围的数据,发现没有修改到B事务插入的数据,就像出现了幻觉一样。

总结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。

为了避免并发情况下出现的脏读,不可重复读,幻读的问题,Spring在TransactionDefinition中定义了五种隔离规则:

隔离级别说明脏读不可重复读幻读
ISOLATION_DEFAULT使用数据库默认的隔离级别默认默认默认
ISOLATION_READ_UNCOMMITTED允许读取事务尚未提交的数据,是最低的隔离级别
ISOLATION_READ_COMMITTED允许读取事务已经提交的数据不会
ISOLATION_REPEATABLE_READ多次读取结果都一致,除非结果由自己修改不会不会
ISOLATION_SERIALIZABLE最高的隔离级别,通过锁表实现不会不会不会

总结:实际应用中,最高隔离级别ISOLATION_SERIALIZABLE一般很少使用,因为锁表会导致其他事务执行时直接报错。一般采用READ_COMMITTED或者REPEATABLE_READ隔离级别,如MySQL默认的隔离级别是REPEATABLE-READ,Oracle默认的隔离级别是READ_COMMITTED。

1.2.3 回滚规则

回滚规则是定义哪些异常会回滚,哪些不会回滚事务。Spring事务默认捕获到未检查异常会回滚事务,其中包括RuntimeException和Error异常。你也可以使用rollbackFor自定义哪些异常会回滚事务、noRollbackFor自定义哪些异常不会回滚事务。

1.2.4 超时时间

为了合理利用数据库资源,设置事务执行超时时间的设置是有必要的,@Transactional注解的timeout属性可设置事务超时时间,超过设置的时间事务会自动回滚。
这里小插一句,timeout设置的超时时间容易使人产生错误的理解,简单总结就是:Spring事务超时 = 事务开始时到最后一个Statement创建时时间 + 最后一个Statement的执行时超时时间(即其queryTimeout)。

1.2.5 是否只读

数据库引擎可以根据客户端的事务规则优化事务,提升数据库效率。如果设置@Transactional注解的属性(readOnly = true),表示当前事务只会读取数据库数据,而不会更新数据。

二、@Transactional注解

2.1 简单介绍
  • Spring 事务管理分为编码式和声明式的两种,我们这里主要讲声明式事务实现方式。声明时事务基于AOP,本质是对事务方法执行前后进行拦截,将目标方法加入到事务中。可以通过xml配置方式和@Transactional注解方式实现,很明显注解方式更优雅。
  • @Transactional注解 可以作用于接口、接口方法、类以及类方法上。由于基于AOP,因此用于方法上时,必须是public方法才会生效,使用到其他类型方法上也不会抛异常,只是会自动忽略。
  • 默认情况下,也是由于AOP动态代理的缘故。类中的方法若没有来自外部的方法调用;或者同一个类中有来自外部方法调用却没有添加@Transactional注解,再调用本类内部添加了@Transactional注解的其他方法,事务均不会生效。这里理解起来可能比较绕, 2.3 的使用举例会详细说明注解生效规则。
2.2 注解属性

我们来看看@Transactional注解中定义的属性:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
	/**
     * 当有多个transactionManager配置时,可以指定Manager
     */
	@AliasFor("transactionManager")
	String value() default "";
	/**
     * 作用同上
     */
	@AliasFor("value")
	String transactionManager() default "";
	/**
     * 传播行为
     */
	Propagation propagation() default Propagation.REQUIRED;
	/**
     * 隔离级别
     */
	Isolation isolation() default Isolation.DEFAULT;
	/**
     * 超时时间
     */
	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
	/**
     * 是否为只读事务
     */
	boolean readOnly() default false;
	/**
     * 指定触发事务回滚的异常类型
     */
	Class<? extends Throwable>[] rollbackFor() default {};
	/**
     * 作用同上,指定类名
     */
	String[] rollbackForClassName() default {};
	/**
     * 指定触发事务不回滚的异常类型
     */
	Class<? extends Throwable>[] noRollbackFor() default {};
	/**
     * 作用同上,指定类名
     */
	String[] noRollbackForClassName() default {};
}
  • value()和transactionManager()属性可以指定存在多个TransactionManager的其中一个qualifier属性值或者bean名称。
  • propagation()属性可以设置Propagation枚举类中的七个传播行为,参考1.2.1,默认为REQUIRED。
  • isolation()属性可以设置Isolation枚举类中的五个隔离级别,参考1.2.2,默认为DEFAULT。
  • timeout()设置事务超时时间,默认不超时。
  • readOnly()设置事务是否是只读事务。
  • rollbackFor()和rollbackForClassName()设置哪些异常事务会回滚。
  • noRollbackFor()和noRollbackForClassName()设置哪些异常事务不会回滚。
2.3 使用举例

注:以下举例仅第一个例子是完整代码,后面均省略公共代码

@Data
@Entity
@Table(name = "sal_emp")
public class SalEmpEntity {
    public SalEmpEntity() { }
    public SalEmpEntity(Integer id, String name) {
        this.id = id;
        this.name = name;
    }
    @Id
    @Column(name = "id")
    private Integer id;
    @Column(name = "name")
    private String name;
}
@Repository
public interface SalEmpRepository extends CrudRepository<SalEmpEntity, Integer> { }
public interface IAClass {
    void aFunction();
    void aInnerFunction();
}
2.3.1 同类不同方法

情况一:aFunction添加@Transactional注解,aInnerFunction函数不添加,aInnerFunction抛异常。

@Slf4j
@Service
public class AClassImpl implements IAClass {

    @Autowired
    private SalEmpRepository salEmpRepository;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void aFunction() {
        SalEmpEntity entity1 = new SalEmpEntity(4, "name1");
        salEmpRepository.save(entity1);
        this.aInnerFunction();
    }

    @Override
    public void aInnerFunction() {
        SalEmpEntity entity2 = new SalEmpEntity(5, "name2");
        salEmpRepository.save(entity2);
        throw new RuntimeException("occur Exception!");
    }
}

结果:两个函数的操作都回滚。

情况二:aFunction添加@Transactional注解,aInnerFunction函数不添加,aInnerFunction抛异常,不过在aFunction里面把异常捕获了。

@Transactional(rollbackFor = Exception.class)
@Override
public void aFunction() {
    SalEmpEntity entity1 = new SalEmpEntity(4, "name1");
    salEmpRepository.save(entity1);
    try {
        this.aInnerFunction();
    } catch (Exception e) {
        log.info(e.getMessage());
    }
}

@Override
public void aInnerFunction() {
    SalEmpEntity entity2 = new SalEmpEntity(5, "name2");
    salEmpRepository.save(entity2);
    throw new RuntimeException("occur Exception!");
}

结果:日志打印“occur Exception!”,并且两个方法均不会回滚,因为@Transactional需要捕获到异常才会回滚事务,例子中使用try catch将异常捕获了,并且没有继续往上抛异常。

情况三:aFunction和aInnerFunction均添加注解,aInnerFunction抛异常。

@Transactional(rollbackFor = Exception.class)
@Override
public void aFunction() {
    SalEmpEntity entity1 = new SalEmpEntity(4, "name1");
    salEmpRepository.save(entity1);
    this.aInnerFunction();
}

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
@Override
public void aInnerFunction() {
    SalEmpEntity entity2 = new SalEmpEntity(5, "name2");
    salEmpRepository.save(entity2);
    throw new RuntimeException("occur Exception!");
}

结果:和情况一相同,事务回滚,因为同类方法相互调用,只有外部方法的注解生效,内部添加的注解无效。

情况四:aFunction不添加注解,aInnerFunction添加注解,aInnerFunction抛异常。

@Override
public void aFunction() {
    SalEmpEntity entity1 = new SalEmpEntity(4, "name1");
    salEmpRepository.save(entity1);
    this.aInnerFunction();
}

@Transactional(rollbackFor = Exception.class)
@Override
public void aInnerFunction() {
    SalEmpEntity entity2 = new SalEmpEntity(5, "name2");
    salEmpRepository.save(entity2);
    throw new RuntimeException("occur Exception!");
}

结果:事务不会回滚,因为只有外部方法的注解生效,内部添加的注解不会生效。

2.3.2 不同类不同方法

新增加IBClass接口:

public interface IBClass {
    void bFunction();
}

情况一:aFunction添加注解,bFunction不添加注解,bFunction抛异常。

@Slf4j
@Service
public class AClassImpl implements IAClass {
    @Autowired
    private SalEmpRepository salEmpRepository;
    @Autowired
    private IBClass bClass;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void aFunction() {
        SalEmpEntity entity1 = new SalEmpEntity(17, "name5");
        salEmpRepository.save(entity1);
        bClass.bFunction();
    }
}

@Slf4j
@Service
public class BClassImpl implements IBClass {
    @Autowired
    private SalEmpRepository salEmpRepository;

    @Override
    public void bFunction() {
        SalEmpEntity entity2 = new SalEmpEntity(18, "name6");
        salEmpRepository.save(entity2);
        throw new RuntimeException("occur Exception!");
    }
}

结果:两个操作都回滚

情况二:aFunction、bFunction都添加注解,bFunction抛异常。

@Transactional(rollbackFor = Exception.class)
@Override
public void aFunction() {
    SalEmpEntity entity1 = new SalEmpEntity(17, "name5");
    salEmpRepository.save(entity1);
    bClass.bFunction();
}

@Transactional(rollbackFor = Exception.class)
@Override
public void bFunction() {
    SalEmpEntity entity2 = new SalEmpEntity(18, "name6");
    salEmpRepository.save(entity2);
    throw new RuntimeException("occur Exception!");
}

结果:两个操作都会回滚,因为两个方法用的还是同一个事务

情况三:aFunction、bFunction都添加注解,bFunction抛异常,aFunction捕获抛出的异常。

@Transactional(rollbackFor = Exception.class)
@Override
public void aFunction() {
    SalEmpEntity entity1 = new SalEmpEntity(17, "name5");
    salEmpRepository.save(entity1);
    try {
        bClass.bFunction();
    } catch (Exception e) {
        log.info(e.getMessage());
    }
}
    
@Transactional(rollbackFor = Exception.class)
@Override
public void bFunction() {
    SalEmpEntity entity2 = new SalEmpEntity(18, "name6");
    salEmpRepository.save(entity2);
    throw new RuntimeException("occur Exception!");
}

结果:首先结论是都回滚。但是控制台还打印了报错信息:“org.springframework.orm.jpa.JpaSystemException: Transaction was marked for rollback only; cannot commit; nested exception is org.hibernate.TransactionException: Transaction was marked for rollback only; cannot commit”。(我这里使用的是Spring Data JPA,因此是jpa打印的堆栈信息),我们只用关注标黄文字(事务已经被标记成rollback only,不能提交)。其实这句话很好理解,bFunction里面出现异常,会立马调用事务的rollback函数,事务就被标记成了rollback only状态。然后由于aFunction catch了异常,而且没有抛出将异常抛出,事务捕获不到异常,最后会调用commit函数。由于是同一个事务,但事务状态之前已经被标记成了rollback only,再提交就会异常。

情况四:aFunction、bFunction都添加注解,bFunction抛异常,bFunction @Transactional注解加了参数propagation = Propagation.REQUIRES_NEW,控制事务的传播行为。

@Transactional(rollbackFor = Exception.class)
@Override
public void aFunction() {
    SalEmpEntity entity1 = new SalEmpEntity(17, "name5");
    salEmpRepository.save(entity1);
    try {
        bClass.bFunction();
    } catch (Exception e) {
        log.info(e.getMessage());
    }
}
    
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
@Override
public void bFunction() {
    SalEmpEntity entity2 = new SalEmpEntity(18, "name6");
    salEmpRepository.save(entity2);
    throw new RuntimeException("occur Exception!");
}

结果:aFunction中由于没有异常抛出,save成功;而bFunction事务回滚;与情况三的差别是bFunction的注解添加了参数propagation = Propagation.REQUIRES_NEW,表明是新建事务。既然都是不同事务了,也就不会报错,能顺利回滚事务。

初次使用Spring事务一定要理解清楚其中的原理,特别是使用注解方式来控制事务,一定要对事务传播机制和隔离级别有详细的理解,不然在使用中可能会出现一些莫名其妙的问题不好分析。以上例子基本覆盖了所有情况,了解规则后使用就不会出错了。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值