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