背景 有个多层业务,在进行新增操作的时候,需要操作多张表,然后是多段提交,分别执行了三条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 { } 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) { try { TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } catch (Exception exception) { return result; } return result; } finally { } }