解析Spring事务传播行为及常用属性

257 阅读6分钟

前言

在日常开发中,我们经常会使用Spring框架来管理事务。只需简单地在方法上添加@Transactional注解,即可开启事务管理。然而,除了基本的开启和关闭事务,Spring还提供了丰富的事务传播行为配置,尤其是propagation参数,它决定了在已有事务的情况下如何处理嵌套事务。本文将详细探讨Spring中的事务传播行为,包括其概念、作用及七种具体传播行为,并通过案例分析,重点介绍PROPAGATION_REQUIREDPROPAGATION_REQUIRES_NEW两种常用传播行为的差异和应用场景。

七种事务传播行为

首先先介绍一下事务的传播行为,Spring 事务传播行为是指在一个事务已经存在的情况下,如何处理嵌套事务。Spring 支持 7 种事务传播行为,分别是:

  1. PROPAGATION_REQUIRED(默认):如果当前没有事务,就创建一个新的事务。如果已经存在一个事务,就加入到这个事务中。这是最常见的选择,适用于大多数情况。
  2. PROPAGATION_SUPPORTS:如果当前没有事务,就以非事务方式执行。如果已经存在一个事务,就加入到这个事务中。适用于支持事务的操作,但不需要事务管理。
  3. PROPAGATION_MANDATORY:如果当前没有事务,就抛出异常。如果已经存在一个事务,就加入到这个事务中。适用于必须在事务中执行的操作。
  4. PROPAGATION_REQUIRES_NEW:始终创建一个新的事务。如果当前存在事务,就将当前事务挂起,然后创建一个新的事务。适用于需要独立于其他事务执行的操作。
  5. PROPAGATION_NOT_SUPPORTED:以非事务方式执行。如果当前存在事务,就将当前事务挂起。适用于不支持事务的操作。
  6. PROPAGATION_NEVER:如果当前存在事务,就抛出异常。以非事务方式执行。适用于禁止事务的操作。
  7. PROPAGATION_NESTED:如果当前没有事务,就创建一个新的事务。如果已经存在一个事务,就创建一个嵌套事务。嵌套事务可以独立于外部事务提交或回滚。适用于需要独立于外部事务执行,但又需要保持与外部事务的关联的操作。

在选择事务传播行为时,需要根据具体的业务场景和需求来决定。通常情况下,使用默认的 PROPAGATION_REQUIRED 就足够了。在需要更细粒度的控制事务传播时,可以考虑使用其他的传播行为。

REQUIRED和REQUIRES_NEW案例演示

在实际开发中,PROPAGATION_REQUIREDPROPAGATION_REQUIRES_NEW是最常用的两种传播行为。接下来就开始讲解这两种不同传播行为在实际开发中的应用场景。

案例一:允许不同事务单独提交

允许不同事务单独提交其实这种场景,在实际开发中很少出现,我们利用事务就是保证整个业务数据一致性,不提供事务单独提交,会出现某个事务回滚了,但是另一个事务继续提交,就有可能破坏数据一致性。但是,恶心的面试官,可能就会问这种问题,比如下面这道面试题:

请在此添加图片描述

这道题的意思是,要实现insertB回滚,但是insertA照常提交事务,不受insertB影响。很显然上面的方案是不能做到,因为事务注解@Transation在整个类中,说明这个类都是一样的事务特性,由于事务是基于动态代理,也等于都有TestService这个代理处理事务,所以insertB出现异常回滚肯定会导致insertA回滚,那么应该怎么处理呢?

这道题其实有两种解法:

第一种:同个事务代理类

事务注解分到每个方法,insertB捕获业务处理,不返回异常,直接吃掉,等于事务失效。同时insertA捕获insertB,也不处理异常,这样就能保证insertB出现异常了,不向上抛出,但是insertA捕获,发现没异常,不会回滚,insertA就会照样执行。

@Service
public class TestService {
    @Autowired
    private JdbcTemplate jt;

    @Transactional
    public void insertA() {
        try {
            jt.execute("insert into a(m,n)values(1,2)");
            insertB();
        } catch (Exception e) {
            // 处理异常
        }
    }

    @Transactional
    public void insertB() {
        try {
            jt.execute("insert into b(h,i)values(1,2)");
        } catch (Exception e) {
            // 不返回异常,直接吃掉,事务失效
            // 处理异常
        }
    }
}

方便测试,将上面jdbc处理改成service层处理,并且在insertB中加个运行时异常: int a= 1/0;

@Service
public class TestService {
//    @Autowired
//    private JdbcTemplate jt;

    @Autowired
    private ddd ddd;
    @Autowired
    private SignLogService signLogService;
    @Autowired
    private LotteryService lotteryService;
    @Transactional
    public void insertA() {
        try {
//            jt.execute("insert into a(m,n)values(1,2)");
            SignLog signLog = new SignLog();
            signLog.setUid("1233");
            signLogService.save(signLog);
            insertB();
        } catch (Exception e) {
            // 处理异常
        }
    }

    public void insertB() {
        try {
//            jt.execute("insert into b(h,i)values(1,2)");
            Lottery lottery = new Lottery();
            lottery.setTopic("SDEF");
            int a= 1/0;
            lotteryService.save(lottery);
        } catch (Exception e) {
            // 处理异常
        }
    }

结果发现 signLogService可以成功保存数据,但是lotteryService不会保存数据,出现了insertB回滚,insert不回滚。

signLogService保存用户id1233成功

请在此添加图片描述

lotteryService保存主题SDEF失败

请在此添加图片描述

第二种:不同事务代理类 + REQUIRES_NEW

第二种方式就是使用REQUIRES_NEW传播属性,让insertB方法新建一个新的独立事务,配Propagation.REQUIRES_NEW,

其实看起来还是跟第一种方式一样,insertB吃掉了异常,不抛出,实际insertB没有设置成功Propagation.REQUIRES_NEW

的。这个后面讲解REQUIRED和REQUIRES_NEW异常回滚的时候在分析一下

  @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void insertA() {
//            jt.execute("insert into a(m,n)values(1,2)");
            SignLog signLog = new SignLog();
            signLog.setUid("1233444");
            signLogService.save(signLog);

        insertB();

    }

    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    public void insertB() {
        try {
         //   jt.execute("insert into b(h,i)values(1,2)");
        Lottery lottery = new Lottery();
        lottery.setTopic("SDEF111");
        lottery.setStartTime(new Date());
        int a= 1/0;
        lotteryService.save(lottery);
        } catch (Exception e) {
            // 处理异常
        }
    }

案例二:多数据源事务传播处理

在涉及多个数据源的场景下,Spring的默认事务管理可能无法满足需求。例如,在抽奖活动中,奖品领取操作需要同时更新两个不同数据源的数据。这时,我们可以使用PROPAGATION_REQUIRES_NEW来确保每个数据源的操作都在独立的事务中进行。

@Transactional(rollbackFor = Exception.class)
public ActivityPrize award(Integer appId, String uid, Integer drawId) {
   // 当前数据源
    doAwardDiamond(uid, diamond);
    try {
    // 另一个数据源
        if (Boolean.FALSE.equals(lotteryService.awardPrize(appId, uid, drawId, String.valueOf(orderId)))) {
            throw new GenericAppException(ErrorCode.ACTIVITY_AWARD_FAILED, "activity_award_failed");
        }
    } catch (Exception e) {
        throw new GenericAppException(ErrorCode.ACTIVITY_AWARD_FAILED, "activity_award_failed");
    }
    return prize;
}

lotteryService.awardPrize这个方法使用也是@Transactional(rollbackFor = Exception.class)

@Transactional(rollbackFor = Exception.class)
@Override
public Boolean awardPrize(Integer appId, String uid, Integer drawId, String note) {
    // 修改抽奖记录为已领取
    UpdateWrapper<LotteryRecord> wrapper = new UpdateWrapper<>();
    wrapper.eq(LotteryRecord.ID, drawId)
            .eq(LotteryRecord.APP_ID, appId)
            .eq(LotteryRecord.UID, uid)
            .eq(LotteryRecord.STATUS, 1)
            .set(LotteryRecord.STATUS, 2)
            .set(StrUtil.isNotBlank(note), LotteryRecord.NOTE, note);
    return lotteryRecordService.update(wrapper);
}

请求的时候就会发现,都是在同个数据源寻找,就会发现修改奖品记录LotteryRecord没有找到这个表。

如果改成@Transactional(propagation = Propagation.REQUIRES_NEW)来修饰awardPrize就可以正常切换数据源了。

总结

本文主要是介绍事务的7种传播属性,并且着重讲解了两种常用的传播属性PROPAGATION_REQUIRED和PROPAGATION_REQUIRES_NEW,这一个知识点,在面试中,面试官也经常抓住不放,如果没有彻底弄懂,很容易把自己绕远,所以本文通过案例分析两种传播行为事务回滚的情况,以及在实际开发中如何保证整体事务一致性,来区分PROPAGATION_REQUIRED和PROPAGATION_REQUIRES_NEW。不过,还是要注意点,事务是基于动态代理,需要的是不同类,设置不同传播行为才会生效。