RabbitMQ踩坑 生产者事务提交在消费者之后

本人在使用RabbitMQ的过程中遇到了一些问题,主要是由于生产者事务还没完全提交,消费者代码已经开始运行引起的,下面把一步一步的解决过程分享给大家。有更好解决方式的同学欢迎评论指导一下。

问题

有个业务,用户每天获取饲料在30g时,和130g时,上报数据到合作方,同时会存一条上报记录。
某天合作方的人反馈,他的饲料获取到了30g,没有收到上报数据,于是赶紧查找问题。
查了一下发现丢失的数据占正常数据的1%。

代码解释:我们是先保存用户的饲料记录,然后队列解耦,消费者查询饲料记录,查出用户当天累计获取的饲料数,判断饲料数是否到达30和130。

一、超出TPS限制导致的通道关闭

2023-06-15 15:16:01.887 ERROR 70300 --- [.55.10.135:5672] o.s.a.r.c.CachingConnectionFactory       : Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=530, reply-text=denied for too many requests, ErrorHelp[dce6ef75-0993-4361-8ec8-1ff1c951a887], class-id=60, method-id=40)
2023-06-15 15:16:01.893 ERROR 70300 --- [.55.10.135:5672] o.s.a.r.c.CachingConnectionFactory       : Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=530, reply-text=denied for too many requests, ErrorHelp[ecf0f9e7-243e-49e0-b2db-5638cc8b2758], class-id=60, method-id=40)
2023-06-15 15:16:01.893 ERROR 70300 --- [.55.10.135:5672] o.s.a.r.c.CachingConnectionFactory       : Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=530, reply-text=denied for too many requests, ErrorHelp[148ccb82-8d48-4833-bc6a-0fb534b42dc7], class-id=60, method-id=40)

之前有过一个秒杀场景,经常有这个报错,reply-text=denied for too many requests 直译过来就是“拒绝过多的请求”,我们买的是阿里云的RabbitMQ产品,当时询问阿里云的技术人员,回复是请求超过了产品的TPS限制,需要花钱提升TPS。

上面报错并非本次场景里找到的报错,只是发生过类似的事,于是怀疑到,可能是偶尔有TPS超出的情况,导致通道关闭,然后消息丢失。然后决定使用生产者confirm的方式来确保数据不会丢失。
参考这个,写得比较简单:link

rabbitmq 整个消息投递的路径为:producer—>exchange—>queue—>consumer
producer到exchange失败会有一个confirmCallback,exchange到queue失败会有一个returnCallback,此处我们使用confirmCallback就足够了。

定义confirmCallback

@Slf4j
@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {

    @Autowired
    IAlipayGameCenterService alipayGameCenterService;

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            //接收成功,不做处理
        } else {
            //接收失败
            if (correlationData != null){
                ReturnedMessage returnedMessage = correlationData.getReturned();
                if (returnedMessage != null){
                    String key = returnedMessage.getRoutingKey();
                    Message message = returnedMessage.getMessage();
                    if (message != null){
                        String body = new String(message.getBody(), StandardCharsets.UTF_8);
                        if (QueueName.GAMECENTER.equals(key)){
                            //执行消费者代码,此处省略部分业务代码
                            JSON.parseObject(body, ?.class);
                        }
                    }
                }
            }
        }
    }
}

在发送队列时使用confirmCallback

    @Autowired
    private ConfirmCallbackService confirmCallbackService;
    
    public void sendMessageConfirm(String key, Object object) {
        //设置回调
        masterRabbitTemplate.setConfirmCallback(confirmCallbackService);
        String uuid = IdUtil.randomUUID();
        CorrelationData correlationData = new CorrelationData(uuid);
        MessageProperties properties = new MessageProperties();
        properties.setMessageId(uuid);//消息唯一ID,用力防止幂等性
        Message message = new Message(JSONObject.toJSONString(object).getBytes(StandardCharsets.UTF_8), properties);
        ReturnedMessage returnedMessage = new ReturnedMessage(message, 0, null, null, key);
        correlationData.setReturned(returnedMessage);
        masterRabbitTemplate.convertAndSend(key, message, correlationData);
    }

在使用上写得比较复杂,因为在确认回调里发现CorrelationData里面消息体是null,于是在生产者里手动new了ReturnedMessage,放入CorrelationData。那些博客里不需要手动new ReturnedMessage就能自动得到消息体的,不知道是怎么做到的。

写好测完后部署上线,运行一段时间看看效果。

二、在生产者事务提交之前就已经开始消费

跑了一段时间,发现线上消息并没有消息进入confirmCallback,但依然有用户获取30g饲料没有上报数据的。重新分享了一遍代码,没看出问题所在。没办法,加了大量日志,看看哪个节点出的问题。
吐槽:我们是分布式多台服务器,但没有集中的日志服务,翻日志得一台一台找。不是迫不得已我也不想加日志

生产者
生产者
消费者
在这里插入图片描述
加了日志后部署上线,运行一段时间后再查数据。
然后发现生产者保存饲料记录的时间是12:00:04,消费者查出用户饲料数的时间是12:00:03.568。生产者数据库insert肯定是在放入队列之前执行的,但实际数据库的时间比消费者查出饲料数的时间还晚。
很有可能是insert执行完之后,事务还没提交,等到队列发送执行之后,整个方法结束了,再进行事务提交,数据实际落库。而消费者代码在队列发送之后就立马执行了,此时生产者事务还没提交。
在这里插入图片描述
在这里插入图片描述

三、事务提交后再执行队列

既然推论到是事务过慢的原因,那控制事务落地在发送队列前执完毕就好了。有一种手动提交事务的方式,还有一个在事务提交后再执行后续操作的方式。参考
不过里面的TransactionSynchronizationAdapter已经过时了。

我们改造生产者那里的代码
在这里插入图片描述
如果想复制代码,请进入参考里进行复制。

一般情况下,问题应该就此解决了。可实际测起来,问题很多。因为我们是多数据源,这里使用了一个@DSTransactional,由MybatisPLUS提供的多数据源事务注解,此事务脱离了spring提供的原生事务管理,一系列spring的事务功能在此注解下无法使用。

四、在消费者里判断生成者的数据是否插入

既然无法从事务角度解决,只能思考别的方式。我们想了两种:

  1. 使用延迟队列,让消费者延迟,这样执行消费代码的时候,生产者的数据已经存入mysql。
  2. 在消费者里判断生产者的数据是否安全落库。

因为数据丢失的情况只占1%,所以我们选择了第二种。
生产者保存饲料记录时,可以得到记录的主键id,把主键id通过队列传入消费者,消费者查询用户饲料数的时候,根据最新的饲料记录id和队列传入的记录id,可以判断查到的是否是最新数据,如果没查到最新数据,等待1秒再查一次。

思路确定了,然后再次更改代码。
生产者
在这里插入图片描述
消费者查询用户饲料数
在这里插入图片描述
测完后部署上线,运行一段时间后查询数据,确认问题成功解决。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值