GQ MQ

我看到你的GQ Video简历上提到了RocketMQ,你来讲讲你在这个项目上为什么用上了RocketMQ?

  • 我们最初用的是“线程池 + Redis 列表”做异步,能用但有几个痛点:服务间耦合重、突发流量容易把线程和 Redis 顶满;失败重试、死信、消息追踪都要自己造轮子;一旦进程挂了,队列里的任务容易丢,排障也不直观。

  • 所以我把“播放事件统计、转码任务”这两条链路迁到了 RocketMQ。好处很直接:

    • 彻底解耦:发布方只管发,消费方独立扩缩容;
    • 抗峰值更稳:堆积可观测,消费端可以水平扩展;发布端响应时间明显变短;
    • 可靠性提升:自动重试、死信队列、顺序/延时/事务消息这些能力都是现成的,任务丢失概率大幅下降;
    • 可观测:Topic 堆积、耗时、轨迹一目了然,问题定位快。

    你为什么考虑的是RocketMQ这个消息队列而不是RabbitMQ或者Kafka?

  • 之所以选 RocketMQ,而不是 RabbitMQ 或 Kafka,我主要考虑了这个项目的特点:

    • 对 RabbitMQ:它路由模型很灵活,但吞吐相对没那么高;延时消息要装插件,顺序保证容易受限于单队列瓶颈。我们的“播放事件”和“转码发布**”更偏大吞吐+延时/顺序/重试**,RocketMQ 原生支持更合适。
    • 对 Kafka:吞吐极强,做日志/流式计算非常好。但业务型消息常用的“单条精确重试、死信、延时、事务消息”需要额外组件或自研,运维门槛也更高。我们更看重业务可靠投递和易用性,所以优先 RocketMQ。
  • 落地时我做了一个开关 mq.enabled:开启走 MQ,关闭就回退到原来的“线程池 + Redis 队列”,可以灰度切换、出问题一键回滚。幂等用数据库唯一键或 Redis 做了防重。

一句话总结:上 RocketMQ 后,我们把“重活、峰值、可靠性、可观测性”这些问题一次性解决了;对这个以业务消息为主的场景,RocketMQ 比 RabbitMQ 更省插件、比 Kafka 更贴合业务消息的开箱能力。

你具体讲讲播放事件统计、转码任务这两个任务,没上MQ你是怎么实现的,用了MQ你又是怎么实现的?

当然可以 👍 我帮你把这段技术回答改成一个更自然、面试口语化、容易讲顺的版本👇


💬 面试口语版回答(MQ 改造播放统计 & 转码任务)

我用两分钟把“上 MQ 前后”的差异讲清楚。


一、没上 MQ 之前

那时候我们还没引入消息队列,用的是「线程池 + Redis 列表」来异步处理任务。

  • 播放事件
    前端上报播放,我这边直接把事件丢进 Redis 列表(LPUSH),后台有个线程池 while(true) 从列表里取(RPOP),
    做三件事:视频播放数 +1、写播放历史、同步 ES 计数。

    但问题挺多的:高峰期线程容易打满、Redis 队列堆积难监控,失败重试、死信都得自己写。进程挂了也不好排查。

  • 转码任务
    用户投稿上传完视频,我也是把转码任务信息塞进 Redis 列表,然后线程池轮询去拿,用 FFmpeg 转码、切片、传对象存储、最后更新数据库状态。
    同样的问题:并发不好控、延时触发不方便、重试补偿全靠自己维护。


二、上 MQ(RocketMQ)之后

后来我们用 RocketMQ 做异步解耦,效果挺明显。

我设计了两个 Topic:

  • video-play 专门处理播放事件
  • video-transcode 处理转码任务

然后写了一个通用发布器(Publisher),再配两个 Consumer。
另外加了个 mq.enabled 开关,可以灰度切换或快速回退到 Redis 方案。

  • 播放事件这块
    发布端接到上报后,直接把事件对象发到 MQ,不再阻塞主线程。
    消费端收到后,还是那三件事:数据库 +1、播放历史 upsert、ES 同步计数。
    可靠性方面 RocketMQ 自带重试、死信、堆积可查,我还做了幂等处理(比如用 userId+videoId+fileIndex 做唯一键)。
  • 转码任务这块
    用户提交稿件或合并分片后,我发一条转码消息到 video-transcode
    消费端收到后调转码组件,用 FFmpeg 转码、上传 MinIO、更新状态。
    并发我用 Semaphore 控制;异常重试、超出次数进死信队列,支持延时重试退避。

三、上 MQ 之后的变化

  • 性能上:主流程只负责发消息,响应速度更快。
  • 稳定性:堆积看得见,消费可水平扩,不容易被线程卡死。
  • 可靠性:MQ 自带重试、死信、轨迹,可观测性很好。
  • 扩展性:以后要加统计维度或新转码逻辑,只需多加个消费者,不改主流程。

✅ 一句话总结

以前用“线程池 + Redis 列表”能跑,但维护成本高、峰值容易崩;
上了 RocketMQ 后,我改成“发布订阅”模型,用它的重试、死信、延时和监控能力,把可靠性和扩展性都拉满,还能随时灰度回退,整体更稳更可控。


要不要我帮你再压缩成「30 秒速答版」?适合一开口就能讲流畅的那种。

说一下 RocketMQ 中关于事务消息的实现?

这个问题其实就是在考“RocketMQ 怎么保证消息和本地事务的一致性”。
RocketMQ 用的是两阶段提交 + 事务回查机制。

第一阶段:发送半消息(Half Message)

当我发送事务消息时,生产者会先往 MQ 里发一个“半消息”,也叫 Prepared 消息
这个消息已经写入 Broker,但对消费者是不可见的
然后生产者执行自己的本地事务,比如扣库存、加积分这些业务逻辑。

第二阶段:提交或回滚

本地事务执行完后有三种结果:

  • 成功:生产者告诉 MQ “我这边事务 OK 了”,MQ 把半消息改成 正式消息,消费者就能看到。
  • 失败:生产者通知 MQ 回滚,半消息就会被删除。
  • 没回消息:如果 MQ 一直收不到 commit 或 rollback,它会主动回查

事务回查(Transaction Check)
RocketMQ 会定期去问生产者:“你上次那个事务到底成了没?”
生产者实现 checkLocalTransaction 回调,返回三种状态:

  • COMMIT_MESSAGE → 确认提交
  • ROLLBACK_MESSAGE → 回滚消息
  • UNKNOWN → 还没确定,Broker 过一会儿再问

这样即使网络异常、应用重启,也能靠回查机制最终确认消息状态。

为什么要这么设计?

如果不用事务消息,而是“本地事务先提交,再发消息”,一旦第二步发消息失败,就会造成业务成功但消息丢,数据不一致。
事务消息相当于先发半消息 → 执行业务 → 最后 commit
就算 commit 消息失败,MQ 还能回查,最终也能补回来,保证一致性。

小结

RocketMQ 的事务消息通过“半消息 + 本地事务 + 回查机制”实现两阶段提交,
确保消息状态和业务事务状态保持一致,即使中途宕机也能恢复。

什么是消息队列?

消息队列是一种异步通信机制,用于在分布式系统中解耦发送方和接收方的通信。它通过在消息生产者和消费者之间引入一个中间缓冲区(broker),将消息存储在broker中,然后由消费者从brker中读取和处理消息。

img
我举个最常见的例子来说: 假设服务 A 每秒要发 200 个请求给服务 B,但服务 B 一秒只能处理 100 个。 这时候如果直接调用,B 很快就被压垮了。
那怎么办?
这时候我们就在中间加一层“消息队列”。

A 不再直接把请求发给 B,而是先把消息丢进队列,B 再按照自己的节奏、自己的处理能力,从队列里一点点取出来消费。
这样,A 和 B 就被队列隔离开了,互不影响。

消息队列到底解决了什么问题?(消息队列的作用)

  1. 异步处理
  • 以前 A 发完请求得等 B 处理完才能继续,现在有了 MQ,A 发完消息直接返回。

  • 这样响应更快、系统吞吐量也更高。流量削峰(流量控制)
    在高峰期,比如大促、秒杀时,请求量会瞬间暴增。
    消息队列可以当“缓冲层”,先把请求堆起来,消费者慢慢消费,防止系统被冲垮。
    这也是很多系统抗压的关键。

  • 比如:下单之后再去发短信、发邮件这种操作,其实不需要同步完成,放 MQ 里异步处理就行。

  1. 服务解耦
  • 有了消息队列,发送方和接收方就不再强绑定。
  • A 只负责“发消息”,根本不用知道是谁来消费。
  • 将来如果要多加几个消费者,比如一个做日志,一个做分析,直接订阅这个队列就行,完全不用改 A。
  1. 流量削峰(流量控制)
  • 在高峰期,比如大促、秒杀时,请求量会瞬间暴增。
  • 消息队列可以当“缓冲层”,先把请求堆起来,消费者慢慢消费,防止系统被冲垮。
  • 这也是很多系统抗压的关键。

消息队列的核心价值是:
解耦、异步、削峰。
它让系统更快(异步),更松耦合(解耦),也更稳(削峰)。

为什么需要消息队列?

img
我举个最常见的例子来说:
假设服务 A 每秒要发 200 个请求给服务 B,但服务 B 一秒只能处理 100 个。
这时候如果直接调用,B 很快就被压垮了。

那怎么办?
这时候我们就在中间加一层“消息队列”。

A 不再直接把请求发给 B,而是先把消息丢进队列,B 再按照自己的节奏、自己的处理能力,从队列里一点点取出来消费。
这样,A 和 B 就被队列隔离开了,互不影响。

消息队列的典型应用场景:

  • 订单系统:在电商系统中,订单的创建、支付、发货等步骤可以通过消息队列进行异步处理和解耦。
  • 日志处理:使用消息队列将日志从应用系统传输到日志处理系统,实现实时分析和监控。
  • 任务调度:在批量任务处理、任务调度系统中,通过消息队列将任务分发给多个工作节点,进行并行处理。
  • 数据同步:在数据同步系统中,消息队列可以用于将变更的数据异步同步到不同的存储系统或服务。

那消息队列到底解决了什么问题?

  1. 异步处理
  • 以前 A 发完请求得等 B 处理完才能继续,现在有了 MQ,A 发完消息直接返回。

  • 这样响应更快、系统吞吐量也更高。流量削峰(流量控制)
    在高峰期,比如大促、秒杀时,请求量会瞬间暴增。
    消息队列可以当“缓冲层”,先把请求堆起来,消费者慢慢消费,防止系统被冲垮。
    这也是很多系统抗压的关键。

  • 比如:下单之后再去发短信、发邮件这种操作,其实不需要同步完成,放 MQ 里异步处理就行。

  1. 服务解耦
  • 有了消息队列,发送方和接收方就不再强绑定。
  • A 只负责“发消息”,根本不用知道是谁来消费。
  • 将来如果要多加几个消费者,比如一个做日志,一个做分析,直接订阅这个队列就行,完全不用改 A。
  1. 流量削峰(流量控制)
  • 在高峰期,比如大促、秒杀时,请求量会瞬间暴增。
  • 消息队列可以当“缓冲层”,先把请求堆起来,消费者慢慢消费,防止系统被冲垮。
  • 这也是很多系统抗压的关键。

消息队列的核心价值是:
解耦、异步、削峰。
它让系统更快(异步),更松耦合(解耦),也更稳(削峰)。

说一下消息队列的模型有哪些?

其实常见的消息队列模型主要有两种:队列模型和发布订阅模型

队列模型(也叫点对点模型)

这个模型就像排队买咖啡。
生产者(比如点单的人)把消息放进队列,
消费者(比如咖啡师)从队列里一个个取消息来处理。

特点是:

  • 每条消息只会被一个消费者处理一次(谁先拿到谁处理)。
  • 消费完消息就从队列中删掉了。

这种模型适合“一个任务只需要一个人干”的场景,比如:
订单处理、视频转码、短信发送这种“谁处理都行”的任务分发场景。

发布 / 订阅模型(Publish / Subscribe)

这个模型就像微信公众号。
生产者(比如公众号)发一条消息到一个主题(Topic),
所有订阅这个主题的消费者(比如订阅号的用户)都会同时收到。

特点是:

  • 一条消息会被多个订阅者同时收到。
  • 特别适合广播通知、实时推送、监控告警这类场景。

比如我发布一条“系统更新通知”,只要订阅了这个 Topic 的系统都能收到消息。

消息队列主要有两种模型:
队列模型 一条消息只被一个消费者处理,适合任务分发;
发布订阅模型 一条消息可以被多个消费者收到,适合广播通知。

消息队列设计成推消息还是拉消息?推拉模式的优缺点?

消息队列有两种消费模式:推(Push) 和 拉(Pull)。

  • 推模式:Broker 主动把消息推给消费者。
  • 拉模式:消费者主动去 Broker 拉取消息。

RocketMQ 和 Kafka 其实都选择了“拉模式”,只是 RocketMQ 的“推”其实是封装了长轮询的拉模式也就是消费者在后台不断长轮询去拉消息,看起来像是 Broker 在推
所以本质上还是拉,只是体验像推。

优缺点对比一下

  • 推模式优点:消息能实时送达,延迟很低;

    • 缺点是如果消费者处理慢,Broker 一直推消息,容易把消费者压垮,尤其是高并发时不好做流控。
  • 拉模式优点:消费者自己掌握节奏,可以根据自身负载调节拉取速率,避免过载,还能做批量拉取;

    • 缺点就是实时性稍差,拉得太慢可能延迟会高一点。

为什么 RocketMQ 选拉模式

RocketMQ 和 Kafka 一样,选择拉模式主要是为了稳定可控
因为现在 MQ 都有“消息持久化”的需求——消息要先存好,再被消费。
所以用拉模式更安全,消费者按能力来拉,不会被推爆;再加上长轮询机制,也能兼顾实时性。

一句话总结:
推模式实时但容易压垮消费者,
拉模式可控但实时性略差。
RocketMQ 实际上是“伪推真拉”,底层用长轮询实现拉取,这样既能稳又能快。

RocketMQ 的事务消息有什么缺点?你还了解过别的事务消息实现吗?

先讲 RocketMQ 事务消息的优缺点,再顺带提到其他方案对比一下。

RocketMQ 事务消息的缺点

  1. 改造成本高
    RocketMQ 的事务消息需要你去改造业务逻辑,把原本的发送逻辑拆成两步(半消息 + 提交),
    还要实现一个回查接口 checkLocalTransaction,保证回查不会重复、不丢、不乱。
    对已有业务侵入性比较大。

  2. 功能受限
    它只支持单事务消息,没法处理跨多个 Topic 或多步骤的复杂分布式事务。
    简单说,RocketMQ 的事务消息更偏“轻量分布式事务”,不是真正的全局事务。

  3. 可用性问题
    如果 MQ 集群挂了,事务消息就没法继续执行。
    因为事务消息机制是“先发半消息”,Broker 不可用时半消息发不出去,后续本地事务也没法往下走。
    等于说 MQ 是事务执行的关键环节,一旦不可用整个流程都卡住。

其他事务消息方案

  1. Kafka 事务消息
    Kafka 的事务偏“生产者侧原子提交”,主要是保证“写入消息 + 提交 offset”一致,
    更适合流式计算和 Exactly Once 语义,不太像 RocketMQ 这种业务型事务。

  2. 本地消息表(Outbox Pattern)
    很多公司其实用这个方案。
    就是业务操作和消息写入放在同一个本地事务里,然后异步任务去扫消息表发 MQ。
    好处是实现简单、无 MQ 依赖,坏处是延迟略高、需要额外表和任务管理。

  3. Seata 的 AT 模式 / TCC 模式
    这类属于真正的分布式事务框架,强调强一致。
    优点是自动补偿,缺点是侵入性更强,对性能影响更大。

小结
RocketMQ 的事务消息是通过“半消息 + 本地事务 + 回查”实现最终一致性的,但也有三个主要缺点:
一是改造成本高,要改业务逻辑并实现回查接口;
二是功能有限,只支持单事务,不适合跨 Topic;
三是依赖 MQ 可用性,Broker 挂了事务就卡住。
其他常见方案像 本地消息表 更简单易用,Seata 强一致但性能开销大。
所以 RocketMQ 的事务消息更适合“高可用、最终一致”的业务场景。

为什么 RocketMQ 不使用 Zookeeper 作为注册中心呢?而选择自己实现 NameServer?

RocketMQ 为什么要自主设计一个 NameServer,而不用其他现成的注册中心 zookeeper,nacos?

答:这个问题直击 RocketMQ 的设计核心,因为 RocketMQ 的核心是金融,电商这类非常灵活的业务场景服务的,这使得 RocketMQ 的整个设计思想和 Kafka 这样追求极致的吞吐的消息中间件有根本的区别,在保证高性能的同时,对于服务的可靠性也要做到极致。

  • 一方面 NameServer 采用一种极为轻量级的集群方案,每个 NameServer 节点之间不需要和集群当中的其他节点发生任何的数据交互,这样可以保证 NameServer 集群当中只要有任何一个节点正常工作,那么整个 NameServer 集群就能够保持正常。
  • 另一方面,业务的频繁更迭,使得 RocketMQ 也需要及时进行升级,自主研发的 NameServer 可以更灵活的应对新的业务场景

消息队列如何处理重复消息(保证消息的幂等性)?

首先,在消息队列里重复消息是一定会发生的。无论是 MQ 重投、网络抖动、Broker 写入成功但生产者没收到 ACK,或者消费者消费成功但没来得及提交 offset,都会导致同一条消息被多次投递。所以我们不能寄希望于“消息只来一次”,真正可靠的做法是让消费者的处理逻辑具有幂等性

1. 核心思路:让业务逻辑幂等(处理多次结果一样)

① 基于唯一幂等号(业务 ID / 消息 ID)防重复

给消息里加一个唯一 ID(比如订单号、业务流水号、UUID)。
消费者在处理前先检查这个 ID 是否处理过

  • 处理过 → 直接跳过
  • 没处理过 → 执行并记录这个 ID

可以把这个记录放在:

  • Redis(高并发读写)
  • 数据库(唯一索引、insert ignore)
  • 业务表中的状态字段(如“已支付”、“已发货”)

② 利用数据库的唯一约束实现幂等

例如 MySQL 使用:

1
insert into ... on duplicate key update ...

或者条件判断:

1
update t set money = 150 where id = 1 and money = 100;

无论执行业务多少次,结果相同。

2. 为什么不能从源头完全避免重复消息?

  • 生产者 → Broker 的确认可能丢
  • Broker 已写入但 ACK 丢了 → 生产者重发
  • 消费者处理成功但 offset 未提交 → 新的消费者会再次消费
    因此重复消息是 无法彻底避免的只能在消费端处理幂等

小结

在 MQ 里,重复消息是无法彻底避免的,比如生产者重发、ACK 丢失、消费者 offset 未提交等。所以我们不依赖“消息只来一次”,而是让消费端具备幂等性

常用的幂等处理方式有三种:

  1. 唯一业务 ID 防重(最常用)
    消息里带一个唯一的幂等号,比如订单号、流水号。消费者处理前先查这个 ID 是否处理过,处理过就跳过。

  2. Redis 防重(SETNX)
    收到消息先执行 SETNX,成功才继续处理,失败说明重复消费。

  3. 数据库幂等(最终兜底)

    • 条件更新:update ... where status = 'UNPAID'
    • 唯一键约束:insert ... on duplicate key update
      保证重复执行不会影响最终结果。

一句话总结:

重复消息不可避免,但重复消费可控。核心是:唯一业务 ID + Redis 去重 + DB 幂等更新,执行多次结果保持一致。

举个例子 订单重复支付幂等处理

在我们的支付项目里,支付成功后会把消息投到 MQ,因为回调重试、Broker ACK 丢失、offset 未提交等原因,消息可能会被多次投递。为了避免重复扣库存、重复更新订单,我们做了完整的幂等设计。

核心做法有三点:

  1. 消息中携带唯一业务 ID(paymentId)
    每条支付消息都有唯一流水号,用于判断是否处理过。

  2. Redis 防重(SETNX)
    消费端先用 Redis 做一次快速幂等校验,SETNX 成功才继续处理。

  3. 数据库层幂等

    • 订单更新使用条件更新:where pay_status = 'UNPAID',只会从未支付 → 已支付一次
    • 支付记录表用唯一索引 paymentId,重复 insert 不会生效

最终效果:
无论 MQ 消息来几次,订单只更新一次,库存只扣一次,实现真正的幂等性。

消息队列如何保证消息的有序性(顺序性)?

MQ 想保证消息顺序,核心思想其实只有一句话:
让同一类相关的消息进入同一个队列,并让这个队列只被一个消费者、一个线程顺序处理。

下面按常见的 3 种方式总结:

① 单生产者 + 单消费者 + 单队列(最简单)

只要把消息全部发到一个队列,让消费者单线程处理,就天然有序。
缺点也很明显:性能瓶颈严重,不适合高并发场景

② 使用分区 + 分区键(Partition Key)——Kafka / RocketMQ 最常用

做法:

  • 设置一个 顺序键(如订单号、用户 ID)
  • 用这个键做 hash 路由,让同一类业务消息始终进入同一个分区 / 队列
    例如:订单号相同的消息全部进入 Queue #3

这样每个分区内部天然是顺序的,又能通过多个分区 提高并发能力

③ 使用顺序队列(Ordered Queue)机制

像 RabbitMQ、某些 MQ 提供 原生顺序队列

  • 存储顺序 = 投递顺序 = 消费顺序
  • 但要求“同一顺序队列只能配一个消费者”,吞吐量有限

RocketMQ 的顺序消息(补充)

RocketMQ 的顺序消息除了分区路由外,还需要:

  • MessageQueueSelector 控制路由
  • MessageListenerOrderly 顺序消费模式
  • 同时在消费端 三层加锁(Broker Queue 锁、MessageQueue 锁、ProcessQueue 锁)来确保只有一个线程顺序处理队列消息

小结

MQ 保证顺序的核心就是:让同一类消息进入同一个队列,并由一个消费者顺序处理

常见做法有三种:

  1. 单队列 + 单消费者
    所有消息进一个队列,单线程消费,天然有序,但吞吐量低。

  2. 分区 + 分区键(最常用,如 Kafka、RocketMQ)
    通过 Partition Key(如订单号) 做路由,让同一业务的消息都落到同一个分区;分区内部天然有序,还能利用多分区并发。

  3. 使用顺序队列(一些 MQ 原生支持)
    顺序队列保证写入顺序 = 投递顺序 = 消费顺序。

一句话总结:

顺序性 = 相同消息路由到同一队列 + 单线程顺序消费。

消息队列如何处理消息堆积?

消息堆积本质上是 生产速度 > 消费速度,导致大量消息积在队列里。所以解决思路就是提升消费能力、降低生产速度,或者清理无效消息。

可以从以下几方面来处理:

① 增加消费者的并发能力(最有效)

  • 增加消费者线程数 → 提高单实例并发度
  • 增加消费者实例数 → 多机器一起消费,提高整体吞吐

② 优化消费者消费逻辑

  • 减少单条消息的处理时延
  • 提高数据库批量写效率、减少 I/O
  • 引入线程池异步处理

③ 降低生产者速度(如果可控)

  • 对生产端限流
  • 消息分级,只优先处理关键消息

④ 清理过期或无效消息

如果某些消息已无业务价值,可以评估后丢弃,快速消化堆积。

⑤ 调整 MQ 配置 / 增加队列数

  • 增加 Topic 的队列数量,提高并行度
  • 调整 MQ 消费模式、拉取频率等配置

小结

消息堆积主要是因为消费速度跟不上生产速度,所以解决方式就是提高消费能力或降低生产速度。

常见处理办法 3 步:

  1. 扩大消费能力(最有效)

    • 增加消费者实例数
    • 增加消费者线程
    • 增加 Topic 队列数,提高并行度
  2. 优化消费逻辑

    • 减少单条消息处理时间
    • 批量处理、减少 I/O、引入线程池
  3. 必要时限流或丢弃无效消息

    • 控制生产端速度
    • 清理过期/失败无意义的消息

一句话总结:

核心就是提升消费能力,加并发、加实例、提高效率;如果不行,就限流生产或清理无效消息。