队列的一些场景题以及处理方式

1.如何防止重复消费(以rbmq为例其它原理都一样)

  • 消费者拿到消息 → 开始处理
  • 处理太慢,超时了
  • RabbitMQ 自动认为消费失败 → 隐式 NACK
  • 队列把这条消息重新放回队列,再发一次
  • 这时候,另一个消费者(或同一个消费者的另一个线程)又收到这条消息,开始执行第二遍
  • 关键来了:原来那个慢的消费者,根本不知道超时了,还在继续跑!
  • 最后:
    • 老线程执行完了
    • 新线程也执行完了→ 同一个业务逻辑执行了两遍 → 重复消费
  • 把消息给消费者,放到消费者内存里
  • 但消息本体还在 MQ 队列里,一直都在
  • 只是状态变成 unacked(被拿走待确认)
  • 等待结果:
    • ACK → 队列里这条消息删掉
    • NACK / 超时 → 状态变回 ready再发一遍

下面来看解决方法:

分布式锁 + 状态幂等 + 手动 ACK/NACK + 重试 3 次进死信 +乐观锁+ 防重复消费

下面来看代码:

java 复制代码
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

    listener:
      simple:
        # 必须手动ACK,超时才生效
        acknowledge-mode: manual
        # 消费超时时间:毫秒
        consumer-timeout: 60000  # 60秒

消费者完整代码

java 复制代码
import com.rabbitmq.client.Channel;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

@Component
@RequiredArgsConstructor
public class OrderConsumer {

    private final StringRedisTemplate redisTemplate;
    private final OrderMapper orderMapper;
    private final RabbitTemplate rabbitTemplate;

    // 监听业务队列
    @RabbitListener(queues = "order.queue")
    public void handleOrderMessage(String orderId, Message message, Channel channel) throws IOException {
        // 消息唯一标识,必须用这个ACK/NACK
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        // 分布式锁key:用订单id做唯一标识
        String lockKey = "lock:order:" + orderId;
        // 从消息头获取已重试次数,默认0
        Integer retryCount = (Integer) message.getMessageProperties()
                .getHeaders().getOrDefault("retry-count", 0);

        try {
            // ====================== 1. 先加Redis分布式锁 ======================
            // 30秒过期,防止服务宕机死锁
            Boolean lockSuccess = redisTemplate.opsForValue()
                    .setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);

            // 拿不到锁:说明有线程正在处理,直接拒绝,不重试
            if (Boolean.FALSE.equals(lockSuccess)) {
                channel.basicNack(deliveryTag, false, false);
                return;
            }

            // ====================== 2. 幂等判断 ======================
            Order order = orderMapper.selectById(orderId);
            // 订单不存在/不是待支付状态:说明已经处理过,直接ACK,不重复执行
            if (order == null || !"WAIT_PAY".equals(order.getStatus())) {
                channel.basicAck(deliveryTag, false);
                return;
            }

            // ====================== 3. 乐观锁CAS更新(核心兜底) ======================
            // 只有状态是WAIT_PAY,才能改成PROCESSING,原子操作
            int updateRows = orderMapper.casUpdateStatus(orderId, "WAIT_PAY", "PROCESSING");
            // 更新行数=0:说明被其他线程抢先处理了,直接ACK结束
            if (updateRows == 0) {
                channel.basicAck(deliveryTag, false);
                return;
            }

            // ====================== 4. 真正执行业务逻辑 ======================
            // 这里写你的核心业务:扣库存、调用支付、生成物流单等
            // ......

            // 业务执行成功:更新订单最终状态
            orderMapper.casUpdateStatus(orderId, "PROCESSING", "SUCCESS");

            // 业务成功:手动ACK,消息从队列删除
            channel.basicAck(deliveryTag, false);

        } catch (Exception e) {
            // ====================== 异常处理:重试3次,超过进死信 ======================
            retryCount = retryCount + 1;
            if (retryCount >= 3) {
                // 重试满3次:直接NACK,不重回队列,自动进死信
                channel.basicNack(deliveryTag, false, false);
            } else {
                // 不足3次:把重试次数塞进消息头,重新投递到原队列
                message.getMessageProperties().setHeader("retry-count", retryCount);
                rabbitTemplate.send("order.exchange", "order.routingKey", message);
                // 原消息ACK,避免重复
                channel.basicAck(deliveryTag, false);
            }
        } finally {
            // ====================== 无论成功失败,必须释放锁 ======================
            redisTemplate.delete(lockKey);
        }
    }
}

OrderMapper 里的 CAS 方法:

java 复制代码
@Update("UPDATE `order` SET status = #{targetStatus} " +
        "WHERE id = #{orderId} AND status = #{expectStatus}")
int casUpdateStatus(String orderId, String expectStatus, String targetStatus);

@Update("UPDATE `order` SET status = #{status} WHERE id = #{orderId}")
int updateStatus(String orderId, String status);
java 复制代码
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderMapper extends BaseMapper<Order> {

    /**
     * 核心:用status做乐观锁的CAS原子更新
     * 只有订单当前状态是expectStatus,才会更新成targetStatus
     * 数据库行锁保证:同一时间只有1个线程能更新成功
     */
    @Update("UPDATE `order` SET status = #{targetStatus} " +
            "WHERE id = #{orderId} AND status = #{expectStatus}")
    int casUpdateStatus(String orderId, String expectStatus, String targetStatus);
}
  • 消息来了先加 Redis 分布式锁同一订单同时只能有一个线程处理,防止并发重复。

  • 拿锁后先查数据库状态如果已经不是待支付,说明处理过,直接 ACK,不再执行。

  • 执行业务并更新订单状态 状态一旦改变,天然实现幂等,后面再来多少条消息都不会重复执行。

  • 然后在进行CAS判断如果说状态是WALL_PAY(待支付)就可以改然后改成已支付,然后下面在判断是不是Processing,是的话就改成已支付

  • 成功就 ACK,消息删除告诉 RabbitMQ 消费完成。

  • 失败就计数,最多重试 3 次超过 3 次直接 NACK 进死信,不卡死队列。

  • finally 里一定删锁避免死锁,保证下一次能正常消费。

用分布式锁防并发,用数据库状态做幂等,还有CAS,用手动 ACK 保证可靠性,用重试 + 死信避免死循环,从根源彻底防止消息重复消费。

为什么要加锁,其实就是当第一个线程超时的时候,然后发送NACK,另一个线程也来执行逻辑,所有判断都一路无阻然后在执行扣减库存然后就会多扣,加了锁此时第一个线程拿到锁但是锁还没有过期即使他超时了,然后其他消费者线程也执行这个逻辑是会被阻拦到拿锁那一步的。

为什么要加幂等性就是当第一个消费者线程锁过期了,然后第二个消费者线程进来,然后如果说不加幂等判断当线程1执行完,然后线程2也会继续执行然后就多扣了,你进行幂等判断当线程1执行完那个status就会被改变了,然后线程2就会被阻拦到这一步。

为什么要用CAS:就是当有一种情况就是当锁达到过期时间进行释放,此时也超时了,那么就会发送NACK然后就会再开一个线程执行这些逻辑,然后那个线程拿到锁然后判断什么的都成功了,此时原来那个超时的线程也要开始该状态了,如果你不加cas那他俩都会进行改状态,也会改成功,然后当然下面的扣库存也会进行成功,就没有办法防止重复消费了,所以要加CAS。

一、整套防重复机制总结

用分布式锁防并发,用数据库状态做幂等,用 CAS 做最终兜底,用手动 ACK 保证消息可靠性,用重试 + 死信避免死循环,从根源彻底防止消息重复消费。


二、为什么要加分布式锁

当第一个消费者线程执行超时 ,MQ 会自动返回 NACK,消息重新入队,第二个线程就会开始执行。如果不加锁,两个线程会同时一路执行到底 ,都去扣库存、改状态,导致重复扣减、数据错误

加了分布式锁后:同一时间只有一个线程能拿到锁 。即使第一个线程超时了,只要锁没过期,其他线程在拿锁这一步就会被直接拦住,根本进不来,从源头挡住并发重复。


三、为什么要加幂等判断(状态判断)

极端场景:第一个线程的锁过期自动释放了,第二个线程成功拿到锁。如果不加幂等判断,当线程 1 执行完业务、线程 2 也会继续执行,依然会重复扣库存、重复处理。

加了幂等判断:线程 1 执行完后,订单状态会被修改 。线程 2 进来后,第一步就查到状态已变,直接被拦截,不再执行业务,避免重复执行。


四、为什么必须加 CAS(乐观锁)

最极端的场景:第一个线程锁过期释放了 ,同时消费超时,返回 NACK。第二个线程进来,拿到锁 + 状态判断都通过此时,原来的旧线程恢复了,也要开始改状态、扣库存。

如果不加 CAS:两个线程都会修改成功,都会扣库存,重复消费无法避免!

加了 CAS:

sql 复制代码
UPDATE order
SET status = ?
WHERE id = ? AND status = WAIT_PAY

这是数据库原子操作只有一个线程能更新成功 。另一个线程更新行数 = 0,直接被拦住,绝对不会执行库存扣减

CAS 是最后一道防线,彻底杜绝锁超时带来的重复执行问题。


五、整套机制的三层防护(最核心)

  1. 分布式锁:挡住并发,同一时间只让一个线程进
  2. 状态幂等:挡住已处理完的重复消息
  3. CAS 乐观锁:兜底挡住锁过期的极端情况

六、最终一句话(最强总结)

分布式锁防并发,状态幂等防重复,CAS 兜底防锁超时, 三层防护层层兜底,从根源彻底杜绝消息重复消费。

RocketMQ如何保证消息的顺序性:

先来看为什么会出现顺序性问题:

  • 主体:A、B、C 是三个先后发起下单请求的用户,正常业务要求:必须先完整处理完先下单的 A 用户,再处理 B,再处理 C,全局顺序不能乱
  • 问题根源 1(生产者集群闯的祸):你用了订单服务集群(多实例部署),A 用户的下单请求落到了实例 1,B 用户的落到了实例 2,C 用户的落到了实例 3。哪怕 A 用户是先点的下单、先触发的消息发送,但实例 1 刚好遇到网络波动,导致 A 用户的消息,比后下单的 B 用户的消息晚到 MQ,队列里的顺序直接变成了 B → A → C入队顺序本身就错了
  • 问题根源 2(消费者集群补的刀):你用了消费者集群,多个实例同时监听这个队列。哪怕队列里顺序是对的A→B→C,MQ 也会把 A 分给消费者 1,B 分给消费者 2,C 分给消费者 3,并行处理。结果 A 的处理卡了,B、C 先处理完了,最终执行顺序变成了B→C→A哪怕入队顺序对,执行顺序也乱了

要 100% 保证「全局用户请求严格按顺序处理」,唯一的、缺一个条件都不行的方案,就是你一直坚持的:

生产端:禁用生产者集群,只用单生产者单实例,同一个线程严格按用户请求的先后顺序串行发送消息,保证先发起的 A,一定先入队,彻底杜绝网络波动导致的入队乱序

或者是用分布式锁,它们共用同一把锁也就是锁都是长得一样的也就是那个key,在生产者就拿,发送成功后在释放,然后再让别的去拿。

如何处理RocketMQ消息积压问题:

一、源头控制(生产者侧)

  1. 生产者限流限制消息发送速率,避免生产速度远大于消费速度,从源头减少队列压力。

  2. 非核心消息降级 / 丢弃日志、统计、通知类非核心消息可临时关闭发送,优先保障核心业务消息。


二、消费速度优化(消费者侧核心)

  1. 优化消费逻辑中的 SQL加索引、避免全表扫描、减少大事务、批量操作,降低单条消息处理耗时。

  2. 减少外部慢调用避免消费时调用慢接口、慢 RPC、冗余 IO,能异步的都异步。

  3. 提高消费并发增加消费者线程数 / 新增消费者实例,提升整体消费能力。(注意:全局有序场景不能随便加并发)

  4. 调整预取数 prefetch适当调大消费者一次性拉取消息数量,减少网络 IO 开销。

  5. 关闭不必要的手动重试 / 死循环重试避免失败消息反复消费占用消费线程。


三、队列架构急救(严重积压时)

  1. 废弃原积压队列,新建队列接收新流量旧队列不再使用,新消息直接发到新队列,保证当前业务不阻塞。

  2. 旧队列消息转移到死信 / 临时队列存量积压消息转发到死信队列或临时队列,用独立消费者慢慢消化,不影响主流程。

  3. 批量清理无效消息过期、重复、已失效的消息直接 ACK 丢弃,快速减少队列长度。


四、Broker 层面优化

  1. 队列分片 / 多队列拆分按用户 ID / 订单 ID 哈希路由到多个队列,并行消费提升吞吐量。
  2. 清理队列冗余、避免队列过长
  3. 提升 MQ 节点资源(CPU、内存、磁盘)

五、兜底保障

  1. 消费幂等,防止重复处理导致业务异常
  2. 死信队列兜底,消费失败消息不丢失
  3. 监控报警,堆积数量、消费速度实时告警

RocketMQ如何解决消息丢失问题

1. 生产者发消息:同步发送 + 事务注解

(1)同步发送(sync send)

  • 生产者发送消息 → 阻塞等待 Broker 确认
  • 收到 SEND_OK 才算发送成功
  • 优点:可靠性最高,消息不会丢
  • 缺点:性能比异步、单向发送低

适用场景:订单创建、支付、扣费等核心业务消息,绝不允许丢失

(2)加事务注解(分布式事务消息)

RocketMQ 事务消息 为例:

流程

  1. 生产者发送 半消息 Half Message
  2. Broker 响应半消息发送成功
  3. 执行本地事务(@Transactional 注解)
  4. 根据本地事务结果,向 Broker 提交 COMMITROLLBACK
  5. 如果没提交 / 超时,Broker 会回查生产者确认状态

作用

保证:本地事务成功 ↔ 消息一定发送成功 本地事务失败 ↔ 消息一定不发送

彻底解决:本地事务成功但消息没发出去 / 消息发了但本地事务回滚 的问题。


2. 异步发送失败:回调机制处理

异步发送:producer.sendAsync(msg, callback)

回调接口(Callback)

两个方法:

  • onSuccess():消息发送成功
  • onException()发送失败(网络异常、Broker 宕机、流量超限等)

失败时做什么?(生产实战)

  1. 日志记录(消息 ID、内容、时间、错误信息)
  2. 存入 DB 重试表
  3. 定时任务重试(重试 3~5 次)
  4. 仍失败 → 告警 + 人工介入

目的:异步发送不能丢消息,失败必须有兜底。


3. 队列端:同步刷盘

什么是刷盘?

消息到 Broker 后,从 内存 → 写入磁盘 的过程。

同步刷盘(SYNC_FLUSH)

  • 消息写入磁盘成功后 ,才返回 ACK 给生产者
  • 只要 Broker 回复成功,消息一定落盘
  • 机器断电、宕机也不会丢

异步刷盘(ASYNC_FLUSH)

  • 写到内存就返回成功
  • 后台线程批量刷盘
  • 性能高,但断电可能丢少量消息

你要记住一句话

同步刷盘 = 数据绝对安全,性能稍低 异步刷盘 = 性能极高,有极低丢消息风险

金融、支付必须用 同步刷盘


4. 消费者端:消息重试机制(最重点、最复杂)

(1)什么时候会重试?

  • 消费者业务抛异常
  • 消费超时
  • 主动返回 RECONSUME_LATER

(2)重试规则(RocketMQ 为例)

默认 16 次重试,间隔越来越长:

  1. 10s
  2. 30s
  3. 1min
  4. 2min
  5. 4min
  6. 6min...直到最后 2 小时

超过最大重试次数 → 进入 死信队列(DLQ)

(3)死信队列(Dead Letter Queue)

  • 重试多次仍失败的消息
  • 不会丢弃,进入专门的死信队列
  • 人工排查:数据问题、接口挂了、依赖异常等

(4)消费重试必须注意

  1. 消费者必须幂等重试会导致重复消费 → 必须保证重复消费不影响结果
  2. 不要捕获异常后不处理异常吃掉 = 消息被认为消费成功,丢消息
  3. 慢消费会导致大量重试队列堵塞 → 系统雪崩

(5)消费重试的本质

Broker 收到消费失败响应 →不删除消息,重新发送给消费者

不是消息真的 "复制",是重投递


整体串一遍(生产完整链路)

  1. 生产者同步发送,保证消息不丢
  2. 事务注解,保证本地事务与消息一致
  3. 异步发送失败用回调记录 + 重试
  4. Broker 同步刷盘,消息落盘才确认
  5. 消费者失败自动重试
  6. 最终失败进入死信队列人工处理
相关推荐
ictI CABL2 小时前
MySQL数据库的数据文件保存在哪?MySQL数据存在哪里
java
鱼鳞_2 小时前
Java学习笔记_Day20(二叉树)
java·笔记·学习
番茄去哪了2 小时前
任务调度功能实现
java·开发语言·spring boot
想你的液宝3 小时前
Spring Boot 中基于 AOP 的 Controller 统一日志打印方案
java·后端
天草二十六_简村人3 小时前
阿里云SLS采集jvm日志(上)
java·运维·数据库·后端·阿里云·容器·云计算
_MyFavorite_3 小时前
JAVA重点基础、进阶知识及易错点总结(15)缓冲流 + 转换流
java·开发语言·spring boot
qq_333120973 小时前
头歌答案--爬虫实战
java·前端·爬虫
TT哇3 小时前
【项目】从“本地能跑”到“生产级部署”:Java + Docker 自动化部署深度复盘
java·docker·自动化
摇滚侠3 小时前
JAVA 项目教程《苍穹外卖-11》,微信小程序项目,前后端分离,从开发到部署
java·开发语言·微信小程序