Spring事务失效事件(非常规原因)

        近期遇到一个spring注解事务失效的问题,先上代码(以下代码经过简化,只保留关键点)。

        没耐心的朋友可以直接看最下面结论。

第一部分:具体问题

接下来上一下代码,大致介绍一下流程。

Controller

Controller里面使用@Autowired注入了一个serviceImpl,然后调用这个serviceImpl的业务方法。  

ServiceImpl的属性

自动装配了另一个service和一个dao

重点看serviceImpl的方法(经过简化,只保留了关键点)

红框是两个insert数据库操作,需要事务管理

最下面的抛出异常是为了测试事务。

其中红框里面的super.addNew方法上有@Transactional(rollbackFor = Exception.class)注解。所以看上去并没有什么问题,但是每次抛出异常,数据库的操作都没有回滚。

问题大致就是这样,本来以为事情会很容易被解决,因为在网上看过很多事务失效的例子,各种原因被各种大神列举了很多次了。

第二部分:常规问题解决


普通事务失效原因:

1.如使用mysql且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB

2. 如果使用了spring+mvc,则context:component-scan重复扫描问题可能会引起事务失败。 (即父子容器)

3. @Transactional 注解开启配置,必须放到listener里加载,如果放到DispatcherServlet的配置里,事务也是不起作用的。 

4. @Transactional 注解只能应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错,事务也会失效。 

5. Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承 的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。

以上五点摘抄自:Spring事务失效的原因


看到常规的原因,首先的解决思路如下

1.首先,数据库使用的是oracle,所以排除1

2.项目确实使用的是spring+mvc不过已经分开扫描了,mvc只扫描Controller和RestController,spring扫描排除这两个注解这样可以排除2,

3.事务管理器配置确实是放在spring容器里,用Listener加载的

4.方法是public

5.是在类的方法上使用的,非接口上

所以,常规的问题被我巧妙地全避开了。

第三部分:解决思路

        这个事务失效问题是第一次出现,并没有广泛存在我们的系统中。于是和其他的事务方法对比,发现其他的事务方法都是通过Service.getInstance()方法获取到的实例,跟进去,发现getInstance方法是通过我们项目定义的工具类从xml里面getBean获取到的。看上去貌似没什么问题,getBean和使用@Autowired不都一样嘛,都能获取到bean。

        打开日志debug级别,看能不能查到点什么细节。并且给方法入口和spring的事务管理器入口org.springframework.transaction.interceptor.TransactionInterceptor#invoke打上断点,然后运行到该事务方法。

        首先在方法入口的断点处用evaluate expression执行AopUtils.isAopProxy这个方法来看一下注入的serviceImpl是否被代理了,结果是true。

        继续执行,进入到spring事务管理器里面,然后一步步跟代码,获取TransactionInfo等,并没有发现什么可疑点,因为都已经进入到了spring事务管理器了,而spring几乎不会出bug,进入到方法第一行后,查看了下debug日志,发现有Creating new transaction with name [XXX]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; '',-java.lang.Exception,继续执行,执行到super.addNew方法时,debug日志又出现了一行 Creating new transaction with name [XXX]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; '',-java.lang.Exception,而且此XXX非刚才的XXX,意思是在第一个事务里面,又起了一个事务,但是我事务没有显示配置PROPAGATION,默认的是REQUIRED,为什么会又重新起一个事务呢,这里让我百思不得其解。于是我回过头又检查了一遍父子容器问题,是不是还是因为包扫描重复导致的呢,虽然如果是子容器(即MVC)的话,肯定不会进入到事务管理器,但我还是抱着侥幸心理检查了一遍,没有任何问题。

        没找到问题所在,继续执行下去,发现跑出的异常被事务管理局接管了,debug日志也显示了transaction rollback。这就更奇怪了,既然事务都回滚了,数据是怎么插到数据库里的呢。我继续在插数据的那一行sql打上断点,重复执行,发现执行完这条sql的时候,数据就已经在数据库了,但是事务还没提交。。这又是怎么回事。这里我想到了会不会是数据库连接设置的autoCommit,于是重复执行,在spring的transaction源码里面打断点,发现事务管理器会自动把autoCommit关掉。

        感觉这条追查的路已经堵死了。。反而产生了两个其他的问题,1.既然是PROPAGATION_REQUIRED,为什么会在事务里又起一个事务,2.既然事务都还没提交,你是怎么插入到数据库里的。。

        想到最一开始观察到的,和其他事务起作用的方法差别在于,他们的成员变量是getInstance获得的,而这个有问题的是@Autowired注入的,那么是不是这里有问题呢,于是我把Autowired的全换成了getInstance获取在xml里面注册的bean,果然,事务起作用了。可是,这tm到底咋回事。。。Autowired和getBean不是一样么,为什么会出现两种截然不同的结果。刚好最近刚看完SpringIoc的源码,就决定追查到底。

第四部分:真相大白

        理理思路,父容器先加载,子容器后加载,子容器在加载过程中会通过获取到父容器。

        请求到达应用后,Controller会先被初始化,然后他的私有属性serviceImpl会被 inject,实质上是会被父容器inject,然后循环注入serviceImpl私有属性的私有属性。。一直循环直到所有的bean都被实例化。事务管理器也在父容器中定义,貌似没有问题,事实也证明了,事务管理器确实代理了目标方法。

        然后深入看了下getInstance方法,这个方法是通过项目的一个工具类的getBean方法工作的,工具类的getBean方法实际上是new了一个ClassPathXmlApplicationContext,然后调用他的getBean方法,那么看到这里好像一切都明朗了。。

        @Autowired自动装配的是web.xml里面定义的Listener加载的父容器里的bean,getInstance是自己定义的第二个容器获取到的bean,那么事务管理器也是一样,实际上有两个事物管理器,第一个是webxml加载父容器的,第二个是工具类加载的容器的。所以才会出现在一个事务中又起一个事务,因为容器2不知道方法已经在容器1起了一个事务,我所看到的事务没提交也是看到的容器1起的事务没提交。

        如果我再仔细缕一遍全流程就会发现,dao里面还封装了一层mybatis的sqlsession,这里使用的是工具类的getBean方法。

        如果再仔细看一下debug日志的话,就会发现启动应用的时候,每个xml都被加载了两次,一次是webxml的父容器,一次是工具类new加载的。

终章:结论

遇到事务失效的时候,先看一下是否被事务管理器接管了,如果有,那么多半是容器的问题,这里并不一定是父子容器,也有可能是父父容器。。

借用《黑客与画家》的一句话:如果自己就是潮水的一部分,又怎么能看见潮流的方向呢。

当遇到问题时,不要迷在某个局部,要跳出来,先把整个流程理清。

最后再说一句,开启了上帝视角之后,感觉这问题实在是很简单,不过解决过程中花费了我本人非常多时间(不少于十小时)。

希望这篇博客可以给有问题的朋友带来一点思路。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348