RabbitMQ延时队列

1.延时队列定时关单

复习RabbitMQ:7.延迟队列-7.4.队列TTL
https://www.cnblogs.com/yydscn/p/15208402.html

升级为:

1.1.代码

1.交换机、队列、路由key常量声明类:

public class OrderConstant {
    //定时关单交换机
    public static final String ORDER_EVENT_EXCHANGE = "order-event-exchange";

    //定时关单延时队列
    public static final String ORDER_DELAY_QUEUE = "order-delay-queue";

    //定时关单-订单释放队列
    public static final String ORDER_RELEASE_ORDER_QUEUE = "order-release-order-queue";

    //定时关单-下单路由key
    public static final String ORDER_CREATE_ORDER_ROUTING_KEY = "order.create.order.routing.key";

    //定时关单-订单释放路由key
    public static final String ORDER_RELEASE_ORDER_ROUTING_KEY = "order.release.order.routing.key";
}

2.RabbitMQ配置类:

@Configuration
public class RabbitMQConfig {

    /**
     * 使用JSON序列化机制,进行消息转换
     *
     * @return
     */
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }


    /**
     * 定时关单交换机
     *
     * @return
     */
    @Bean
    public DirectExchange orderEventExchange() {
        return ExchangeBuilder.directExchange(OrderConstant.ORDER_EVENT_EXCHANGE)
                .durable(true)
                .build();
    }

    /**
     * 定时关单延时队列
     *
     * @return
     */
    @Bean
    public Queue orderDelayQueue() {
        Map<String, Object> arguments = new HashMap<>(3);
        // 声明当前队列绑定的死信交换机
        arguments.put("x-dead-letter-exchange", OrderConstant.ORDER_EVENT_EXCHANGE);
        // 声明当前队列的死信路由 key
        arguments.put("x-dead-letter-routing-key", OrderConstant.ORDER_RELEASE_ORDER_ROUTING_KEY);
        // 声明队列的TTL
        arguments.put("x-message-ttl", 60000);
        return QueueBuilder.durable(OrderConstant.ORDER_DELAY_QUEUE)
                .withArguments(arguments)
                .build();
    }

    /**
     * 定时关单-订单释放队列
     *
     * @return
     */
    @Bean
    public Queue orderReleaseOrderQueue() {
        return QueueBuilder.durable(OrderConstant.ORDER_RELEASE_ORDER_QUEUE).build();
    }

    /**
     * 定时关单-下单绑定路由
     *
     * @param orderDelayQueue
     * @param orderEventExchange
     * @return
     */
    @Bean
    public Binding orderCreateOrderBinding(@Qualifier("orderDelayQueue") Queue orderDelayQueue,
                                           @Qualifier("orderEventExchange") DirectExchange orderEventExchange) {
        return BindingBuilder.bind(orderDelayQueue).to(orderEventExchange).with(OrderConstant.ORDER_CREATE_ORDER_ROUTING_KEY);
    }

    /**
     * 定时关单-订单释放绑定路由
     *
     * @param orderReleaseOrderQueue
     * @param orderEventExchange
     * @return
     */
    @Bean
    public Binding orderReleaseOrderBinding(@Qualifier("orderReleaseOrderQueue") Queue orderReleaseOrderQueue,
                                            @Qualifier("orderEventExchange") DirectExchange orderEventExchange) {
        return BindingBuilder.bind(orderReleaseOrderQueue).to(orderEventExchange).with(OrderConstant.ORDER_RELEASE_ORDER_ROUTING_KEY);
    }

}

3.消费者监听队列

@Slf4j
@Component
public class OrderReleaseConsumer {

    @Autowired
    private RedisTemplate redisTemplate;

    @RabbitListener(queues = OrderConstant.ORDER_RELEASE_ORDER_QUEUE)
    public void listenOrderReleaseOrderQueue(String msg, Message message, Channel channel) throws IOException {
        String messageId = message.getMessageProperties().getHeader("spring_returned_message_correlation");
        log.info("队列{}已经接收到id为{}的消息:{}", OrderConstant.ORDER_RELEASE_ORDER_QUEUE, messageId, msg);

        try {
            String fullKey = OrderCacheConst.ORDER_RELEASE_ORDER_KEY + messageId;
            Boolean flag = redisTemplate.hasKey(fullKey);
            //redis中不存在,则说明消息没有消费过,按正常业务处理;否则,直接跳过
            if (!flag) {
                Map messageMap = JSON.parseObject(msg, HashMap.class);
                String content = (String) messageMap.get("content");
                log.info("定时关单:" + content);
                //存入redis
                redisTemplate.opsForValue().setIfAbsent(fullKey, fullKey, OrderCacheConst.MQ_MESSAGE_ID_CACHE_TIMEOUT_DAYS, TimeUnit.DAYS);
            }
        } catch (Exception e) {
            //业务异常,不重回队列,而是放入死信队列处理
            log.error("队列{}接收到id为{}的消息:{},处理业务异常:{}", OrderConstant.ORDER_RELEASE_ORDER_QUEUE, messageId, msg, e);

            //TODO:记录消息日志:消费失败
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }

        //TODO:记录消息日志:消费成功
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

4.测试模拟发送下单消息

@SpringBootTest
class GulimallOrderApplicationTests {

    @Autowired
    private MQSenderUtil mqSenderUtil;

    @Test
    void contextLoads() throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Map<String, Object> messageMap = Maps.newHashMap();
            messageMap.put("content", "下单成功,延时队列定时关单" + i);
            String message = JSON.toJSONString(messageMap);
            mqSenderUtil.sendMessage(OrderConstant.ORDER_EVENT_EXCHANGE, OrderConstant.ORDER_CREATE_ORDER_ROUTING_KEY, message);
            System.out.println("下单成功" + i);
            TimeUnit.SECONDS.sleep(3);
        }
    }

}

1.2.附:RabbitMQ相关配置

1.引入依赖:

    implementation 'org.springframework.boot:spring-boot-starter-amqp'
    testImplementation 'org.springframework.amqp:spring-rabbit-test'

2.配置YML

spring:
  # 消息中间件配置
  rabbitmq:
    addresses: 192.168.56.10:5672
    username: guest
    password: guest
    # 配置虚拟机
    virtual-host: /
    # 开启发送端消息抵达Broker交换机确认
    publisher-confirm-type: correlated
    # 开启发送端消息抵达Queue确认
    publisher-returns: true
    # 只要消息抵达Queue,就会异步发送优先回调return confirm
    template:
      mandatory: true
    listener:
      type: simple
      # 消费端开启手动ack消息,不使用默认自动ack
      simple:
        acknowledge-mode: manual
        retry:
          enabled: true # 开启重试机制
          max-attempts: 3 # 最大重试传递次数
          initial-interval: 5000ms # 第一次和第二次尝试传递消息的间隔时间,单位毫秒
          multiplier: 3 # 上一重试间隔的乘数 步长
          max-interval: 300000ms # 最大重试时间间隔,单位毫秒
        # 以上配置的间隔0s  5s  15s  45s
        # 重试次数超过上面的设置之后是否重回队列(消费者listener抛出异常,是否重回队列,默认true:重回队列,false为不重回队列(结合死信交换机))
        default-requeue-rejected: false

3.发送消息工具类

@Slf4j
@Configuration
public class MQSenderUtil {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 依赖注入rabbitTemplate之后再设置它的回调对象
     * Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)
     */
    @PostConstruct
    public void init() {

        /**
         * 交换机不管是否收到消息的一个回调方法
         * 1.CorrelationData 消息相关数据
         * 2.ack 交换机是否收到消息
         * 3.cause 未收到消息的原因
         */
        RabbitTemplate.ConfirmCallback confirmCallback = (correlationData, ack, cause) -> {
            String id = correlationData != null ? correlationData.getId() : "";
            if (ack) {
                log.info("交换机已经接收到id为:{}的消息", id);

            } else {
                //交换机未接收到的消息,需要重试发送消息
                log.error("交换机未接收到id为:{}的消息,由于原因:{}", id, cause);

            }
        };

        /**
         * 消息回退:只有当消息传递过程中不可达目的地时才将消息返回给生产者
         */
        RabbitTemplate.ReturnsCallback returnsCallback = returned -> {
            //被退回的消息保存到备份交换机,由备份交换机来进行转发和处理
            String errorMsg = String.format("消息:%s被交换机退回,退回原因:%s,交换机是:%s,路由key:%s",
                    new String(returned.getMessage().getBody()), returned.getReplyText(), returned.getExchange(), returned.getRoutingKey());
            log.error(errorMsg);

            String messageId = returned.getMessage().getMessageProperties().getHeader("spring_returned_message_correlation");
            log.error("被退回的消息ID:{}", messageId);

        };

        rabbitTemplate.setConfirmCallback(confirmCallback);
        rabbitTemplate.setReturnsCallback(returnsCallback);
    }

    /**
     * 发送消息
     *
     * @param exchange   交换机名
     * @param routingKey 路由key名
     * @param message    消息
     */
    public void sendMessage(String exchange, String routingKey, String message) {
        CorrelationData correlationData = new CorrelationData(String.valueOf(SnowflakeIdGenerator.generateUniqueId()));
        log.info("生产者发送消息id:{}, message:{}, exchange:{}, routingKey:{}", correlationData.getId(), message, exchange, routingKey);

        try {
            rabbitTemplate.convertAndSend(exchange, routingKey, message, messagePostProcessor -> {
                //设置消息持久化
                messagePostProcessor.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                return messagePostProcessor;
            }, correlationData);

        } catch (Exception e) {
            log.error("生产者发送消息失败id:{}, message:{}, exchange:{}, routingKey:{}, 异常原因:{}", correlationData.getId(), message, exchange, routingKey, e);
        }

    }

    /**
     * 消息重试
     *
     * @param exchange
     * @param routingKey
     * @param messageId
     * @param message
     */
    public void retryMessage(String exchange, String routingKey, String messageId, String message) {
        CorrelationData correlationData = new CorrelationData(messageId);
        log.info("ERP生产者重试消息id:{}, message:{}, exchange:{}, routingKey:{}", messageId, message, exchange, routingKey);

        try {
            rabbitTemplate.convertAndSend(exchange, routingKey, message, messagePostProcessor -> {
                //设置消息持久化
                messagePostProcessor.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                return messagePostProcessor;
            }, correlationData);

        } catch (Exception e) {
            log.error("ERP生产者重试消息失败id:{}, message:{}, exchange:{}, routingKey:{}, 异常原因:{}", correlationData.getId(), message, exchange, routingKey, e);
        }
    }
}

分布式ID生成器(雪花算法):

package com.sen.gulimall.order.utils;

import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.Random;

/**
 * @author Kisen
 * @email liuqs@jaid.cn
 * @date 2022/8/28 17:25
 * @detail 分布式ID生成器(雪花算法)
 */
public class SnowflakeIdGenerator {


    /**
     * 时间戳标识所占二进制位数
     */
    private static final int TIME_STAMP_BIT_LEN = 41;
    /**
     * 机房标识所占二进制位数
     */
    private static final int SERVER_ROOM_BIT_LEN = 5;
    /**
     * 服务器标识所占二进制位数
     */
    private static final int SERVER_BIT_LEN = 5;
    /**
     * 每毫秒中序列所占二进制位数
     */
    private static final int SEQ_BIT_LEN = 12;

    /**
     * 时间戳标识向左移动的位数(这里的1标识最高位)
     */
    private static final int TIME_STAMP_LEFT_BIT_LEN = 64 - 1 - TIME_STAMP_BIT_LEN;
    /**
     * 机房标识左移位数
     */
    private static final int SERVER_ROOM_LEFT_BIT_LEN = TIME_STAMP_LEFT_BIT_LEN - SERVER_ROOM_BIT_LEN;
    /**
     * 服务器标识左移位数
     */
    private static final int SERVER_LEFT_BIT_LEN = SERVER_ROOM_LEFT_BIT_LEN - SERVER_BIT_LEN;

    /**
     * 开始时间戳,此处为 2022年8月28日
     */
    private static final long START_TIME_STAMP = 1661616000000L;
    /**
     * 上次生成ID的时间戳
     */
    private static long LAST_TIME_STAMP = -1L;
    /**
     * 上一次毫秒内存序列值
     */
    private static long LAST_SEQ = 0L;

    /**
     * 获取机房标识(可以手动定义0-31之间的数)
     */
    private static final long SERVER_ROOM_ID = getServerRoomId();
    /**
     * 获取服务器标识(可以手动定义0-31之间的数)
     */
    private static final long SERVER_ID = getServerId();

    /**
     * 机房标识最大值 +1
     */
    private static final int SERVER_ROOM_MAX_NUM_1 = ~(-1 << SERVER_ROOM_BIT_LEN) + 1;
    /**
     * 服务器标识最大值 +1
     */
    private static final int SERVER_MAX_NUM_1 = ~(-1 << SERVER_BIT_LEN) + 1;
    /**
     * 毫秒内存列的最大值
     */
    private static final long SEQ_MAX_NUM = ~(-1 << SEQ_BIT_LEN);

    /**
     * 对服务器地址的哈希码取余作为服务器标识
     *
     * @return 服务器标识
     */
    private static int getServerId() {
        try {
            String hostAddress = Inet4Address.getLocalHost().getHostAddress();
            return (hostAddress.hashCode() & Integer.MAX_VALUE) % SERVER_MAX_NUM_1;
        } catch (UnknownHostException e) {
            return new Random().nextInt(SERVER_MAX_NUM_1);
        }
    }

    /**
     * 对服务器名称的哈希码取余作为机房标识
     *
     * @return 机房标识
     */
    private static int getServerRoomId() {
        try {
            String hostName = Inet4Address.getLocalHost().getHostName();
            return (hostName.hashCode() & Integer.MAX_VALUE) % SERVER_ROOM_MAX_NUM_1;
        } catch (UnknownHostException e) {
            return new Random().nextInt(SERVER_ROOM_MAX_NUM_1);
        }
    }

    /**
     * 一直循环直到获取到下毫秒的时间戳
     *
     * @param lastMillis
     * @return 下一毫秒的时间戳
     */
    private static long nextMillis(long lastMillis) {
        long now = System.currentTimeMillis();
        while (now <= lastMillis) {
            now = System.currentTimeMillis();
        }
        return now;
    }

    /**
     * 生成唯一ID
     * 须加锁避免并发问题
     *
     * @return 返回唯一ID
     */
    public synchronized static long generateUniqueId() {
        long currentTimeStamp = System.currentTimeMillis();
        // 如果当前时间小于上一次ID生成的时间戳,说明系统时间回退过,此时应抛出异常
        if (currentTimeStamp < LAST_TIME_STAMP) {
            throw new RuntimeException(String.format("系统时间错误! %d 毫秒内拒绝生成雪花ID", START_TIME_STAMP));
        }
        if (currentTimeStamp == LAST_TIME_STAMP) {
            LAST_SEQ = (LAST_SEQ + 1) & SEQ_MAX_NUM;
            if (LAST_SEQ == 0) {
                currentTimeStamp = nextMillis(LAST_TIME_STAMP);
            }
        } else {
            LAST_SEQ = 0;
        }
        // 上次生成ID的时间戳
        LAST_TIME_STAMP = currentTimeStamp;
        return ((currentTimeStamp - START_TIME_STAMP) << TIME_STAMP_LEFT_BIT_LEN | (SERVER_ROOM_ID << SERVER_ROOM_LEFT_BIT_LEN) | (SERVER_ID << SERVER_LEFT_BIT_LEN) | LAST_SEQ);
    }

    /**
     * 主函数测试
     *
     * @param args
     */
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int num = 100;
        for (int i = 0; i < num; i++) {
            System.out.println(generateUniqueId());
        }
        long end = System.currentTimeMillis();

        System.out.println("共生成 " + num + " 个ID,用时 " + (end - start) + " 毫秒");
    }
}

2.下订单-消息队列架构图

库存解锁的场景:

1)、下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存
2)、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。

2.1.库存定时自动解锁代码

1.交换机、队列、路由key常量声明类

public class WareConstant {

    //解锁库存交换机
    public static final String STOCK_EVENT_EXCHANGE = "stock-event-exchange";

    //解锁库存延时队列
    public static final String STOCK_DELAY_QUEUE = "stock-delay-queue";

    //解锁库存队列
    public static final String STOCK_RELEASE_STOCK_QUEUE = "stock-release-stock-queue";

    //解锁库存路由key
    public static final String STOCK_RELEASE_ROUTING_KEY = "stock.release.#";

    //锁定库存路由key
    public static final String STOCK_LOCKED_ROUTING_KEY = "stock.locked";
}

2.RabbitMQ配置类

@Configuration
public class RabbitMQConfig {

    /**
     * 使用JSON序列化机制,进行消息转换
     *
     * @return
     */
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }


    /**
     * 解锁库存交换机
     *
     * @return
     */
    @Bean
    public TopicExchange stockEventExchange() {
        return ExchangeBuilder.topicExchange(WareConstant.STOCK_EVENT_EXCHANGE)
                .durable(true)
                .build();
    }

    /**
     * 解锁库存延时队列
     *
     * @return
     */
    @Bean
    public Queue stockDelayQueue() {
        Map<String, Object> arguments = new HashMap<>(3);
        // 声明当前队列绑定的死信交换机
        arguments.put("x-dead-letter-exchange", WareConstant.STOCK_EVENT_EXCHANGE);
        // 声明当前队列的死信路由 key
        arguments.put("x-dead-letter-routing-key", "stock.release");
        // 声明队列的TTL
        arguments.put("x-message-ttl", 50 * 60 * 1000);
        return QueueBuilder.durable(WareConstant.STOCK_DELAY_QUEUE)
                .withArguments(arguments)
                .build();
    }

    /**
     * 解锁库存队列
     *
     * @return
     */
    @Bean
    public Queue stockReleaseStockQueue() {
        return QueueBuilder.durable(WareConstant.STOCK_RELEASE_STOCK_QUEUE).build();
    }

    /**
     * 锁定库存绑定路由
     *
     * @param stockDelayQueue
     * @param stockEventExchange
     * @return
     */
    @Bean
    public Binding stockLockedBinding(@Qualifier("stockDelayQueue") Queue stockDelayQueue,
                                      @Qualifier("stockEventExchange") TopicExchange stockEventExchange) {
        return BindingBuilder.bind(stockDelayQueue).to(stockEventExchange).with(WareConstant.STOCK_LOCKED_ROUTING_KEY);
    }

    /**
     * 解锁库存绑定路由
     *
     * @param stockReleaseStockQueue
     * @param stockEventExchange
     * @return
     */
    @Bean
    public Binding stockReleaseBinding(@Qualifier("stockReleaseStockQueue") Queue stockReleaseStockQueue,
                                       @Qualifier("stockEventExchange") TopicExchange stockEventExchange) {
        return BindingBuilder.bind(stockReleaseStockQueue).to(stockEventExchange).with(WareConstant.STOCK_RELEASE_ROUTING_KEY);
    }

}

3.监听解锁队列

StockReleaseListener类:

@Slf4j
@Service
@RabbitListener(queues = WareConstant.STOCK_RELEASE_STOCK_QUEUE)
public class StockReleaseListener {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private WareSkuService wareSkuService;

    /**
     * 1、库存自动解锁。
     * 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁。
     * 2、订单失败。
     * 锁库存失败
     *
     * @param to
     * @param message
     * @param channel
     * @throws IOException
     */
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        String messageId = message.getMessageProperties().getHeader("spring_returned_message_correlation");
        log.info("队列{}已经接收到id为{}的消息:{}", WareConstant.STOCK_RELEASE_STOCK_QUEUE, messageId, to);

        try {
            String fullKey = WareCacheConst.STOCK_RELEASE_STOCK_KEY + messageId;
            Boolean flag = redisTemplate.hasKey(fullKey);
            //redis中不存在,则说明消息没有消费过,按正常业务处理;否则,直接跳过
            if (!flag) {

                wareSkuService.unlockStock(to);

                //存入redis
                redisTemplate.opsForValue().setIfAbsent(fullKey, fullKey, WareCacheConst.MQ_MESSAGE_ID_CACHE_TIMEOUT_DAYS, TimeUnit.DAYS);
            }
        } catch (Exception e) {
            //业务异常,不重回队列,而是放入死信队列处理
            log.error("队列{}接收到id为{}的消息:{},处理业务异常:{}", WareConstant.STOCK_RELEASE_STOCK_QUEUE, messageId, to, e);

            //TODO:记录消息日志:消费失败
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }

        //TODO:记录消息日志:消费成功
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

库存业务类WareSkuServiceImpl:


    @Override
    public void unlockStock(StockLockedTo to) {
        Long id = to.getId();
        StockDetailTo detailTo = to.getDetailTo();
        Long detailId = detailTo.getId();
        /**
         * 解锁
         * 1、查询数据库关于这个订单的锁定库存信息
         *    有:证明库存锁定成功了
         *      解锁:订单情况。
         *          1、没有这个订单。必须解锁
         *          2、有这个订单。不是解锁库存
         *              订单状态:已取消,解锁库存;没取消,不能解锁
         *    没有:库存锁定失败了,库存回滚了。这种情况无需解锁
         */
        log.info("收到解锁库存的消息");
        WareOrderTaskDetailEntity entity = wareOrderTaskDetailService.getById(detailId);
        if (entity != null) {
            WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(id);
            String orderSn = taskEntity.getOrderSn(); //根据这个订单号查询订单的状态
            R r = orderFeignService.getOrderStatus(orderSn);
            if (r.getCode() != 0) {
                throw new RuntimeException("远程服务失败");
            }

            //订单数据返回成功
            OrderVo data = r.getData(new TypeReference<OrderVo>() {
            });
            if (data == null || data.getStatus() == 4) {
                //订单不存在或订单状态已取消,解锁库存
                if (entity.getLockStatus() == 1) {
                    //当前库存工作单是已锁定状态,才需解锁
                    unLockStock(detailTo.getSkuId(), detailTo.getWareId(), detailTo.getSkuNum(), detailId);
                }
            }

        }
    }

    private void unLockStock(Long skuId, Long wareId, Integer skuNum, Long detailId) {
        //库存解锁
        wareSkuDao.unLockStock(skuId, wareId, skuNum);
        //更新库存工作单的状态
        WareOrderTaskDetailEntity updateDetailEntity = new WareOrderTaskDetailEntity();
        updateDetailEntity.setId(detailId);
        updateDetailEntity.setLockStatus(2); //已解锁
        wareOrderTaskDetailService.updateById(updateDetailEntity);
    }

4.测试模拟库存定时自动解锁

    StockLockedTo lockedTo = new StockLockedTo();
    lockedTo.setId(wareOrderTaskEntity.getId());
    StockDetailTo stockDetailTo = new StockDetailTo();
    BeanUtils.copyProperties(taskDetailEntity, stockDetailTo);
    lockedTo.setDetailTo(stockDetailTo);
    mqSenderUtil.sendMessage(WareConstant.STOCK_EVENT_EXCHANGE, WareConstant.STOCK_LOCKED_ROUTING_KEY, lockedTo);

2.如何保证消息可靠性(消息丢失、消息重复、消息积压)

2.1.消息丢失

  • 消息发送出去,由于网络问题没有抵达服务器
    • 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式
    • 做好日志记录,每个消息状态是否都被服务器收到都应该记录
    • 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
  • 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。
    • publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
  • 自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机
    • 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队

2.2.消息重复

  • 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
  • 消息消费失败,由于重试机制,自动又将消息发送出去
  • 成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送

方案:

  • 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
  • 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理
  • rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的

2.3.消息积压

  • 消费者宕机积压
  • 消费者消费能力不足积压
  • 发送者发送流量太大

方案:

  • 上线更多的消费者,进行正常消费
  • 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

总结:
防止消息丢失:
1、做好消息确认机制( pulisher, consumer【手动 ack】 )
2、每一个发送的消息都在数据库做好记录。 定期将失败的消息再次发送一遍

posted @ 2023-01-05 22:14  冰枫丶  阅读(52)  评论(0编辑  收藏  举报