最近组织团队内技术培训,刘聪为分享的一个跟事务和写数据库相关的case(bug)很有代表性。用事务,要小心!
从策划到设计制作,每一步都追求做到细腻,制作可持续发展的企业网站。为客户提供成都网站设计、成都网站制作、网站策划、网页设计、域名注册、雅安服务器托管、网络营销、VI设计、 网站改版、漏洞修补等服务。为客户提供更好的一站式互联网解决方案,以客户的口碑塑造优易品牌,携手广大客户,共同发展进步。一、故障现象
车辆交付履约流程上两个节点(工程项目)A和B, A修改一条数据记录item(工单),然后发消息给B,B也会对item进行修改。
故障现象,有时候(不是必现)感觉A没有成功修改item这条数据,而日志显示A修改成功了数据item!
看一下具体代码实现。下图是工程A代码,3个红框依次动作。
1、开启事务
2、修改工单记录item
3、向下游节点发送mq消息
下图是下游消费mq消息的节点B,红框表示采用JPA技术修改数据记录item
二、原因分析
这个过程总共经历5个步骤,见下图
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步骤读到脏数据
2、第1步、第5步修改不同字段。第4步读到col2字段的oldvalue,第5步目的是修改col3的值,但是采用jpa或者mybatis的一些默认写法,会把col2的oldvalue更新回数据库。
一般的ORMapping框架利用一个vo对象写数据库记录,没有修改的字段不会更新(代码里并没有改col2的值),但是第4步读取数据后,第1步对数据item进行了修改。这样默认的写库方法,会check记录的变化,然后把col2字段的值更新。这样就出现了旧值覆盖新值的问题。
三、解决办法
1、考虑到实施成本,如果修改不同的字段,不存在竞争关系。只需要在第5步写库的环节指定更新字段就能快速解决这个问题。事实上,生产环境下也是选择的这个方案临时修复。
2、解决办法1显然不够优秀。更好的做法,把第2步发mq消息从事务中拆出来,等第1步操作commit后在发mq消息。这个办法涉及到一些逻辑的梳理(业务代码里会有不少的if……else),代码的改动。这样处理仍然不够完美,第1步执行完了,第2步失败了怎么办?在这里可能需要一些额外的代码工作保证第2步执行成功。
3、如果业务压力不大,也可以考虑从数据库的事务隔离级别方面入手来解决这个问题。
4、业务上,第1步到第5步如果需要强一致,了解一下分布式事务
https://www.jianshu.com/p/16b1baf015e8