一、背景
微服务项目中用了RocketMQ来做解耦,做完业务逻辑然后登记条消息到RocketMQ消息队列,此时才提交事务,在这种情况下,消费端因为要做幂等,所以会根据一个流水ID去查询数据库中有没有记录,若有记录才会开始消费,但是因为微服务那边还没有提交事务,所以消费端第一次去查询数据库可能会查询不到记录。
二、解决方案
1、将消息发送和事务分离
可以修改程序,在提交完事务后再登记消息,这样子消费程序将会马上查询到记录,不会有任何问题,但是登记消息之前也要插入一条做幂等的记录,这里可能会登记消息失败,这种情况我们可以再登记消息失败的时候插入一个消息补偿表,然后定时去重发即可,至于插入幂等记录和插入补偿表记录失败的可能性很小很小,如果遇到则走人工处理即可,毕竟没有程序是可以达到100%完美。100%的可用性的。
因为我们的微服务有别的逻辑在修改不能马上换版到生产,所以微服务改完后还不能上线,不能马上解决问题,所以采取了下面的方案。
2、让消息重发
我们如果根据ID查询不到记录,则返回false,然消息中间件重发,本来想到消息重发的级别是1,3,5,10….,应该最多一秒钟就可以等到了,可以接受,但是出现个奇怪的问题,发现每次重发都要等10秒,查看源码发现消费端返回的延迟级别是0,然后服务端重发会加上几个级别,这消费端又不能强行设置级别,也不能修改服务端的延迟级别,真尴尬,这个可能只有修改源码才能解决了,也不知怎么回事,为啥1,3不生效,希望知道的大神告知一下。
鉴于上面的会10秒后才会重新推送,严重影响用户体验,只能放弃这种方式。
3、消费端轮询获取记录
在第一种方式不能上线解决的情况下,我们采取了第三种方式,那就是如果消费端第一次根据ID取不到数据,那么休眠200毫秒再去取一下,连续10次,直到有记录就跳出循环,若10次都还没有记录则再返回失败。
虽然上面这种情况会导致一个线程被占用,影响消费端程序的效率,但是如果第一种解决方案上线后在第一次就会查询到记录了,这种轮询模式就不会生效了,所以可以作为一个临时的解决方案。
可惜想法是好的,因为自己对MyBatis的源码研究不够深入,太过理所当然,导致被MyBatis的一级缓存狠狠坑了一把,怪自己。
三、坑的晕头转向
当我按第三种解决方案处理后,突然发现,怎么问题不仅仅没有解决,而且还更加严重了,这他喵什么鬼,我去看消费端的日志发现连续两秒都没有读取到记录,难道微服务提交记录这么慢?然后我改为400毫秒发现更加慢了要等待14秒才能处理,这绝对有问题,微服务不可能这么慢!
我赶紧去微服务上看了下接口调用消耗时间:”26mm”!尼玛26mm就执行完了整个接口,26mm,消费程序4s都没有结果,这绝对有大问题!
然后就开始一直检查,先是认为微服务日志打错了,因为我们用的是log4j2,异步打印,最后这个可能性排除,然后猜测会不会数据库提交是异步的,后台慢慢提交,然后想想更加不可能,这样子搞的话就不能保证事务了,最后一只研究代码,到底这条sql语句有啥问题,想着除非第二次查询数据库没有去查?啥!灵机一动,尼玛应该就是这个原因,一级缓存,尼玛,MyBatis的一级缓存,我一直没怎么认真去研究过源码,一只以为查询结果为null就不会放入一级缓存中,尼玛没想到一级缓存跟返回值无关,只跟session、sql、参数等有关,尼玛!
四、解决一级缓存
打开MyBatis的xml配置在那条查询的sql上加上一个属性:
flushCache="true"
表明这条sql不开启一级缓存,完美解决!
喵了个咪!