rabbitmq如何避免消息不要一直堆积在服务器端

rabbitmq如何避免消息不要一直堆积在服务器端

1.消费者集群消费(在使用k8s根据流量的形式实现监控 动态扩容与缩容)
2.消费者批量的形式获取消息;
3.生产者批量的形式投递消息;
例如我们消费者实现每次批量消费5条消息,我们生产者在投递消息过程中,可以将5条msg消息直接
合并成一条msg 投递到mq中,消费者订阅到该msg消息 在拆分5条小msg。

在这里插入图片描述
以批量5条消息为例子,先放到缓存池,再开单独开线程将这5条消息合并为1条消息,再投放到 mq服务器端。
消费者消费 拿到一条消息,实际上里面有5条。5条消息中有消息消费失败,不会暂停,先扔到死信队列(备胎队列)。

如果缓存池 生产者没有够5条,难道就不进行5合1,消费者就不消费吗?不是,引入定时任务,即使没有满五条,假设就是2条,进行2合1投递。
生产者采用定时任务 每隔一段时间 将缓冲区中msg消息批量的形式投递到MQ中。
(缓存池用到了LinkedBlockingDeque队列,增加 删除效率高,而且队列增加消息,删除消息 线程安全 用到了lock锁)

生产者批量投递消息代码

方式1

生产者缓冲区存放消息的时候,就将该消息投递到MQ服务器端存放。批量的形式

package com.mayikt.main.container;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.mayikt.main.constant.MQConstant;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.concurrent.LinkedBlockingDeque;


@Component
@Slf4j
public class BatchDeliveryBuffer {
    @Autowired
    private AmqpTemplate amqpTemplate;
    /**
     * 定义缓冲区容量
     */
    private LinkedBlockingDeque<JSONObject> buffer = new LinkedBlockingDeque<>();

    public void addMsg(JSONObject data) {
        buffer.offer(data);
    }

    public BatchDeliveryBuffer() {
        new BatchBufferThread().start();
    }

    /**
     * 批量抓取线程
     */
    class BatchBufferThread extends Thread {
        private LinkedBlockingDeque<JSONObject> tempBuffers = new LinkedBlockingDeque<JSONObject>();
        @SneakyThrows
        @Override
        public void run() {
            while (true) {
                JSONObject msg = buffer.poll();
                if (msg == null) {
                    Thread.sleep(60);// 避免cpu飙高
                    continue;
                }
                tempBuffers.offer(msg); // 存入到 tempBuffer
                //如果tempBuffer满了 则合并成一条msg消息投递到mq中
                int size = tempBuffers.size();
                if (size < MQConstant.BUFFER_SIZE) {
                    continue;
                }
                ArrayList<JSONObject> jsonObjects = new ArrayList<>();
                for (int i = 0; i < size; i++) {
                    jsonObjects.add(tempBuffers.poll());
                }
                // 将该msg投递到mq中
                String batchBufferMsg = JSONObject.toJSONString(jsonObjects);
                amqpTemplate.convertAndSend("/mayikt_ex", "", batchBufferMsg);
                log.info(">>登录之后投递mq消息,批量形式异步处理后续操作..dataJSON:{}<<", batchBufferMsg);
                Thread.sleep(20);// 避免cpu飙高
            }
        }
    }

}

方式2

生产者采用定时任务 每隔一段时间 将缓冲区中msg消息批量的形式投递到MQ中。

    /**
     * 批量抓取线程
     */
    class BatchBufferThread extends Thread {
        @SneakyThrows
        @Override
        public void run() {
            while (true) {
                Thread.sleep(MQConstant.BUFFER_INTERVAL_TIME);
                if (buffer.size() <= 0) {
                    continue;// 结束本次循环
                }
                ArrayList<JSONObject> tempBuffer = new ArrayList<>();
                for (int i = 0; i < buffer.size(); i++) {
                    tempBuffer.add(buffer.poll());
                }
                // 将该msg投递到mq中
                String batchBufferMsg = JSONObject.toJSONString(tempBuffer);
                amqpTemplate.convertAndSend("/mayikt_ex", "", batchBufferMsg);
                log.info(">>登录之后投递mq消息,批量形式异步处理后续操作..dataJSON:{}<<", batchBufferMsg);
            }
        }
    }

消费者批量消费过程

以日志讲解 批量的形式插入数据到db当中。

package com.mayikt.main.consumer.mapper;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mayikt.main.consumer.entity.SysUser;
import com.mayikt.main.consumer.entity.SysUserLoginLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * <p>
 * 用户信息表 Mapper 接口
 * </p>
 *

 */
public interface SysUserMapper extends BaseMapper<SysUser> {

    @Insert("<script>INSERT INTO `sys_admin`.`sys_user_login_log` (`user_id`, `login_ip`, `login_time`, `login_token`, `channel`, `equipment`, `is_Delete`, `message_id`)  values " +
            "<foreach collection='list' item='c' separator=','>(#{c.userId},#{c.loginIp},#{c.loginTime},#{c.loginToken},#{c.channel},#{c.equipment},#{c.isDelete},#{c.messageId})</foreach></script>")
    int batchInsert(@Param("list") List<SysUserLoginLog> sysUserLoginLogs);
}

package com.mayikt.main.consumer;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.mayikt.common.core.utils.RedisUtil;
import com.mayikt.main.consumer.constant.RabbitMQConstant;
import com.mayikt.main.consumer.entity.SysUserLoginLog;
import com.mayikt.main.consumer.mapper.SysUserLoginLogMapper;
import com.mayikt.main.consumer.mapper.SysUserMapper;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;


@Component
@Slf4j
public class UniqueLoginBatchConsumer {
    @Autowired
    private SysUserLoginLogMapper sysUserLoginLogMapper;

    @RabbitHandler
    @RabbitListener(queues = "fanout_uniquelogin_queue")
    @Transactional
    public void process(String msg, Channel channel, Message message) throws IOException {
        log.info("开始批量处理:msgs{}", msg);
        List<SysUserLoginLog> sysUserLoginLogs = JSONObject.parseArray(msg, SysUserLoginLog.class);
        try {
            // 采用mybatis 批量插入数据
            int result = sysUserLoginLogMapper.batchInsert(sysUserLoginLogs);
            log.info("<batchInsert result:{} >", result);
        } catch (Exception e) {
            log.error("<e:{}>", e);
            // 将异常消息记录下来 后期补偿
        } finally {
            long deliveryTag = message.getMessageProperties().getDeliveryTag();
            channel.basicAck(deliveryTag, true);// 手动ack成功 发送通知给 mq服务器端说 消费成功了 msg 从mq服务器端删除
        }
        // 只有异常的情况下 才会不断重试重试!

    }
}



批量消费的原理:

1.生产者会将多个msg 合并成一条msg(消费者批量消费速率)5–20
过多的msg 合并成一条msg 变成一个消费者 单次消费效率很低
2.消费者批量获取msg-----获取一条msg 让后在拆分n多条msg。

如果在批量消费的过程中,如果中间某个msg消费失败呢 如何处理?

回答:先将该msg转移存放死信队列(备胎队列中)后期补偿
继续向下消费 不能够为了这一个msg 卡主 。

msg消息堆积在mq中有哪些原因?

1.生产者投递消息之后----没有对应的消费者来消费。----消费者挂了
解决: 消费者集群的形式消费。—保证高可用的问题 mq消费者集群的形式
2.生产者投递消息的速率大于消费者消费的速率
对消费者集群消费 消费者集群的数量如何控制呢?
—压测单机版本消费者消费速率能力
----整合k8s 根据流量的形式监控 自动启动消费者弹性扩容 如果流量下降的情况下灵活缩容。
3.对单机版本消费者 优化改成批量的形式消费
高并发的情况下减轻数据库访问的压力,所以消费者批量数据 插入数据库中。
批量插入日志案例

MQ重试过程如何避免幂等性问题

情况: 消费者消费代码异常

默认的情况下 MQ服务器端给消费者不断重试!
发生效果:
A.容易引发消费者对应的服务器 cpu飙高的问题 应该设定间隔重试策略 例如 每隔3s 重试 重试五次还是失败的情况下,存放在死信队列中。

如果消费者代码有bug,MQ服务器端会不断重试 (幂等),如果本身代码是有bug的话 ,不建议说一直重试 、意味着该消息就会一直堆积在mq服务器端。----代码落地实战 死信队列整合

B.注意消费者不要try?不然 重试策略会失效
情况2: 消费者如果采用手动ack模式,不返回结果给MQ服务器端,该msg消息会一直存放在rabbitmq服务器端中

如果消费者代码 有异常自己了try? mq服务器端以为你的代码没有bug 认为该msg消息是消费成功的,则会从mq服务器端进行删除。
消费者代码 消费的模式 自动签收的模式 消费者消费失败的情况下(代码有bug) 该msg消息会一直存放在mq服务器端。

如果消费者被重试多次还是消费失败的情况下 如何处理呢?

认为该msg 消费成功,然后再将该msg消息存放在备胎队列中,后期通过改造消费者代码的 形式来进行实现。

判断重试次数 超过三次还是失败, 则将该消息存入到死信队列中 或者是备胎队列中 、或者存入 mysql表中 后期 直接补偿

什么情况下 消费者可以被重试呢?

1.消费者 调用第三方接口 网络抖动延迟 可以重试
2.消费者 代码的bug的情况下 需要发布版本才可以解决 认为该msg 消费成功,让后在将该msg消息存放在备胎队列中,后期通过改造消费者代码的
形式来进行实现。

rabbitmq如何保证消费者幂等性

核心点就是根据我们的全局id提前查询业务链是否执行过 如果有执行过的情况下就不能够重复执行。
在重试过程中遇到并发了 重试(重试间隔时间设置长一点、)db层面保证数据幂等性问题
真的在并发重试 最终只会有一个插入成功插入失败 重新重试 查询数据发现在数据库中已经存在就不会重复执行。

当前表结构需要修改 新增一个字段 message_id

  1. 消费者获取消息,如果消费消息失败, mq 服务器则会间隔的形式 实现重试策略
  2. 重试过程中,需要保证业务幂等性(保证数据不能够重复)问题,保证业务不能够重复执行;
  3. 我们可以通过全局的消息 id,提前查询如果该业务逻辑已经执行过,则不会重复执行。
    (提前根据全局id查询该表中的数据 如果存在 就无需重复执行业务逻辑。
    则需要应该手动ack 让MQ不要在重试。手动ack成功 发送通知给 mq服务器端说 消费成功了 msg 从mq服务器端删除
  4. 我们也需要在数据库的 db 层面需要保证幂等性问题,唯一主键约束、乐观锁等。
    (同时数据库设置根据messageid 不能够重复 在最底层避免重复插入)

如何生成全局id? uuid?

rabbitmq如何保证消息不丢失

1.生产者:

必须确保消息投递到MQ成功
Ack 消息确认机制
同步或者异步的形式
方式1:Confirms
方式2:事务消息
如果生产者投递消息失败的情况下,则通过日志记录下来后期通过定时任务自动补偿投递msg

2.MQ服务器端:

需要将消息持久化避免MQ宕机之后消息丢失; MQ服务器端 在默认的情况下 都会对队列中的消息实现持久化
持久化硬盘。
刷盘同步(严格意义上保证消息不丢失)或者是异步有可能会丢失。

3.消费者:

必须确保消息消费成功(同时需要注意幂等性问题);

3.1在rabbitmq情况下:

必须要将消息消费成功之后,会将该消息从mq服务器端中移除

3.2在kafka中的情况下:

不管是消费成功还是消费失败,该消息都不会立即从mq服务器端移除。手动提交offset
如果消费者消费失败的情况下则MQ会采用间隔的形式不断重试重试 。
重试过程中需要解决幂等性问题

如何解决幂等问题:

根据msgid作为全局id 根据该全局id提前查询下该 数据是否已经插入了如果插入了不能够继续插入,db层面根据该msgid创建一个唯一约束,防止db层面重复插入

4.延迟问题:

提高消费的速率
1.MQ消费者批量消费
2.MQ消费者集群消费
该流程环境是不需要解决消息顺序一致性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值