面试专题:深入事务的传播行为,绕晕面试官

2023-12-21 16:12:23 浏览数 (1)

前言

关于事务,大家都知道怎么用吧,当我们需要在使用事务的方面加注解@Transation即可,但是,其实关于事务还有很多属性可以配置,比如事务传播信息,配置参数:propagation,可以指定事务的传播行为。所以本文将主要介绍了Spring中事务传播行为的概念、作用以及Spring支持的7种事务传播行为。通过了解这些事务传播行为,开发者可以更好地掌握Spring事务管理的核心原理,并在实际开发中合理地使用事务传播行为来保证事务的正确性和一致性。并且将详细介绍两种常用传播行为REQUIRED和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案例演示

虽然事务有七种事务传播行为,但是在开发中比较常用的主要是REQUIRED和REQUIRES_NEW这两种,接下来就开始讲解这两种不同传播行为在实际开发中的应用场景。

允许不同事务单独提交

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

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

这道题其实有两种解法:

第一种:同个事务代理类

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

代码语言:java复制
@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;

代码语言:java复制
@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异常回滚的时候在分析一下

代码语言:java复制
  @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) {
            // 处理异常
        }
    }

多数据源事务传播处理

多数据源事务处理,这个在实际开发中就很常见的,主要是因为@Transactional默认是必须保证在通过数据源才能回滚的,这也是单机事务的缺点。比如有一种场景:抽奖活动中的奖品领取分成两个数据源处理,奖品发货一个数据源,奖品记录修改另一个数据源,我们肯定是要保证整个奖品领取的事务一致性的。所以就要加@Transactional注解。但是会发现加了@Transactional,所有处理都会走同个数据源

代码语言:java复制
@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)

代码语言:java复制
@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就可以正常切换数据源了。

REQUIRED和REQUIRES_NEW异常回滚

不同事务想生效,必须在不同类,因为事务是通过代理对象创建,同个类不同方法设置不同的事务传播或者其他属性,是不生效的。接下来,通过案例验证REQUIRED和REQUIRES_NEW异常,事务是否回滚。

A有异常 B有异常

A调用B,B出现异常,A也出现异常

代码语言:java复制
@Service
public class TestService {
    
    @Autowired
    private TestService2 testService2;
    @Autowired
    private SignLogService signLogService;

    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void insertA() {
            SignLog signLog = new SignLog();
            signLog.setUid("1233444");
            signLogService.save(signLog);
            testService2.insertB();
            int a= 1/0;
    }
}

B事务

代码语言:java复制
@Service
public class TestService2 {
    @Autowired
    private LotteryService lotteryService;

    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    public void insertB() {

        Lottery lottery = new Lottery();
        lottery.setTopic("SDEF111");
        lottery.setStartTime(new Date());
        int a= 1/0;
        lotteryService.save(lottery);
    }
}

结果,数据库中没有 A 数据,也没有B 数据

A 有异常 B 没有异常

保留A中的异常,去掉B的异常

代码语言:java复制
@Service
public class TestService {

    @Autowired
    private TestService2 testService2;
    @Autowired
    private SignLogService signLogService;

    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void insertA() {
            SignLog signLog = new SignLog();
            signLog.setUid("1233444");
            signLogService.save(signLog);
            testService2.insertB();
            int i = 1 /0;
    }
}

B没有异常

代码语言:java复制
@Service
public class TestService2 {
    @Autowired
    private LotteryService lotteryService;

    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    public void insertB() {

        Lottery lottery = new Lottery();
        lottery.setTopic("SDEF111");
        lottery.setStartTime(new Date());
        lotteryService.save(lottery);
    }
}

结果,数据库中没有 A 数据,有B 数据

这情况其实要注意,在平时开发,调用第三方服务或者另一个数据源的时候,要注意整体事务是否一致。如果没有条件能处理分布式事务的话,就要在代码上顺序要求一下,A必须处理完业务之后才调用B,保证A全部成功去执行B。所以要求事务一致,可以将A改成:

代码语言:java复制
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void insertA() {
            SignLog signLog = new SignLog();
            signLog.setUid("1233444");
            signLogService.save(signLog);
            int i = 1 /0;
            testService2.insertB();

    }

A没有异常 B有异常

去掉A中的异常,保留B的异常

代码语言:java复制
@Service
public class TestService {

    @Autowired
    private TestService2 testService2;
    @Autowired
    private SignLogService signLogService;

    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void insertA() {
            SignLog signLog = new SignLog();
            signLog.setUid("1233444");
            signLogService.save(signLog);
            testService2.insertB();

    }
}
代码语言:java复制
@Service
public class TestService2 {
    @Autowired
    private LotteryService lotteryService;

    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    public void insertB() {
        Lottery lottery = new Lottery();
        lottery.setTopic("SDEF111");
        lottery.setStartTime(new Date());
        lotteryService.save(lottery);
        int i = 1 /0;
    }
}

结果数据库中没有 A 数据,也没有 B数据

结论

如果不是同个事务(A是REQUIRED,B是REQUIRES_NEW),内层方法B有可能影响到外层方法A,但是外层方法A是不会影响到内层方法B的(不同车)

当然,如果B出现异常,直接把异常吃了,获取A捕获B,但是不处理异常,上面结论可能就不生效。

可以看一下,如果REQUIRED和REQUIRES_NEW都在一个类上配置,来看一下A 有异常 B 没有异常,这种情况是不是也是出现:数据库中没有 A 数据,有B 数据

代码语言:java复制
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void insertA() {
            SignLog signLog = new SignLog();
            signLog.setUid("1233444");
            signLogService.save(signLog);
            insertB();
            int a= 1/0;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    public void insertB() {
        Lottery lottery = new Lottery();
        lottery.setTopic("SDEF111");
        lottery.setStartTime(new Date());
        lotteryService.save(lottery);
    }

最终结果是A和B都是没有数据,很显然B也被回滚了,这说明外层A出现异常影响到内层B,说明这两个应该是同个事务,也就说明Propagation.REQUIRES_NEW没生效。

总结

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

我正在参与2023腾讯技术创作特训营第四期有奖征文,快来和我瓜分大奖!

0 人点赞