很多知识细节都来自生产事故,只有经历过,才能记得住。今天的故事,也源于一次线上事故。
01
某天,做完产品的业务升级后,还是比较放松的。刚想搞点别的事,用户群就有用户反馈有问题。真的是不让人省心,先看问题吧。如下图,用户在添加卡片时,提示错误,无法新增,但是列表里又多出了一些数据。点击查看详情时,又提示空白。
这个问题还是比较严重的,优先解决报错的问题,回想了升级的内容后,很容易就定位到问题了,顺利解决(你以为我要说报错的问题吗?这是另一个更长的故事)。
解决完问题后,我关注的是另一个问题:按理说,如果新增数据报错了,列表数据应该也是没有的,这里第2步中会多出这么几条数据?
02
根据对系统的了解,我判断出列表数据读取的是ES上的数据,而详情页(第3步)中的数据来源于数据库,是因为两边的数据不一致吗?查了下数据库,确实没有那几条重复的数据。那问题就比较明显了,应该是新建卡片时,同进写ES和数据库,出了问题,导致两边数据不一致。拉代码看看呗。
如上图,在新增卡片时,先做了数据库的插入,然后做ES的插入,最后做事件的通知及其他操作。看着好像也没什么问题。
等等,不对,为什么这里没做事务管理?如果有事务,失败了不就会回滚么?
03
研发应该不会犯这么低级的错误,再看看代码。想到了Spring中有统一的事务管理注解,应该会使用到的,为什么会没生效呢?找了下,还真是有用了Transactional,那应该不会有问题的呀。
正常情况下,只要在类上添加@Transactional注解就完事了,那是什么原因导致注解失效了呢?问问ChatGPT吧,回复如下:
看着也没什么问题,这些情况都不符合我的场景。那问题出在哪里呢?在其官网上也没找到相关的信息,看看其他大神的文章吧。在Bing上搜索了一阵子,发现在别人的文章中有提到@Transactional失效中的原因有一条:如果数据库引擎不支持事务,那么就无法回滚对应的数据。
隐约记得ES是不支持事务的,会是这个问题么?
04
继续问问ChatGPT吧,看看它能给我什么思路。
看来问题的根源找到了:
用户在操作新增卡片时,先往数据库插了条数据,然后ES上也增加了对应的数据,但是在做事件更新时,出了问题(第1步的报错信息来源于此),触发了@Transactional事务回滚的机制,所以数据库里的数据被回滚了。但是由于ES不支持事务,所以@Transactional也没办法回滚,所以列表中的数据还是能被查看到(第2步,ES中的数据还是在的),但是点击详情(第3步)时,因为在数据库中找不到对应的数据,所以页面无法显示。
05
针对这个问题,团队讨论了下,解决方案有三种:
1. 因为双写(同时写数据库和ES),才导致了这个问题,那就去掉双写,只写数据库,然后通过异步或者MQ的方式,再去写ES,这样能解决一致性的问题,但是时效性会差点。
2. 在异常类中统一处理,如果发现这个方法有异常抛出,就记录数据信息,去ES中做对应的回退操作(分类处理,例如数据库是insert操作,就调用ES的delete操作数据删除),人为实现ES的回滚;
3. 解偶,在方法中只处理双写操作,其他的业务逻辑做异步处理(例如这个场景中,事件更新可以异步处理,并做对应的补偿机制),这样就不会影响主数据的一致性。
最终我们选择了代价最小的第3种方案,如果你有更好的方案,可以联系我哟,感谢。
06
解决完问题,回想下如何避免此类事件的发生(应该是个小概率事件)。对于事务的一致性测试,在平时很容易被忽略,大家都还是相信开发会使用事务的。但是对于事务管理是否会失效,没有引起足够的重视。
对于测试人员而言,常见的事务一致性测试场景有哪些呢?
a. 双写或者多写的情况:随着现在中间件使用得越来越多,双写或者多写的情况也会增加,当数据记录在多个地方时,需要关注一致性问题
b. 异步处理,常见的是MQ,如果消费失败,是否有对应的补偿机制来保障一致性
c. 跨系统的数据存储,有些业务数据存在关联性,又分布在不同的系统中,如何保障一致性,也是测试人员需要关注的。