【分布式websocket】RocketMQ发送消息保证消息最终一致性需要做哪些处理?【第15期】

前言

发送消息的逻辑在发给mq后消息链路就直接结束了。那么消息在mq收到后我们该如何保证后面消息一定能存储呢?执行业务逻辑出错了怎么办呢?当然这期只是IM特定场景下的一致性哈,不是分布式事务哈,有点小区别?使用MQ做分布式事务后面肯定也会发文章。

目前已经写的文章有。并且有对应视频版本。
git项目地址 【IM即时通信系统(企聊聊)】点击可跳转
sprinboot单体项目升级成springcloud项目 【第一期】
前端项目技术选型以及页面展示【第二期】
分布式权限 shiro + jwt + redis【第三期】
给为服务添加运维模块 统一管理【第四期】
微服务数据库模块【第五期】
netty与mq在项目中的使用(第六期(废弃))】
分布式websocket即时通信(IM)系统构建指南【第七期】
分布式websocket即时通信(IM)系统保证消息可靠性【第八期】
分布式websocket IM聊天系统相关问题问答【第九期】
什么?websocket也有权限!这个应该怎么做?【第十期】
分布式ID是什么,以美团Leaf为例改造融入自己项目【第十一期】
IM聊天系统为什么需要做消息幂等?如何使用Redis以及Lua脚本做消息幂等【第12期】
微信发送一条消息经历哪些过程。企业微信以及钉钉的IM架构对比【第13期】
微信群为什么上限是500人,IM设计系统中的群聊的设计难点【第14期】

B站上面关注我呐 B站和CSDN同名,1000粉丝后建群。然后B站关注我后可以私信CSDN来的,然后后面我建群的时候拉你!

问题回顾

简单的说问题就是IM发送一条消息投递到mq里面,然后mq如何能保证这条消息落库。确保消息一定能落库涉及到消息的可靠生产和消费。我们这篇文章先分析一下技术方案,然后进行选择,一步一步来。 以下是一些可以选择的步骤和策略:

    1. 可靠生产
      同步发送 :在发送消息时,使用 RocketMQ 的同步发送方式,这样生产者会等待消息服务器的响应,确保消息已经被成功接收。
      消息发送重试 :对于发送失败的消息,可以在生产者端进行重试。RocketMQ 客户端自带重试机制,但你也可以根据业务需求自定义重试策略。
      事务消息:如果消息发送是业务操作的一部分,可以使用 RocketMQ 的事务消息来保证本地事务和消息发送的原子性。这样,只有当本地事务成功提交时,消息才会被真正发送出去。
    1. 可靠消费
      消费状态确认 :在消费者处理完消息并成功将其落库后,必须正确地返回消费成功的状态。这样,RocketMQ 不会再次投递这条消息。
      幂等性处理 :为了防止消息被重复消费(例如,在消息重试的情况下),消费者在处理消息时需要实现幂等性。这通常通过在数据库中记录每条消息的唯一标识,并在处理前检查该标识是否已存在来实现。
      死信队列处理:对于无法成功处理的消息(如因为业务逻辑错误或系统异常),应该将其转移到死信队列。然后可以通过人工干预或自动化脚本处理这些消息,确保它们最终被正确处理。
    1. 消息监控和告警
      监控:利用 RocketMQ 提供的监控工具,如 RocketMQ Dashboard,监控消息的生产和消费状态,及时发现问题。
      告警:设置合理的告警规则,当发现消息堆积、消费延迟等异常情况时,及时通知相关人员进行处理。

选择技术方案

从上面可靠生产的方案选择异步发送重试策略。同步对系统性能影响大了。所以选择异步加重试。

采用如下流程

  1. 消息发送:IM 服务在用户发送消息时,通过 RocketMQ 的异步发送将消息投递到消息队列。
  2. 消息消费:消息消费服务监听消息队列,收到消息后进行处理,包括检查消息的幂等性,并将消息内容落库。
  3. 确认消费:消息成功落库后,消费者返回消费成功的状态给 RocketMQ,确保消息不会被重复投递。
  4. 异常处理:如果消息消费失败,利用 RocketMQ 的重试机制进行重试,或者将消息转移到死信队列,等待后续处理。
    通过上述步骤和策略,可以在使用 RocketMQ 时,尽可能地保证 IM 消息的可靠投递和落库。

rocketmq内置重试机制

  1. 消费者重试:对于消费者来说,如果消费消息失败(即消费逻辑抛出异常),消息会被标记为"重试消息"并稍后重新投递。RocketMQ 默认会重试 16 次,每次重试的间隔时间逐渐增加,从 1 秒到最多 2 小时。
  2. 生产者重试:对于同步发送的消息,如果发送失败,生产者客户端会根据配置的重试次数自动重试。异步发送和单向发送则需要开发者在回调函数中自行处理重试逻辑。
    自定义重试策略

开发者可以根据业务需求自定义重试策略,这包括但不限于以下几种方式:

  1. 修改默认重试次数和间隔:通过配置文件或编程方式,可以调整默认的重试次数和重试间隔时间,使其更适合特定的业务场景。
  2. 自定义消费失败处理逻辑:在消费者处理消息的逻辑中,可以捕获异常,并根据异常类型或其他业务条件决定是否重试、重试的次数和间隔,或者将消息转移到另一个队列等待人工处理。
  3. 异步发送的自定义重试:对于异步发送的消息,可以在发送失败的回调函数中实现自定义的重试逻辑,比如基于异常类型选择是否重试、重试的次数和间隔等。
  4. 使用延时消息实现重试:对于需要延迟重试的场景,可以将失败的消息作为延时消息重新发送,通过设置延时级别来控制重试的时间。
  5. 持久化重试队列:对于重试次数多、间隔长或需要特殊处理的消息,可以将其持久化到数据库或日志文件中,通过外部程序或定时任务控制重试逻辑。
    通过这些自定义策略,开发者可以根据业务的具体需求和特点,设计更加灵活和可靠的消息重试机制,以提高消息处理的成功率和系统的健壮性。

可靠生产代码演示

使用一个简单的例子

先添加依赖

xml 复制代码
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

代码块

java 复制代码
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MessageService {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public void sendMessageAsync(String topic, String message) {
        rocketMQTemplate.asyncSend(topic, message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                // 消息发送成功的处理
                System.out.println("Message sent successfully: " + sendResult);
            }

            @Override
            public void onException(Throwable e) {
                // 消息发送失败的处理
                System.err.println("Failed to send message: " + e.getMessage());
                // 这里可以实现重试逻辑,例如:
                // retrySendMessage(topic, message);
            }
        });
    }

    // 重试发送消息的方法,这里简单地递归调用自身,实际应用中可能需要限制重试次数
    private void retrySendMessage(String topic, String message) {
        System.out.println("Retrying to send message...");
        sendMessageAsync(topic, message);
    }
}

在上述代码中,sendMessageAsync方法通过RocketMQTemplate的asyncSend方法异步发送消息,并提供了一个SendCallback回调来处理发送成功和失败的情况。在onException方法中,我们可以实现重试逻辑 。这里的示例仅仅是简单地递归调用sendMessageAsync方法进行重试,实际应用中需要更复杂的重试策略,比如限制重试次数、设置重试间隔等。

请注意,过多的重试可能会对系统造成压力,特别是在高并发场景下。因此,设计重试策略时需要谨慎,确保既能提高消息发送的成功率,又不会对系统造成过大的负担。目前自己IM觉的重试三次吧。

可靠消息代码演示

  1. 定义消费者并设置重试次数
    首先,需要在消费者配置中设置最大重试次数。RocketMQ默认会重试16次,每次重试间隔会逐渐增加。如果使用Spring Boot集成的RocketMQ,可以通过配置文件设置重试次数,或者在消费者注解中直接指定。
  2. 消费逻辑中处理异常
    在消费逻辑中,捕获并处理可能发生的异常。如果消费失败,抛出一个自定义异常或特定的异常标记,RocketMQ会根据重试策略重新投递消息。
  3. 监听重试次数并转移至人工队列
    RocketMQ不直接支持在消费端设置重试次数后自动转移队列的功能。因此,需要在消费逻辑中自行实现。可以通过消息的RECONSUME_TIME属性来判断当前重试次数,如果达到预设的最大重试次数后仍然失败,则手动将消息发送到另一个队列。
    以下是一个简化的示例,展示如何在Spring Boot应用中使用RocketMQ实现这一逻辑:
java 复制代码
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Service;

@Service
@RocketMQMessageListener(topic = "your-topic", consumerGroup = "your-consumer-group", consumeMode = ConsumeMode.ORDERLY)
public class YourConsumerService implements RocketMQListener<Message<String>> {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    private static final int MAX_RECONSUME_TIMES = 3; // 最大重试次数

    @Override
    public void onMessage(Message<String> message) {
        try {
            // 处理消息
            System.out.println("Received message: " + message.getPayload());
            // 如果处理失败,抛出异常
            // throw new Exception("处理失败");
        } catch (Exception e) {
            Integer reconsumeTimes = message.getHeaders().get("RECONSUME_TIMES", Integer.class);
            if (reconsumeTimes != null && reconsumeTimes >= MAX_RECONSUME_TIMES) {
                // 超过最大重试次数,转移到人工队列
                rocketMQTemplate.convertAndSend("manual-handle-topic", message.getPayload());
                System.out.println("Transfer to manual handle queue: " + message.getPayload());
            } else {
                // 未达到最大重试次数,抛出异常,让RocketMQ进行重试
                throw new RuntimeException("Consume failed, need retry.");
            }
        }
    }
}

记录重试还失败的消息

配置一个专门的消费者,一般来说可以通过Web界面、日志、邮件等展示给操作人员进行人工处理。但是我们这里就简单的存储一下库吧,然后记录下失败的时候,记录下原因。方便后续分析原因。解释下原因。因为是IM系统时效性很重要的。人工处理黄花菜都凉了。所以一般使用自动化处理策略。我们的解决办法是先重试,重试还失败通知用户发送消息失败。以并发优先。以数据库数据为准。

java 复制代码
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Service;

@Service
@RocketMQMessageListener(topic = "manual-handle-topic", consumerGroup = "manual-handle-group", consumeMode = ConsumeMode.ORDERLY)
public class ManualHandleConsumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        // 将消息展示给操作人员进行处理
        // 例如,打印消息、发送邮件、记录日志等
        System.out.println("Manual handle message: " + message);
        // 或者将消息存储到数据库,通过Web界面供操作人员处理
    }
}
相关推荐
Hsu_kk1 分钟前
Kafka 安装教程
大数据·分布式·kafka
苍老流年3 分钟前
1. kafka分布式环境搭建
分布式·kafka
sj11637394034 分钟前
Kafka参数了解
数据库·分布式·kafka
Hsu_kk6 分钟前
Kafka Eagle 安装教程
分布式·kafka
CodingBrother9 分钟前
Kafka 与 RabbitMQ 的联系
分布式·kafka·rabbitmq
pblh12322 分钟前
2023_Spark_实验十五:SparkSQL进阶操作
大数据·分布式·spark
silver98861 小时前
分布式相关杂项
分布式
jerry6098 小时前
7天用Go从零实现分布式缓存GeeCache(改进)(未完待续)
分布式·缓存·golang
古人诚不我欺9 小时前
jmeter常用配置元件介绍总结之分布式压测
分布式·jmeter
星染xr11 小时前
kafka 生产经验——数据积压(消费者如何提高吞吐量)
分布式·kafka