无涯

无所谓无 无所谓有

踩坑-spring事务失效

背景

有个多层业务,在进行新增操作的时候,需要操作多张表,然后是多段提交,分别执行了三条SQL。其中两条SQL是抽取的公共方法执行的。在线上环境发现,执行后两条SQL出错,而前一条SQL并没有触发回滚。

排查

我确认自己是开启了事务的,使用基于注解的声明式事务配置。

1
2
3
4
5
@Service
@Transactional(rollbackFor = Exception.class)
public class ContentProduceServiceImpl implements ContentProduceService {
// ...
}

注解@Transactional标注在类上面,类的public方法会默认继承这个注解,也就是会开启事务控制。同时我也指定了回滚的超类异常Exception,遇到异常会正常回滚才对。

于是我进行了单元测试,确认后两条SQL执行出现了异常,而且异常被spring包装成了RuntimeException,然而并没有触发回滚。

1
2
Caused by: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'name' at row 1
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:104)

思来想去,突然灵感一闪!那肯定是事务管理器没有捕获这个异常,被我写的异常以及日志AOP先拦截处理掉了。

Spring的声明式事务是基于切面编程实现的,在Spring里里面是使用ASPECT实现的AOP。

解决

方案一:去掉异常处理AOP

最简单粗暴的方法就是直接去掉异常处理的AOP

再去掉自定义的AOP后,顺利抛出异常,线程中止,事务成功回滚。缺点是没了自定义的异常处理。

方案二:遇到异常手动回滚

这种方案既能手动捕获异常进行异常处理,也能回滚事务。能解决方案一的痛点。

1
2
3
4
5
6
7
8
9
10
@Override
public Result<Boolean> createContent(ContentProduceShowDTO dto) {
try {
// do...
} catch (Exception exception) {
// 获取当前线程的事务,进行手动回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return Results.failSystemError(exception.getCause().getMessage());
}
}

获取当前线程的事务,进行手动回滚,需要注意的是,要保证当前方法开启了事务,不然会抛出NoTransactionException异常。

方案三:在异常AOP里面处理异常回滚

在异常AOP里面处理异常回滚,相比方案二,不用写重复的冗余代码。也更合理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected Object processService0(ProceedingJoinPoint joinPoint) {
printEntranceLog(joinPoint);

long startTime = System.currentTimeMillis();
Result<Object> result = null;
Throwable throwable = null;
// 只能处理这一类返回值类型的
try {
Object object = joinPoint.proceed();
if (object instanceof Result) {
result = (Result<Object>) object;
}
return object;
} catch (Throwable e) {
// do..
try {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
} catch (Exception exception) {
return result;
}
return result;
} finally {
// do...
}
}