事务注解(@Transactional)引起的数据覆盖故障

最近组织团队内技术培训,刘聪为分享的一个跟事务和写数据库相关的case(bug)很有代表性。用事务,要小心!

一、故障现象

车辆交付履约流程上两个节点(工程项目)A和B, A修改一条数据记录item(工单),然后发消息给B,B也会对item进行修改。

故障现象,有时候(不是必现)感觉A没有成功修改item这条数据,而日志显示A修改成功了数据item!

看一下具体代码实现。下图是工程A代码,3个红框依次动作。

1、开启事务

2、修改工单记录item

3、向下游节点发送mq消息 

bb

下图是下游消费mq消息的节点B,红框表示采用JPA技术修改数据记录item

bb

二、原因分析

这个过程总共经历5个步骤,见下图 

bb

1、节点A开启一个事务,修改数据表中某条数据item

2、A向B发送mq消息,再做些其他事情,提交事务

3、节点B,消费mq消息

4、节点B读出数据item

5、节点B在内存中修改数据item某些字段,写回数据库

注意到第1、2步骤是在一个事务中。存在一种可能,B节点收到mq消息,执行第4步骤,读取item数据后,步骤1、2的事务才完成提交。由于数据库事务隔离级别,这种情况下,第4步骤读到的数据并不是A节点在第1步写的,已经读到脏数据了。当第5步写回数据的时候,就可能造成老数据覆盖A写的新数据

这里有两个细分场景

1、第1步、第5步修改同一个字段。这种情况,第4步骤读到脏数据 

bb

2、第1步、第5步修改不同字段。第4步读到col2字段的oldvalue,第5步目的是修改col3的值,但是采用jpa或者mybatis的一些默认写法,会把col2的oldvalue更新回数据库。

一般的ORMapping框架利用一个vo对象写数据库记录,没有修改的字段不会更新(代码里并没有改col2的值),但是第4步读取数据后,第1步对数据item进行了修改。这样默认的写库方法,会check记录的变化,然后把col2字段的值更新。这样就出现了旧值覆盖新值的问题。 

bb

三、解决办法           郑州不孕不育医院:http://byby.zztjyy.com/yiyuanzaixian/zztjyy//

1、考虑到实施成本,如果修改不同的字段,不存在竞争关系。只需要在第5步写库的环节指定更新字段就能快速解决这个问题。事实上,生产环境下也是选择的这个方案临时修复。

2、解决办法1显然不够优秀。更好的做法,把第2步发mq消息从事务中拆出来,等第1步操作commit后在发mq消息。这个办法涉及到一些逻辑的梳理(业务代码里会有不少的if……else),代码的改动。这样处理仍然不够完美,第1步执行完了,第2步失败了怎么办?在这里可能需要一些额外的代码工作保证第2步执行成功。

3、如果业务压力不大,也可以考虑从数据库的事务隔离级别方面入手来解决这个问题。

4、业务上,第1步到第5步如果需要强一致,了解一下分布式事务


猜你喜欢

转载自blog.51cto.com/14393794/2413889
今日推荐