Skip to content

第 3 讲:消息队列——削峰填谷与系统解耦的利器

如果说缓存解决的是读的问题,那消息队列解决的就是写的问题


一、消息队列解决什么问题?

1. 三大核心作用

作用1:异步处理

没有消息队列:

用户下单 -> 扣库存 -> 创建订单 -> 发短信 -> 发邮件 -> 加积分 -> 返回
总耗时:50 + 50 + 50 + 100 + 100 + 50 = 400ms

有消息队列:

用户下单 -> 扣库存 -> 创建订单 -> 发消息 -> 返回
总耗时:50 + 50 + 10 = 110ms

消息队列异步处理:
  -> 发短信
  -> 发邮件
  -> 加积分

效果: 响应时间从 400ms 降到 110ms,用户体验大幅提升


作用2:流量削峰

没有消息队列:

秒杀开始 -> 瞬间100万请求 -> 直接打到数据库 -> 数据库扛不住 -> 系统崩溃

有消息队列:

秒杀开始 -> 瞬间100万请求 -> 进入消息队列(缓冲)
-> 消费者按自己的速度消费(每秒5000)-> 数据库平稳处理

类比:

没有消息队列 = 洪水直接冲城市
有消息队列  = 洪水先进水库,再缓慢放水

作用3:系统解耦

没有消息队列:

python
# 订单服务直接调用多个下游 -- 强耦合
def create_order(order: dict) -> None:
    order_dao.insert(order)
    inventory_service.deduct(order)    # 调库存
    sms_service.send(order)            # 发短信
    email_service.send(order)          # 发邮件
    point_service.add(order)           # 加积分
    # 新增一个下游就要改代码...

问题: 强耦合、一个下游挂了整个失败、新增下游要改代码、响应时间叠加

有消息队列:

python
# 订单服务只发消息
def create_order(order: dict) -> None:
    order_dao.insert(order)
    mq.send("order.created", order)  # 发一条消息就够了

# 各下游独立消费
def on_order_created_sms(order: dict) -> None:
    sms_service.send(order)

def on_order_created_email(order: dict) -> None:
    email_service.send(order)

def on_order_created_point(order: dict) -> None:
    point_service.add(order)

# 分别注册消费者
mq.subscribe("order.created", on_order_created_sms, group="sms-group")
mq.subscribe("order.created", on_order_created_email, group="email-group")
mq.subscribe("order.created", on_order_created_point, group="point-group")

好处: 不关心多少下游、新增只需加消费者、互不影响、独立扩缩容


2. 什么时候不该用消息队列?

不适合的场景原因
需要同步结果如查询用户余额,必须立即返回
逻辑简单、调用少加MQ反而增加复杂度
对一致性要求极高转账必须同一事务,不适合异步

原则: 核心链路同步,非核心链路异步,有明确削峰/解耦需求才引入


二、消息队列核心概念

1. 基本模型

[生产者 Producer] -> 发送消息 -> [消息队列 Broker] -> 拉取/推送 -> [消费者 Consumer]

2. 两种消息模型

点对点(Queue)

[Producer] -> [Queue] -> [Consumer]

一条消息只能被一个消费者消费,消费后从队列删除。

发布/订阅(Pub/Sub)

[Producer] -> [Topic] -> [Consumer Group A]
                     -> [Consumer Group B]
                     -> [Consumer Group C]

一条消息可以被多个消费者组消费,每个消费者组独立消费。Kafka和RocketMQ都采用此模型。


3. 核心术语

术语含义
Topic消息主题,逻辑分类
Partition/Queue分区,物理存储单元
Producer消息生产者
Consumer消息消费者
Consumer Group消费者组,组内分摊消费
Offset消费位移,记录消费到哪了
Broker消息服务器节点

三、Kafka 核心原理

1. Kafka 整体架构

[Producer1] [Producer2]
     |           |
[Kafka Cluster]
  [Broker1]  [Broker2]  [Broker3]
  - Topic A    - Topic A    - Topic A
    Partition0   Partition1   Partition2
     |           |           |
[Consumer Group]
  [Consumer1] [Consumer2] [Consumer3]
  消费P0       消费P1       消费P2

2. Topic 与 Partition

Topic: 逻辑概念,一类消息的集合(如 order-topic、payment-topic)

Partition: 物理概念,一个Topic分成多个Partition,每个Partition是一个有序的、不可变的消息序列

为什么要分Partition?

  • 并行消费:多个消费者并行处理
  • 水平扩展:分布在不同Broker上
  • 吞吐量提升:单Partition写入可达100MB/s
Topic: order-topic
  Partition 0: [msg0, msg1, msg2, msg3, ...]  -> Broker1
  Partition 1: [msg4, msg5, msg6, msg7, ...]  -> Broker2
  Partition 2: [msg8, msg9, msg10, msg11, ...] -> Broker3

3. 消息路由:消息发到哪个Partition?

方式做法特点
指定Partition直接指定Partition编号精确控制
按Key哈希相同Key -> 同一Partition保证同Key有序
轮询(默认)依次发到各Partition最均匀
python
# 按Key哈希(最常用)
producer.send("order-topic", key=order_id, value=json.dumps(data))
# 相同order_id的消息会发到同一个Partition

4. Consumer Group

核心规则:同一个Consumer Group内,一个Partition只能被一个Consumer消费

Topic: order-topic (3个Partition)

Consumer Group A (3个消费者):
  Consumer1 -> Partition0, Consumer2 -> Partition1, Consumer3 -> Partition2
  每个消费者处理一个Partition

Consumer Group A (2个消费者):
  Consumer1 -> Partition0 + Partition1, Consumer2 -> Partition2
  消费者不够,一个消费者处理多个Partition

Consumer Group A (4个消费者):
  Consumer1 -> Partition0, Consumer2 -> Partition1, Consumer3 -> Partition2, Consumer4 -> 闲置
  消费者多于Partition数量,多出来的闲置

结论: 消费者数量 > Partition数量 -> 有消费者闲置

不同Consumer Group独立消费:

Consumer Group A (订单服务) -> 消费所有消息
Consumer Group B (积分服务) -> 消费所有消息
Consumer Group C (通知服务) -> 消费所有消息

5. Kafka 存储原理

日志存储:

Partition0/
  00000000000000000000.log      # 第1个日志段
  00000000000000000000.index    # 稀疏索引
  00000000000000000000.timeindex  # 时间索引
  00000000000000065536.log      # 第2个日志段
  ...

为什么Kafka吞吐量高?

原因说明
顺序写磁盘追加写而非随机写,SSD上可达 500-1000 MB/s
零拷贝sendfile 系统调用,4次拷贝+4次上下文切换 -> 2次拷贝+2次切换
批量+压缩batch_size=16KB + linger_ms=5 + LZ4压缩
Partition并行8个Partition = 8路并行写入,吞吐量翻8倍

6. Kafka 副本机制

Partition0:
  Leader: Broker1    (读写都走Leader)
  Follower: Broker2  (同步副本)
  Follower: Broker3  (同步副本)

ISR(In-Sync Replicas): 和Leader保持同步的副本集合,只有ISR中的副本才有资格被选为新Leader

ISR = {Leader(Broker1), Follower(Broker2), Follower(Broker3)}

如果Broker3同步太慢:
ISR = {Leader(Broker1), Follower(Broker2)}  # Broker3被踢出ISR

如果Broker1宕机:
从ISR中选新Leader -> Broker2成为新Leader

四、RocketMQ 核心原理

1. RocketMQ 整体架构

[Producer]
    |
[NameServer集群] <- 注册中心(无状态,互不通信)
    |
[Broker集群]
  Master-A <--> Slave-A
  Master-B <--> Slave-B
    |
[Consumer]

和Kafka的关键区别: Kafka依赖ZooKeeper(新版本用KRaft),RocketMQ依赖NameServer(更轻量)


2. RocketMQ 特色功能

事务消息

场景: 下单时需要同时扣库存,且两边要一致。

1. 发送半消息(Half Message)-> Broker暂存,不投递
2. 执行本地事务(扣库存)
3a. 本地事务成功 -> 提交半消息 -> 消费者可见
3b. 本地事务失败 -> 回滚半消息 -> 消费者不可见
4. 如果Producer宕机 -> Broker回查本地事务状态
python
# 事务消息流程(Python实现思路)
def send_transaction_message(order: dict) -> bool:
    """发送事务消息"""
    # 1. 发送半消息
    msg = Message(topic="order-topic", body=json.dumps(order))
    result = producer.send_message_in_transaction(msg)

    # 2. 执行本地事务
    try:
        order_dao.insert(order)
        inventory_dao.deduct(order["product_id"], order["quantity"])
        # 3a. 成功 -> 提交
        producer.commit(result.transaction_id)
        return True
    except Exception:
        # 3b. 失败 -> 回滚
        producer.rollback(result.transaction_id)
        return False

RocketMQ 的 Python 客户端(rocketmq-client-python)支持事务消息,核心 API 与 Java 版类似。


延迟消息

场景: 订单30分钟未支付自动取消。

python
# 发送延迟消息
msg = Message(
    topic="order-cancel-topic",
    body=json.dumps(order).encode()
)
# RocketMQ 延迟级别:1s 5s 10s 30s 1m 2m ... 30m 1h 2h
msg.set_delay_time_level(16)  # 30分钟
producer.send(msg)
python
# 消费者在30分钟后收到消息
def on_order_cancel(msg: dict) -> None:
    order = json.loads(msg.body)
    if order["pay_status"] == "UNPAID":
        order_service.cancel(order["order_no"])

顺序消息

场景: 订单状态变更:创建 -> 支付 -> 发货 -> 完成,必须有序。

python
# 发送顺序消息:相同order_id发到同一个Queue
def send_ordered(order_id: str, msg_body: dict) -> None:
    """根据order_id哈希选择固定Queue,确保同订单有序"""
    msg = Message(
        topic="order-topic",
        body=json.dumps(msg_body).encode(),
        keys=order_id       # 相同Key -> 同一Queue
    )
    producer.send(msg)

# 顺序消费(单线程处理,保证同一个Queue内有序)
def consume_ordered(messages: list[Message]) -> None:
    for msg in messages:
        process_order(json.loads(msg.body))

五、Kafka vs RocketMQ 怎么选?

维度KafkaRocketMQ
吞吐量极高(百万级)高(十万级)
事务消息不原生支持原生支持
延迟消息不原生支持原生支持
顺序消息Partition级有序Queue级有序
消息过滤不支持Tag/SQL过滤
运维复杂度中(依赖ZK/KRaft)中(依赖NameServer)
社区生态全球最活跃国内活跃
适用场景大数据/日志/流处理业务消息/电商/金融

一句话总结: 大数据选Kafka,业务消息选RocketMQ


六、消息可靠性:如何保证消息不丢失?

消息丢失的三个环节

[Producer] -> 发送阶段 -> [Broker] -> 存储阶段 -> [Consumer] -> 消费阶段
     ^                       ^                        ^
   可能丢                  可能丢                    可能丢

1. 生产端:发送不丢

同步发送 + 确认

python
from kafka import KafkaProducer

# Kafka -- 同步发送 + 确认
producer = KafkaProducer(
    bootstrap_servers=['localhost:9092'],
    acks='all',                # 所有ISR副本确认
    retries=3,                 # 重试3次
    retry_backoff_ms=100,      # 重试间隔100ms
    value_serializer=lambda v: json.dumps(v).encode()
)
future = producer.send('order-topic', value=order_data)
metadata = future.get(timeout=10)  # 同步等待确认

acks参数:

acks=0  -> 不等确认,最快但可能丢
acks=1  -> Leader确认,Leader挂了可能丢
acks=all -> 所有ISR确认,最安全
python
# RocketMQ
result = producer.send_sync(msg)
if result.send_status == SendStatus.SEND_OK:
    logger.info("发送成功")
else:
    retry_producer.send(msg)  # 失败重试

本地消息表(最可靠)

python
from contextlib import contextmanager
from apscheduler.schedulers.background import BackgroundScheduler

@contextmanager
def transaction():
    """事务上下文"""
    session = db_session()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise


def create_order(order: dict) -> None:
    """下单 -- 业务 + 本地消息在同一事务"""
    with transaction() as session:
        order_dao.insert(session, order)

        # 插入本地消息表
        local_msg = {
            "topic": "order-created",
            "body": json.dumps(order),
            "status": "INIT",
            "retry_count": 0,
        }
        local_message_dao.insert(session, local_msg)


# 后台线程定时扫描,发送消息
def send_pending_messages() -> None:
    messages = local_message_dao.find_by_status("INIT")
    for msg in messages:
        try:
            mq.send(msg["topic"], msg["body"])
            msg["status"] = "SENT"
            local_message_dao.update(msg)
        except Exception:
            msg["retry_count"] += 1
            local_message_dao.update(msg)


scheduler = BackgroundScheduler()
scheduler.add_job(send_pending_messages, 'interval', seconds=1)
scheduler.start()
sql
CREATE TABLE `local_message` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `topic` VARCHAR(128) NOT NULL,
    `body` TEXT NOT NULL,
    `status` VARCHAR(16) NOT NULL DEFAULT 'INIT',
    `retry_count` INT NOT NULL DEFAULT 0,
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_status` (`status`)
) ENGINE=InnoDB;

2. Broker端:存储不丢

Kafka:

properties
default.replication.factor=3       # 副本数 >= 2
min.insync.replicas=2              # 最小ISR数
unclean.leader.election.enable=false  # 不允许从非ISR选Leader

RocketMQ:

properties
flushDiskType=SYNC_FLUSH    # 同步刷盘(可靠)
brokerRole=SYNC_MASTER      # 同步复制(可靠)
可靠性要求刷盘复制
最高同步刷盘同步复制
折中异步刷盘同步复制
高性能异步刷盘异步复制

3. 消费端:消费不丢

核心: 关闭自动提交,业务处理成功后手动提交

python
from kafka import KafkaConsumer

consumer = KafkaConsumer(
    'order-topic',
    bootstrap_servers=['localhost:9092'],
    enable_auto_commit=False,    # 关闭自动提交
    group_id='order-group',
    value_deserializer=lambda v: json.loads(v)
)

for message in consumer:
    try:
        process_message(message.value)
        consumer.commit()        # 手动提交
    except Exception as e:
        logger.error(f"处理失败: {e}")
        # 不提交offset,下次重新消费
python
# RocketMQ 消费端
def consume_message(msgs: list[Message]) -> ConsumeStatus:
    try:
        for msg in msgs:
            process_message(msg)
        return ConsumeStatus.SUCCESS
    except Exception as e:
        logger.error(f"消费失败: {e}")
        return ConsumeStatus.RECONSUME_LATER  # 稍后重试

完整的消息不丢方案总结

生产端:
  1. 同步发送 + acks=all(Kafka)
  2. 发送失败重试
  3. 极端场景用本地消息表

Broker端:
  4. 多副本
  5. 同步刷盘(或至少异步刷盘 + 同步复制)

消费端:
  6. 关闭自动提交
  7. 业务处理成功后手动提交
  8. 消费失败重试(死信队列兜底)

七、消息重复:如何做幂等?

为什么会有重复消息?

场景原因
生产者重试发送成功但ACK超时,Producer重试
消费者重试处理成功但提交offset失败
RebalanceConsumer Group重新分配,可能重复消费

结论: 消息重复几乎不可避免,核心是做幂等


幂等性设计

方案1:唯一ID + 去重表(最通用)

sql
CREATE TABLE `message_dedup` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `msg_id` VARCHAR(128) NOT NULL COMMENT '消息唯一ID',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_msg_id` (`msg_id`)
) ENGINE=InnoDB;
python
def on_message(msg: dict) -> None:
    msg_id = msg["msg_id"]

    try:
        message_dedup_dao.insert(msg_id)
    except DuplicateKeyError:
        logger.warning(f"消息已处理: {msg_id}")
        return

    # 业务处理
    order_service.create_order(msg)

方案2:业务状态判断

python
def deduct_stock(product_id: int, quantity: int, order_id: str) -> None:
    order = order_dao.get_by_order_id(order_id)

    if order["status"] == "PAID":
        logger.warning("订单已支付,跳过")
        return

    affected = inventory_dao.deduct(product_id, quantity, order["version"])
    if affected == 0:
        logger.warning("并发冲突,跳过")

方案3:Redis Set去重

python
def on_message(msg: dict) -> None:
    msg_id = msg["msg_id"]

    # SETNX: Key不存在才设置,存在则返回False
    is_new = redis.set(f"msg:dedup:{msg_id}", "1", nx=True, ex=86400)

    if not is_new:
        return  # 已处理

    process_order(msg)

风险: Redis和DB不在同一事务,Redis宕机可能丢去重记录


方案4:数据库唯一约束

python
def create_order(order: dict) -> None:
    try:
        order_dao.insert(order)  # 订单号唯一约束
    except IntegrityError:
        logger.warning(f"订单已存在: {order['order_no']}")

幂等方案对比

方案可靠性性能复杂度适用场景
唯一ID+去重表最高通用
业务状态判断有明确状态的业务
Redis去重最高允许极小概率重复
数据库唯一约束最高有唯一业务键

八、消息顺序性

为什么会乱序?

多Partition并行消费时,不同Partition的消费速度不同,可能导致消息乱序。


解决方案

方案1:单Partition(全局有序)

bash
kafka-topics.sh --create --topic order-topic --partitions 1

缺点: 吞吐量受限,只能一个消费者


方案2:按Key分区(局部有序,推荐)

相同业务Key的消息发到同一个Partition:

python
producer.send("order-topic", key=order_id, value=json.dumps(order))
orderId=1001 -> hash(1001) % 3 = Partition1 -> [创建, 支付, 发货] 按顺序
orderId=1002 -> hash(1002) % 3 = Partition0 -> [创建, 支付, 发货] 按顺序

效果: 同一订单内有序,不同订单间并行,吞吐量高


方案3:消费端排序

python
def on_message(msg: dict) -> None:
    order_id = msg["order_id"]
    seq = msg["sequence"]
    last_seq = redis.get(f"order:seq:{order_id}") or 0

    if seq <= last_seq:
        return  # 已处理

    if seq > last_seq + 1:
        raise RetryLaterError()  # 乱序,放回队列稍后重试

    process_order(msg)
    redis.set(f"order:seq:{order_id}", seq)

九、消息积压怎么处理?

积压的常见原因

原因表现
消费者处理慢慢SQL、接口超时、Bug导致重试
消费者挂了宕机、重启来不及消费
流量突增活动期间流量暴涨
消费者数量不足Partition多但消费者少

紧急方案

方案1:增加消费者实例

原本:2个消费者 <- 8个Partition
现在:8个消费者 <- 8个Partition
消费速度翻4倍

注意: 消费者数量不能超过Partition数量


方案2:消费者内部线程池

python
from concurrent.futures import ThreadPoolExecutor, as_completed

executor = ThreadPoolExecutor(max_workers=20)

def on_message(msgs: list[dict]) -> None:
    futures = {executor.submit(process_message, msg): msg for msg in msgs}
    for future in as_completed(futures, timeout=30):
        future.result()  # 等待所有完成,有异常会抛出

注意: 对顺序有要求不能用多线程,需要保证幂等


方案3:跳过非核心消息(降级)

python
def on_message(msg: dict) -> None:
    if emergency_mode and not is_critical_message(msg):
        return  # 紧急模式跳过非核心
    process_message(msg)

方案4:临时队列转储

1. 新建临时Topic(Partition多)
2. 简单消费者把积压消息搬到临时Topic
3. 部署大量消费者消费临时Topic
4. 积压清零后恢复正常
python
# 转储:只搬运不处理
def transfer_message(msg: dict) -> None:
    producer.send("order-topic-temp", json.dumps(msg))

# 大量消费者消费临时Topic
def process_message(msg: dict) -> None:
    order_service.process(msg)

预防方案

措施做法
监控告警Consumer Lag > 10000 时告警
合理Partition数预估QPS / 单消费者处理能力,留余量
消费端优化批量处理、异步化、减少IO

十、死信队列

什么是死信队列?

消费失败达到最大重试次数后,消息进入死信队列(Dead Letter Queue)。

消费失败 -> 重试1s -> 重试5s -> 重试30s -> ... -> 重试2h -> 还是失败 -> 死信队列

RocketMQ 死信队列

# 死信Topic命名:%DLQ% + 消费者组名
# 原始Topic: order-topic, 消费者组: order-group
# 死信Topic: %DLQ%order-group
python
def on_dead_letter(msg: dict) -> None:
    logger.error(f"死信消息: {msg}")
    alert_service.notify_admin("死信消息", msg)
    dead_letter_dao.insert(msg)

Kafka 死信队列

Kafka没有原生死信队列,需要自己实现:

python
MAX_RETRY = 3

def on_message(message) -> None:
    retry_count = get_retry_count(message)

    try:
        process_message(message)
        consumer.commit()
    except Exception as e:
        if retry_count >= MAX_RETRY:
            # 达到最大重试 -> 进入死信Topic
            producer.send("order-topic-dlq", key=message.key, value=message.value)
            consumer.commit()  # 死信队列已接收,确认消费
        else:
            # 未达到最大重试 -> 发到重试Topic
            producer.send("order-topic-retry",
                key=message.key,
                value=add_retry_header(message.value, retry_count + 1))
            consumer.commit()

十一、消息队列在秒杀系统中的实战

完整秒杀流程

[前端]
  | 秒杀请求
[Nginx] <- 限流(10万QPS -> 放过1万)
  |
[API网关] <- 鉴权、限流
  |
[秒杀服务]
  | 1. Redis预扣库存(Lua原子性)
  | 2. 库存不足 -> 直接返回"已抢完"
  | 3. 库存充足 -> 发送消息到MQ
  | 4. 立即返回"排队中"
  |
[消息队列] <- 削峰缓冲
  |
[订单服务] <- 消费消息
  | 1. 创建订单
  | 2. 扣减DB库存(乐观锁)
  | 3. 更新订单状态
  |
[用户轮询/WebSocket查结果]

秒杀接口代码

python
from fastapi import FastAPI
from redis import Redis

app = FastAPI()
redis = Redis()

@app.post("/seckill")
async def seckill(request: SeckillRequest):
    user_id = request.user_id
    activity_id = request.activity_id

    # 1. 防重复
    dedup_key = f"seckill:dedup:{activity_id}:{user_id}"
    if not redis.set(dedup_key, "1", nx=True, ex=3600):
        return {"code": 400, "message": "不能重复秒杀"}

    # 2. Redis预扣库存
    script = """
    local stock = redis.call('get', KEYS[1])
    if tonumber(stock) > 0 then
        redis.call('decr', KEYS[1])
        return 1
    else
        return 0
    end
    """
    result = redis.eval(script, 1, f"seckill:stock:{activity_id}")

    if result == 0:
        redis.delete(dedup_key)
        return {"code": 400, "message": "已抢完"}

    # 3. 发送消息到MQ
    msg = {"user_id": user_id, "activity_id": activity_id}
    producer.send("seckill-topic", json.dumps(msg))

    return {"code": 200, "message": "排队中,请稍后查询结果"}

订单消费者代码

python
def on_seckill_message(msg: dict) -> None:
    try:
        user_id = msg["user_id"]
        activity_id = msg["activity_id"]

        # 1. 幂等检查
        if order_dao.exists_by_user_and_activity(user_id, activity_id):
            return

        # 2. 扣减DB库存(乐观锁)
        affected = activity_dao.deduct_stock(activity_id)
        if affected == 0:
            notify_user(user_id, "秒杀失败")
            return

        # 3. 创建订单
        order = {
            "order_no": generate_order_no(),
            "user_id": user_id,
            "activity_id": activity_id,
            "status": "CREATED",
        }
        order_dao.insert(order)

        notify_user(user_id, f"秒杀成功,订单号:{order['order_no']}")

        # 4. 发送延迟消息(30分钟未支付自动取消)
        cancel_msg = Message(
            topic="order-cancel-topic",
            body=json.dumps(order).encode()
        )
        cancel_msg.set_delay_time_level(16)  # 30分钟
        producer.send(cancel_msg)

    except Exception as e:
        raise  # 抛异常让MQ重试

超时取消消费者

python
def on_order_cancel(msg: dict) -> None:
    order = order_dao.get_by_order_no(msg["order_no"])

    if order["status"] == "CREATED":
        order_dao.update_status(msg["order_no"], "CANCELLED")
        activity_dao.increase_stock(msg["activity_id"])
        redis.incr(f"seckill:stock:{msg['activity_id']}")

十二、面试高频题

1. 消息队列有什么用?

三大核心作用:异步处理(提升响应速度)、流量削峰(缓冲高峰流量)、系统解耦(降低依赖)


2. Kafka 和 RocketMQ 怎么选?

大数据/日志/流处理选Kafka,业务消息/电商/金融选RocketMQ。需要事务消息和延迟消息选RocketMQ,超高吞吐量选Kafka。


3. 如何保证消息不丢失?

三个环节:生产端同步发送+acks=all+重试+本地消息表;Broker端多副本+同步刷盘/同步复制;消费端关闭自动提交+业务处理成功后手动提交。


4. 消息重复了怎么办?

消息重复不可避免,核心是做幂等:唯一ID+去重表(最通用)、业务状态判断、数据库唯一约束、Redis去重。


5. 如何保证消息顺序?

全局有序:单Partition,性能差;局部有序(推荐):相同Key发到同一Partition,同一订单内有序,不同订单间并行。


6. 消息积压了怎么办?

紧急:增加消费者数量、消费者内部多线程、降级跳过非核心消息、临时队列转储。预防:监控Consumer Lag、合理设置Partition数量、消费端性能优化。


7. 事务消息的原理?

1.发送半消息(暂不投递)-> 2.执行本地事务 -> 3a.成功则提交半消息 / 3b.失败则回滚 -> 4.Producer宕机则Broker定时回查本地事务状态


8. Kafka 为什么吞吐量高?

四个原因:顺序写磁盘(比随机写快5-10倍)、零拷贝(减少数据拷贝和上下文切换)、批量+压缩(减少网络传输)、Partition并行(多路并行读写)


9. Kafka 的 ISR 机制是什么?

ISR = In-Sync Replicas,与Leader保持同步的副本集合。副本落后太多被踢出ISR,Leader宕机只从ISR中选新Leader,unclean.leader.election.enable=false保证数据不丢。


10. 死信队列是什么?什么时候用?

消费失败达到最大重试次数后,消息进入死信队列。用于人工排查消费失败原因、后续补偿处理、避免失败消息阻塞正常消费。


十三、核心结论

  1. 消息队列三大作用:异步、削峰、解耦
  2. 大数据选Kafka,业务消息选RocketMQ
  3. 消息不丢:生产端确认 + Broker持久化+多副本 + 消费端手动提交
  4. 消息重复不可避免,核心是做幂等
  5. 消息顺序:相同Key发到同一Partition
  6. 消息积压紧急处理:加消费者 + 多线程 + 降级
  7. 事务消息:半消息 + 本地事务 + 回查
  8. 死信队列是消费失败的兜底方案

十四、练习题

练习1:方案设计

设计一个订单系统的消息方案:

  • 下单后需要:扣库存、发短信、发邮件、加积分
  • 要求:下单接口 < 200ms
  • 要求:消息不能丢

要求:画出架构图,说明哪些是同步/异步,说明消息不丢的保证


练习2:幂等设计

场景:消费者收到"扣减库存"消息,可能重复收到。商品ID: 1001,扣减数量: 1

要求:设计幂等方案,写出消费代码,说明能覆盖哪些重复场景


练习3:积压处理

场景:Topic有8个Partition,当前2个消费者,积压100万条消息,每条处理需要50ms

要求:估算消费完需要多长时间,制定紧急处理方案和预防方案


练习4:思考题

为什么不建议用消息队列来实现"查询用户余额"这个功能?

基于 VitePress 构建