记一次线上并发导致数据重复创建的问题
问题还原:
有一个功能是自动生成某种单据,每次拉单都可能会创建一条消息去走创建单据的代码,代码流程经测试没有业务逻辑上的问题。
简化来说这个业务流程的代码就是:拉取订单->判断没有生成过该单据->创建生成单据的消息->消息消费->生成单据。
问题发现:
消息那边优化了判断,其实并不会每次生成消息,只有符合条件的消息会生成,所以消息的数量还算可控,并发量进一步减小。发布生产后,线上报了一个mybatis的异常如下:
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 2
根据异常栈信息找个对应的代码行,是调用的一个get方法,这里的get逻辑并没有问题,因为查询条件已经限定,这个get只会返回null或一个对象,就好比通过主键id查询,结果返回了两个结果。
如果控制台有打印sql语句,可以直接通过sql定位到问题数据,我这里是直接通过数据库group by语句进行count,确实存在count=2的数据,看这两个数据的创建时间,创建时间差了1秒,因为当前数据库的创建时间并没有精确到毫秒,所以可以猜测这两条数据是几乎同时创建,所以问题99%可以确定是发生了并发,还有1%到代码中找线索。
回顾代码,在创建单据前有个判断单据是否已经被创建的判断,简化如下:
单据 = get()//1
if (单据 != null)//2
return success//3
create(新单据)//4
这时候问题算是定位差不多了,在客户拉单重复的时候,生成了2+个生成单据的消息,消息分配到不同的机器执行,在执行到第2行代码时,两个机器判断单据!=null返回的都是false,进而走到第4行创建单据的代码。
问题解决:
定位到问题之后就是怎么解决问题,最直观的思路,有并发就加锁啊,因为是多台机器,所以要加分布式锁,分布式锁如redis加锁方案,balabala......
如果加分布式锁,十分考虑锁的健壮性,这又会说到分布式锁存在的一些问题,后面有机会再总结。
因为现在是使用的消息(activeMQ)去处理单据,那从消息入手考虑,activeMQ有个消息分组的特性,公司大佬提示的,我去看了一下官方文档,大概了解了。
所谓消息分组,就是将生成的消息加一个group标识,相同的group标识的消息只会交给同一台机器去处理,当然如果是第一次,那就选择任一一台机器进行消息。
这里简单描述下如何使用,在消息生产者设置string参数,如下:
Mesasge message = session.createTextMessage("<foo>hey</foo>");
message.setStringProperty("JMSXGroupID", "IBM\_NASDAQ\_20/4/05");
...
producer.send(message);
然后就大功告成,分发消息时会判断该JMSXGroupID的消息是否已经有机器处理,如果已经有机器在消费,那么把这条JMSXGroupID的再次交给这台机器处理,从源头上解决了并发,因为这样等于一台机器在串行消费。
为了考虑性能,JMSXGroupID也可以设置为业务单号或其他动态字符,保证相同单号的单据都交给同一台机器进行处理,这样即能避免并发,也能保证性能。