前言
在日常开发中,我们经常会使用Spring框架来管理事务。只需简单地在方法上添加@Transactional注解,即可开启事务管理。然而,除了基本的开启和关闭事务,Spring还提供了丰富的事务传播行为配置,尤其是propagation参数,它决定了在已有事务的情况下如何处理嵌套事务。本文将详细探讨Spring中的事务传播行为,包括其概念、作用及七种具体传播行为,并通过案例分析,重点介绍PROPAGATION_REQUIRED和PROPAGATION_REQUIRES_NEW两种常用传播行为的差异和应用场景。
七种事务传播行为
首先先介绍一下事务的传播行为,Spring 事务传播行为是指在一个事务已经存在的情况下,如何处理嵌套事务。Spring 支持 7 种事务传播行为,分别是:
PROPAGATION_REQUIRED(默认):如果当前没有事务,就创建一个新的事务。如果已经存在一个事务,就加入到这个事务中。这是最常见的选择,适用于大多数情况。PROPAGATION_SUPPORTS:如果当前没有事务,就以非事务方式执行。如果已经存在一个事务,就加入到这个事务中。适用于支持事务的操作,但不需要事务管理。PROPAGATION_MANDATORY:如果当前没有事务,就抛出异常。如果已经存在一个事务,就加入到这个事务中。适用于必须在事务中执行的操作。PROPAGATION_REQUIRES_NEW:始终创建一个新的事务。如果当前存在事务,就将当前事务挂起,然后创建一个新的事务。适用于需要独立于其他事务执行的操作。PROPAGATION_NOT_SUPPORTED:以非事务方式执行。如果当前存在事务,就将当前事务挂起。适用于不支持事务的操作。PROPAGATION_NEVER:如果当前存在事务,就抛出异常。以非事务方式执行。适用于禁止事务的操作。PROPAGATION_NESTED:如果当前没有事务,就创建一个新的事务。如果已经存在一个事务,就创建一个嵌套事务。嵌套事务可以独立于外部事务提交或回滚。适用于需要独立于外部事务执行,但又需要保持与外部事务的关联的操作。
在选择事务传播行为时,需要根据具体的业务场景和需求来决定。通常情况下,使用默认的 PROPAGATION_REQUIRED 就足够了。在需要更细粒度的控制事务传播时,可以考虑使用其他的传播行为。
REQUIRED和REQUIRES_NEW案例演示
在实际开发中,PROPAGATION_REQUIRED和PROPAGATION_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。不过,还是要注意点,事务是基于动态代理,需要的是不同类,设置不同传播行为才会生效。