@Transaction注解的失效场景

作者:京东物流 孔祥东

背景

事情是这样,最近在实现一个需求的时候,有一个定时异步任务会捞取主表的数据并置为处理中(为了防止任务执行时间过长,下次任务执行把本次数据重复捞取),然后根据主表关联明细表数据,然后将明细表数据进行组装,等待所有明细数据处理完成之后,将主表状态置为完成;大概当时的代码示例(只是截取部分)如下:

1 @Override 2 @Transactional 3 protected void executeTasks(List<AbnormalHotspot> list) { 4 CallerInfo infoJk = Profiler.registerInfo("com.jd.xxxxx.executeTasks", "qc-xxxxxx",false, true); 5 6 7 try{ 8 //更新主表的状态为中间态 9 hotSpotService.updateAbnormalHotspotStatus(list, HotSpotStatusEnum.EXECUTING.getCode()); 10 11 12 //处理明细表数据 13 for(AbnormalHotspot hotspot : list){ 14 15 16 //组装批次基本信息 17 AbnormalHotSpotSendToMcssMq spotSendToMcssMq = assemblyAbnormalHotSpotSendToMcssMqFromMain(hotspot); 18 19 //组装附件信息,此处存在抛出IOException 异常的可能 20 List<HotSpotAttachmentBo> attachmentBos = assemblyAttachment(hotspot.getBusinessCode()); 21 spotSendToMcssMq.setAttachmentAddr(JSON.toJSONString(attachmentBos)); 22 23 } 24 //更新主表的状态为终态 25 hotSpotService.updateAbnormalHotspotStatus(list, HotSpotStatusEnum.FINISHED.getCode()); 26 27 28 }finally { 29 Profiler.registerInfoEnd(infoJk); 30 } 31 32 33 34

然后执行测试的时候发现,代码抛出异常了,可主表数据的状态一直是处理中,并没有发生回滚,但是看代码也已经加上@Transaction 注解了,所以就怀疑是不是事务没有生效,带着这个问题就顺便重新复习了一下@Transaction 注解的使用以及事务相关的一些知识。

过程

首先带着刚刚的问题,来看看Spring 的源码。

1/** @Transaction 注解中的这个方法定义,可以指定回滚的异常类型, 2 可以指定0-多个exception 子类 3 * Defines zero (0) or more exception {@link Class classes}, which must be a 4 * subclass of {@link Throwable}, indicating which exception types must cause 5 * a transaction rollback. 6 * <p>This is the preferred way to construct a rollback rule, matching the 7 * exception class and subclasses. 8 * <p>Similar to {@link org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(Class clazz)} 9 */ 10 11 Class<? extends Throwable>[] rollbackFor() default {} 12

接着再看
org.springframework.transaction.interceptor.RollbackRuleAttribute类中有一个方法是在匹配查找异常。

1/** 2 * 递归查询匹配的异常类 3 * Return the depth of the superclass matching. 4 * <p>{@code 0} means {@code ex} matches exactly. Returns 5 * {@code -1} if there is no match. Otherwise, returns depth with the 6 * lowest depth winning. 7 */ 8 public int getDepth(Throwable ex) { 9 return getDepth(ex.getClass(), 0); 10 } 11 12 13 14 15 private int getDepth(Class<?> exceptionClass, int depth) { 16 if (exceptionClass.getName().contains(this.exceptionName)) { 17 // Found it! 18 return depth; 19 } 20 // If we've gone as far as we can go and haven't found it... 21 if (exceptionClass.equals(Throwable.class)) { 22 return -1; 23 } 24 return getDepth(exceptionClass.getSuperclass(), depth + 1); 25 } 26

这时候再看这个getDepth 方法的调用的地方是这个
org.springframework.transaction.interceptor.RuleBasedTransactionAttribute 类,这个类中就会出现一个rollbackOn 的方法,但是这个方法并不是它自身的,而且重写了它的父类org.springframework.transaction.interceptor.DefaultTransactionAttribute,所以我们需要看的是这个默认的实物属性类的描述。

1/** 默认的回滚行为 unchecked exception,并且ERROR 也会回滚 2 * The default behavior is as with EJB: rollback on unchecked exception. 3 * Additionally attempt to rollback on Error. 4 * <p>This is consistent with TransactionTemplate's default behavior. 5 */ 6 public boolean rollbackOn(Throwable ex) { 7 return (ex instanceof RuntimeException || ex instanceof Error); 8 } 9

到这里我们应该就可以知道上述问题的缘故了。

结论

@Transaction 如果不显示声明回滚的异常类型的话,默认只会回滚RuntimeException 异常(运行时异常)及其子类以及Error 及其子类,由此也可以得出,如果事务方法中的异常被catch 了,也会使事务失效。

扩展总结

到这里,你以为就完了吗!这就一点不符合我们的程序员的发型了!!!!

下面,我们就来看一下@Transaction 里面是什么东西

1@Target({ElementType.METHOD, ElementType.TYPE}) 2@Retention(RetentionPolicy.RUNTIME) 3@Inherited 4@Documented 5public @interface Transactional { 6 7 8 @AliasFor("transactionManager") 9 String value() default ""; 10 //事务管理器名称 11 @AliasFor("value") 12 String transactionManager() default ""; 13 //事务传播模式 14 Propagation propagation() default Propagation.REQUIRED; 15 //事务隔离级别 16 Isolation isolation() default Isolation.DEFAULT; 17 //超时时间 18 int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; 19 //是否是只读事务 20 boolean readOnly() default false; 21 //需要回滚的异常类 22 Class<? extends Throwable>[] rollbackFor() default {}; 23 //需要回滚的异常类名称 24 String[] rollbackForClassName() default {}; 25 //排除回滚的异常类 26 Class<? extends Throwable>[] noRollbackFor() default {}; 27 //排除回滚的异常类名称 28 String[] noRollbackForClassName() default {}; 29} 30

value,transactionManager 方法都是设置事务管理器的,不太需要关注

propagation 事务传播行为

为了解决业务层方法之间互相调用的事务问题。

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:

1public enum Propagation { 2 3 4 //默认值 5 //当前有事务,就加入这个事务,没有事务,就新建一个事务(也就是说如果A方法和B方法都添加了注解,默认传播模式下,A方法调用B方法,会将两个方法事务合并为一个) 6 REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED), 7 8 //当前有事务,就加入这个事务,没有事务,就以非事务的方式执行 9 SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS), 10 11 12 //当前有事务,就加入这个事务,没有事务,就抛出异常 13 MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY), 14 15 16 //新建一个事务执行,如果当前有事务,就把当前的事务挂起(如果A方法默认为Propagation.REQUIRED模式,B方法为Propagation.REQUIRES_NEW,在A方法中调用B方法,A方法抛出异常后,B方法不会回滚,因为Propagation.REQUIRES_NEW会暂停A方法的事务) 17 REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW), 18 19 20 //在无事务状态下执行,如果当前有事务,就把当前的事务挂起 21 NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED), 22 23 24 //在无事务状态下执行,如果当前有事务,会抛出异常 25 NEVER(TransactionDefinition.PROPAGATION_NEVER), 26 27 28 //当前有事务,就新建一个事务,嵌套执行,当前无事务,就新建一个事务执行(Spring 特有的) 29 NESTED(TransactionDefinition.PROPAGATION_NESTE 30

看到这里就会发现,如果事务传播行为设置不当的话,也会使事务失效。

从上述来看,配置错误这三种
TransactionDefinition.PROPAGATION_SUPPORTS,TransactionDefinition.PROPAGATION_NOT_SUPPORTED,TransactionDefinition.PROPAGATION_NEVER都有可能会出现失效。

isolation 方法

定义了一个事务可能受其他并发事务影响的程度,带来的是脏读,丢失修改,不可重复读,幻读等问题,所以不会是事务失效,这部分内容还可以进一步研究。

timeout

定义的是事务的最长执行时间,如果超过该时间限制但事务还没有完成,则自动回滚事务,也不会使事务失效。

readOnly:

事务的只读属性是指,对事务性资源进行只读操作或者是读写操作.

rollbackfor,rollbackforClassName,norollbackfor,rollbackforClassName

都是显示声明哪些异常类需要回滚或者不需要回滚,这个在上述已经回答过了。

看到这里,是不是以为失效的场景就这些了呢,No

再进一步想想,Spring 事务是基于什么实现的?是不是每个程序员在学习AOP 的时候都会听到AOP 的应用场景,如日志,事务,权限等等。

所以想想,既然Spring 事务是基于AOP 实现的,那可以想想如果事务方法要是没有被Spring 代理对象来调用的话,是不是就加不上事务了,打个比方,如下代码:

1class TransactionTest{ 2 public void A() throws Exception { 3 this.B(); 4 ... ... 5 } 6 7 8 @Transactional() 9 public void B() throws Exception { 10 //数据源操作 11 } 12} 13

方法B 的事务会生效吗?答案是不会,因为this 是指当前实例,并不是Spring 代理的,所以B 方法的事务肯定是加不上的,由此可以得出,在同一个类中方法调用也会使事务失效。

其实上述提到的事务时效只是基于自己的遇到的问题来分析,对于Spring 事务时效的场景应该来说还有很多很多,下面大概整理一下常见的吧。

失效场景备注
未加入Spring容器管理类未标注@Service、@Component等注解,或者Spring扫描路径不对
表不支持mysql5 之前数据库引擎默认是myisam ,它是不支持事务的
catch 异常将增删改方法catch了
自定义回滚异常默认只能回滚RuntimeException 异常,如果自定义了一个回滚异常,但是实际抛出的异常又不是声明的自定义的,就会时效
抛出了自定义异常默认只能回滚RuntimeException 异常,如果自定义了一个抛出异常,又没有在注解中显示声明对应回滚异常
错误的传播特性上述已讲解
方法内部调用一个类里面的方法,相互调用
方法使用final 修改方法使用final 修饰
方法访问权限不对方法声明成private
多线程调用方法在不同线程中,数据库链接有可能不是一个,从而是两个不同事务
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

VBox 启动虚拟机失败

在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern

FLV文件格式

1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  

mysql设置时区

mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0