Spring 中的事务传播

概述

Spring中, Java方法的事务传播类型通过
@Transactional 注解进行指明, 并通过该注解的 propagation 属性指明事务传播的具体类型.

@Transactional 注解的使用非常灵活, 可以注解在服务接口上, 也可以注解在服务类的方法上, 还可以注解在Spring Repository的接口方法上.

@Transactional(propagation = Propagation.REQUIRED)

各种传播类型的说明

Spring 中一共有七中事务传播类型. 分别说明如下:

一. Propagation.REQUIRED

如果当前已经存在事务, 那么加入该事务, 如果不存在事务, 创建一个事务, 然后执行事务操作. 这是默认的传播属性值. 也是最常见的选择.

何谓当前?

当前也就是你所声明的服务方法被调用的时候.

何谓已经存在事务?

也就是说在声明的事务方法被调用的时候, 就已经在一个事务当中了. 比如下面的伪代码:

@Transactional
public void service(){
    serviceA();
    serviceB();
}
 
@Transactional
serviceA();
@Transactional
serviceB();

service() 方法开启了一个事务, 当 serviceA(); serviceB(); 被调用的额时候回加入 service() 所在的事务上下文.

注意: @Transactional 没有指明 propagation 属性, 取默认值 Propagation.REQUIRED

含义为: 要求的, 必须的. 如果被注解的方法有这个传播属性, 它的行为是:

  1. 如果它作为一个子事务方法, 在其他事务方法中被调用, 那么该方法不会创建新的事务, 使用现有的父级别的事务.
  2. 如果它作为一个子事务方法, 没有在其他事务方法中被调用, 而是在非事务方法中直接调用, 那么它会创建一个新的事务来执行数据库操作.

归纳为: 有就用, 没有就创建(事务).

也就可以理解 Propagation.REQUIRED 的意思了 — 要求的. 不管怎样它能够保证操作总是在一个事务中进行的.

应用场景: 不知道方法的调用者是否创建了事务, 但是要求当前被调用的方法必须在一个事务当中执行.

Propagation.MANDATORY

支持当前事务,如果当前没有事务,就抛出异常

对于这个类型, 它一般作为一个事务的一部分定义. 不能独立执行. 一般作为一个事务中的子操作. 比如经典的转账作为例子.

两个账户 AB 之间转账, 从 A1000B , 拆分为两个操作, 并且要在一个事务中执行.

操作1: 先把 1000 加到 B 账户上, 如果 B 账户增加成功, 执行操作2
操作2: 如果 B 账户增加成功, 那么 A 账户扣除 1000.

当两个操作同时成功时, 事务执行成功, 否则, 任意一步错误, 回滚之前的全部操作. 那么对应的操作1和操作2我们可以实现为两个 Propagation.MANDATORY 类型的Java方法. 并且把这两个方法放在一个 Propagation.REQUIRED 类型的方法中, 例如:

// 服务接口

interface AccountService {
    TransactionLog transfer(Long accountA, Long accountB, BigDecimal amount);
}

// 服务实现

@Service
class AccountServiceImpl implements AccountService {
    // 父事务, 用于组织多个子事务.
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public TransactionLog transfer(Long accountA, Long accountB, BigDecimal amount) {
        // 先加后减
        addAmount(accountB, amount)
        addAmount(accountA, amount.negate())
    }
}

// 数据库访问对象

@Repository
class interface AccountRepository extends JpaRepository<Account, Long> {
        // 余额操作(自增, 自减)
    @Modifying
    @Transactional(propagation = Propagation.MANDATORY)
    @Query(value = "UPDATE account a SET a.balance = a.balance + ?2 WHERE a.id = ?1")
    void addAmount(Long id, BigInteger amount);
}

Propagation.REQUIRES_NEW

新建事务,如果当前存在事务,把当前事务挂起

特征: 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全
提交
回滚 而不依赖于外部事务,它拥有自己的隔离范围, 自己的锁, 等等.当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行.
Propagation.REQUIRES_NEW 常用于日志记录,或者交易失败仍需要留痕.

还有就是 时序控制, 支付依赖于已经创建的订单, 无订单不支付. 先要有订单才能支付. 常见于事务步骤 要求时序 的情况.

开启一个全新的事务, 一般用于分布式事务中的一个前期步骤. 比如一键购功能. 主要步骤包括:

下面的伪代码模拟了一个外层事务和内层事务的业务过程.

// 一键购服务
class BuyService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void buyDirectly() {
        Order OrderService.createOrder(OrderDto orderDto);
        PayService.pay(PayDto payDto);
    }
}
interface OrderService {
    void createOrder(OrderDto orderDto);
}
interface PayService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    Payment pay(PayDto payDto);
}

OrderService.createOrder 运行在外层事务中, 如果创建订单失败, 就没必要发起支付了, 直接回滚了, 根本就到不了支付这一步.

当绑定的银行卡余额不足的情况, PayService.pay(); 是可以回滚的, 而不会影响 buyDirectly 整个事务, OrderService.createOrder(); 成功. 向银行卡不足金额后, 可以重新发起支付, 完成购买过程.

注意:
Propagation.REQUIRES_NEW 如果作为一个子事务运行, 调用者和被调这不要在同一个服务类中(因为Spring AOP动态代理的限制, 在同一个类中事务是不起作用的)

Propagation.REQUIRES_NEW 的一般使用场景是作为内层事务可以单独回滚. 而不是回滚整个外层事务. 因此如果调用者和被调用者如果在一个类中, Propagation.REQUIRES_NEW 注解的方法并 不会 开启一个新的事务. 因此就达不到内层事务单独回滚的目的.

归纳: 内层事务可以独立回滚, 不影响外层事务.
前提是外层事务的方法不能和内层事务的方法在同一个服务类中

Propagation.NOT_SUPPORTED

以非事务方式执行操作, 如果当前存在事务, 就把当前事务挂起

Propagation.NEVER

如果当前存在事务, 则抛出异常, 否则在无事务环境上执行代码

对于方法中只存在只读操作, 我们可以使用这个. 一般事务注解为:

@Transactional(propagation = Propagation.NEVER, readOnly = true)

Propagation.NESTED

嵌套事务, 它的作用相当于 Propagation.REQUIRED 和 Propagation.REQUIRES_NEW 的合体

特征: Propagation.NESTED 启动一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个存储点(Savepoint). 如果这个嵌套事务失败, 我们将回滚到此 存储点. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交. 由此可见, Propagation.REQUIRES_NEWPropagation.NESTED 的最大区别在于, Propagation.REQUIRES_NEW 完全是一个新的事务, 而 Propagation.NESTED 则是外部事务的子事务, 如果外部事务提交, 嵌套事务也会被,这个规则同样适用于回滚.

注意: 使用此种事务传播类型, 需要设置事务管理器的 nestedTransactionAllowed 属性为 true

/**
 * TransactionConfig.java
 *
 * 事务配置
 */
@Configuration
@EnableTransactionManagement
public class Transaction {
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        transactionManager.setNestedTransactionAllowed(true);
        return transactionManager;
    }
}

Propagation.SUPPORTS

表示当前方法不必需要具有一个事务上下文, 但是如果有一个事务的话, 它也可以在这个事务中运行

归纳: 有没有父级事务(外层事务)都可以接受

这个类型一般用于不会修改(UPDATE, DELETE)数据库状态的Java方法里面. 如果说在上述 AccountServiceImpl 实现类的 transfer 方法中还要通过调用其他的方法(包含SELECT语句)去返回某些数据,那么该方法可以标注为 Propagation.SUPPORTS 类型.

总结

关键字: 创建, 挂起, 恢复, 非事务执行, 事务执行.

@Transactional(propagation=Propagation.REQUIRED)
如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)

@Transactional(propagation=Propagation.NOT_SUPPORTED)
容器不为这个方法开启事务

@Transactional(propagation=Propagation.REQUIRES_NEW)
不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务

@Transactional(propagation=Propagation.MANDATORY)
必须在一个已有的事务中执行,否则抛出异常

@Transactional(propagation=Propagation.NEVER)
必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)

@Transactional(propagation=Propagation.SUPPORTS)
如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.

参考资料

    原文作者:developerworks
    原文地址: https://segmentfault.com/a/1190000015794446
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞