前言
最近测试给我提了一个 bug,说我之前提供的一个批量复制商品的接口,产生了重复的商品数据。
追查原因之后发现,这个事情没想象中简单,可以说一波多折。
1. 需求
产品有个需求:用户选择一些品牌,点击确定按钮之后,系统需要基于一份默认品牌的商品数据,复制出一批新的商品。
拿到这个需求时觉得太简单了,三下五除二就搞定。
我提供了一个复制商品的基础接口,给商城系统调用。
当时的流程图如下:
如果每次复制的商品数量不多,使用同步接口调用的方案问题也不大。
2. 性能优化
但由于每次需要复制的商品数量比较多,可能有几千。如果每次都是用同步接口的方式复制商品,可能会有性能问题。因此,后来我把复制商品的逻辑改成使用 MQ 异步处理。
改造之后的流程图:
复制商品的结果还需要通知商城系统:
这个方案看起来,挺不错的。但后来出现问题了。
3. 出问题了
测试给我们提了一个 bug,说我之前提供的一个批量复制商品的接口,产生了重复的商品数据。
经过追查之后发现,商城系统为了性能考虑,也改成异步了。他们没有在接口中直接调用基础系统的复制商品接口,而是在 job 中调用的。
站在他们的视角流程图是这样的:
用户调用商城的接口,他们会往请求记录表中写入一条数据,然后在另外一个 job 中,异步调用基础系统的接口去复制商品。
但实际情况是这样的:商城系统内部出现了 bug,在请求记录表中,同一条请求产生了重复的数据。这样导致的结果是,在 job 中调用基础系统复制商品接口时,发送了重复的请求。
刚好,基础系统现在是使用 RocketMQ 异步处理的。由于商城的 job 一次会取一批数据(比如 20 条记录),在极短的时间内(其实就是在一个 for 循环中)多次调用接口,可能存在相同的请求参数连续调用复制商品接口情况。于是,出现了并发插入重复数据的问题。
为什么会出现这个问题呢?
4. 多线程消费
RocketMQ 的消费者,为了性能考虑,默认是用多线程并发消费的,最大支持 64 个线程。
例如:
@RocketMQMessageListener(topic = "${com.susan.topic:PRODUCT_TOPIC}",
consumerGroup = "${com.susan.group:PRODUCT_TOPIC_GROUP}")
@Service
public class MessageReceiver implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
String message = new String(message.getBody(), StandardCharsets.UTF_8);
doSamething(message);
}
}
也就是说,如果在极短的时间内,连续发送重复的消息,就会被不同的线程消费。
即使在代码中有这样的判断: