第7章:RabbitMQ Queue 声明、持久化与 Classic Queue
2026/9/9 21:50:45 网站建设 项目流程

1. 项目背景

第 6 章把支付消息送进了q.order.pay。预发第一次重启 Broker后,测试发现队列还在,里面的「未消费支付成功」却没了。事故会立刻变成:财务要对账,MQ 里空空如也。

现场对「持久化」有三种说法,全错了一半:

  • 开发:队列勾了 durable,消息就一定还在。
  • 运维:磁盘没满,消息就安全。
  • 测试:重启容器等于重启应用,内存队列也可以。
queue.declare durable=true ← 队列定义能否熬过节点重启 basic.publish delivery_mode=2 ← 这条消息是否要求落盘 x-queue-type=classic ← 用哪套存储引擎(本章默认经典队列 CQv2)

三件套缺一,重启后就会「定义在、货不在」或「货在、定义丢了绑定全断」。

4.3 还加了一刀:默认拒绝非持久且非 exclusive 的队列。老教程里的auto_delete临时队列一 declare 就 406,CI 全红,有人准备在 conf 里打开deprecated_features.permit.transient_nonexcl_queues——第 3 章已禁止预发这么做。

本章只打经典队列。仲裁与 Stream 是第 18–20 章的选型,不要在支付主路径上「先用 classic 顶着」而不写进风险清单。

测试环境还出现过:有人用 Management UI「Purge」把对照队列清空再重启,然后宣布「durable 无效」。实验纪律必须写进用例:重启前禁止 Purge、禁止 Get、禁止 down -v。否则持久化结论全部作废。运维侧则要确认 compose 的 named volume 还在,没有把数据目录指到容器可写层。

另一类误解是把auto_delete当成「消息 TTL」。最后一个消费者走了队列被删,里面未消费的持久消息一起没——这比非持久消息还冤,因为定义层先没了。支付队列禁止 auto_delete。


2. 项目设计

小胖把快递柜拍照发到群里。

小胖:队列不就是快递柜格子吗?格子写上名字,东西放进去。为啥还分耐不耐摔、锁不锁门、人走柜是不是拆掉?食堂的餐盘架也没这么多开关。

大师:格子本身会不会被物业连夜拆走,是durable。格子里的包裹贴没贴「贵重」要入库,是delivery_mode。这趟只给你临时用、人一走格子消失,是exclusive。没人订餐了自动收摊,是auto-delete。四个开关组合出完全不同的命运,混用就会出现「柜子还在,隔夜的盒饭没了」。

技术映射:durable 管元数据(Khepri 里的队列声明);persistent 管消息体(CQv2 store);exclusive 绑 Connection 生命周期。

小白:4.3 为什么禁 transient 非 exclusive?exclusive 的非持久为什么还允许?x-expires和 auto-delete 什么区别?x-max-length满了是丢最老的还是拒绝发布?x-overflow有哪些值?CQv1 还能声明吗?经典队列单机挂了消息是不是一定没?

大师:非持久非 exclusive 等于「写在内存里的公共邮箱」,节点一抖全员丢信,还假装是共享队列,事故不可接受,所以废弃特性默认关闭。exclusive 本来就随连接死,允许非持久是因为 RPC 回调队列那种短命场景。x-expires是队列闲置 TTL(没人用就删定义);auto-delete 是最后一个消费者取消后删。x-max-length默认drop-head丢最老;reject-publish让发布者失败(Confirm 下会 nack),reject-publish-dlx则走死信。CQv1 在 4.3移除x-queue-version=1会失败,CQv2 是经典队列唯一存储。经典队列不复制,单机磁盘挂了,持久化也救不了——支付主路径第 19 章应上 quorum。

小胖:那我所有队列都 durable,所有消息都 persistent,开关全开不就完了?

大师:日志审计用 Stream 更合适;网关临时 RPC 用 exclusive。全开的代价是磁盘与 Confirm 延迟。本章实验要对比命运,而不是宣传「全部 persistent」。

技术映射:x-queue-type=classic是单节点低延迟容器,不是金融级副本。

小白:重启实验怎么做才不算「删了 volume」?Dockerrestartdown -v完全不同。还有声明参数事后用 Policy 改max-length和 declare 时写 arguments 谁说了算?队列名能不能用中文?x-queue-type不写时 4.x 默认是什么?exclusive 队列的消息需要 persistent 吗?

大师:docker restart rabbit-promo-1保留/var/lib/rabbitmqdown -v是毁数据。测试用例必须写明。Policy 与 declare 参数合并规则第 13 章,今天只在 declare 写x-max-length,避免两套数字打架。队列名可以是 UTF-8,但 HTTP API 要编码,中台规范用 ASCII 点分名。4.x 默认队列类型以节点配置default_queue_type为准,很多镜像仍是 classic,声明时显式写出x-queue-type=classic免得出发当天被改成 quorum。exclusive 队列随连接死亡,persistent 几乎无意义,不必画蛇添足。

小胖:实验三枪:重启看消息在不在、非法临时队列要报错、长度 5 的队列灌 8 条看谁没了。再补一枪:auto_delete 队列拉起一个消费者再取消,看定义是否消失。


3. 项目实战

3.1 环境准备

同一套promo节点。不要down -v。准备pika与 curl。

3.2 步骤一:声明三组对照队列

步骤目标:持久队列 + 持久消息、持久队列 + 非持久消息、exclusive 队列,为重启实验打基线。

# promo-mq/ch07/declare_and_publish.pyimportpika creds=pika.PlainCredentials("promo","promo_dev_2026")params=pika.ConnectionParameters("127.0.0.1",5672,"promo",creds,client_properties={"connection_name":"ch07-declare"})conn=pika.BlockingConnection(params)ch=conn.channel()args={"x-queue-type":"classic"}ch.queue_declare("q.cq.durable",durable=True,arguments=args)ch.queue_declare("q.cq.durable.transient-msg",durable=True,arguments=args)# exclusive 队列:连接一关就消失,仅用于对照,不要当支付队列ex_q=ch.queue_declare("",exclusive=True,durable=False)print("exclusive queue =",ex_q.method.queue)persistent=pika.BasicProperties(delivery_mode=2)transient=pika.BasicProperties(delivery_mode=1)ch.basic_publish("","q.cq.durable",b"DUR-MSG",properties=persistent)ch.basic_publish("","q.cq.durable.transient-msg",b"TMP-MSG",properties=transient)ch.basic_publish("",ex_q.method.queue,b"EXCL-MSG",properties=transient)print("published 3 kinds, keep this process if you want exclusive to survive")# exclusive 队列在 conn.close() 后删除——下面关闭前先让测试 listinput("press Enter to close connection (exclusive will vanish)...")conn.close()

先不按 Enter,另开终端:

dockerexecrabbit-promo-1 rabbitmqctl list_queues-ppromo name durable messages exclusive

运行结果:应看到q.cq.durablemessages≥1、durable true;exclusive 那行 exclusive true。回车关闭连接后 exclusive 队列消失。

坑:默认交换机""+ routing_key=队列名,等于第 6 章 Default Direct。
坑:queue_declare("")才是服务端生成 exclusive 名;写死名字 + exclusive 也可以,但多连接会抢。

3.3 步骤二:4.3 拒绝 transient 非 exclusive

步骤目标:不打开废弃特性时,declare 失败。

# promo-mq/ch07/forbid_transient.pyimportpikafrompika.exceptionsimportChannelClosedByBroker conn=pika.BlockingConnection(pika.ConnectionParameters("127.0.0.1",5672,"promo",pika.PlainCredentials("promo","promo_dev_2026")))ch=conn.channel()try:ch.queue_declare("q.illegal.transient",durable=False,exclusive=False,auto_delete=False)print("UNEXPECTED success")exceptChannelClosedByBrokerase:print("expected fail",e.reply_code,e.reply_text[:200])conn.close()

运行结果:406 或 PRECONDITION_FAILED,文案含 deprecated / transient。源码闸门:

is_queue_args_combination_permitted(Durable, Exclusive) -> case not Durable andalso not Exclusive of false -> true; true -> rabbit_deprecated_features:is_permitted(transient_nonexcl_queues) end.

坑:有人加auto_delete=True想绕过,组合仍可能被拒,不要靠咒语,改 durable 或 exclusive。
坑:预发打开 permit 会让老测试变绿、生产升级变炸弹(第 3 章基线)。

CQv1 同样拒绝:

ch.queue_declare("q.cqv1",durable=True,arguments={"x-queue-version":1})# 4.3+ 失败

3.4 步骤三:重启看消息命运

步骤目标:docker restart,对比两条消息。

确保步骤一的两条消息还在(不要消费)。然后:

dockerrestart rabbit-promo-1# 等 pingdockerexecrabbit-promo-1 rabbitmq-diagnostics-qpingdockerexecrabbit-promo-1 rabbitmqctl list_queues-ppromo name messages

运行结果(期望):

队列重启后消息
q.cq.durable(persistent 消息)仍在
q.cq.durable.transient-msg(delivery_mode=1)通常没了
exclusive定义已随连接消失

坑:镜像若把数据目录写在容器可写层且没 volume,restart 也可能丢;第 1 章 compose 必须挂 volume。
坑:持久化 ≠ Confirm 已刷盘。本章只验证「重启后还在」的常见路径;掉电级保证看第 8、19 章。
坑:Management UI「Get message」会把消息取出,重启前别用手点。

CQv2 把消息追加到 segment,index 记偏移,经典队列进程挂了由监督者拉起再从磁盘恢复:

%% When a message needs to be written to disk, it is appended to %% its corresponding segment file. An offset is returned ... %% Messages are not reference counted, and are not shared between queues.

3.5 步骤四:x-max-length溢出

步骤目标:上限 5,灌 8 条,默认丢掉最老的;再对比reject-publish

# promo-mq/ch07/overflow.pyimportpikafrompika.exceptionsimportUnroutableError,NackErrordefpublish_n(name,n,overflow=None):conn=pika.BlockingConnection(pika.ConnectionParameters("127.0.0.1",5672,"promo",pika.PlainCredentials("promo","promo_dev_2026")))ch=conn.channel()args={"x-queue-type":"classic","x-max-length":5}ifoverflow:args["x-overflow"]=overflow ch.queue_declare(name,durable=True,arguments=args)ch.queue_purge(name)ch.confirm_delivery()ok,fail=0,0foriinrange(n):try:ch.basic_publish("",name,f"m{i}".encode(),properties=pika.BasicProperties(delivery_mode=2),mandatory=True)ok+=1except(UnroutableError,NackError)ase:fail+=1print("publish fail",i,type(e).__name__)q=ch.queue_declare(name,durable=True,passive=True)print(name,"ready",q.method.message_count,"ok",ok,"fail",fail)conn.close()publish_n("q.cq.len.drop",8,None)# drop-headpublish_n("q.cq.len.reject",8,"reject-publish")

运行结果:drop-head队列 ready=5,最早的m0m2被丢(可用 get 看第一条是否m3)。reject-publish在 Confirm 下后几条失败,队列不超过 5。

坑:x-overflow必须删队列重建,否则 inequivalent arg 406。
坑:无 Confirm 时reject-publish对生产者几乎静默,大促会「以为发出去了」。
坑:x-max-length-bytes按字节,和条数上限同时存在时先撞上的生效。

x-expires演示(可选):arguments={"x-expires": 60000}表示闲置 60s 删队列,测试别拿支付队列做。

auto_delete 对照(建议测试执行):声明q.lab.autodel为 durable=true、auto_delete=true,用一个消费者basic_consume再立刻 cancel,观察队列是否从list_queues消失。支付路径禁止这个组合:最后一个实例滚动发布时会把队列删掉。

运行结果(overflow 补充):drop-head 时 UI 的 publish 速率仍在涨,队列深度钉死在 5,看起来很健康。所以监控不能只看深度上限,还要看「被丢弃的消息」——经典队列可看drop_unroutable之外的 overflow 指标或自己在应用记 Confirm nack。否则大促丢最老订单无人知晓。

3.6 完整代码清单

column/samples/ch07/ declare_and_publish.py forbid_transient.py overflow.py

3.7 测试验证

编号步骤期望
TC-CH07-01非法 transient 队列Channel 406
TC-CH07-02restart 后 durable+persistent消息仍在
TC-CH07-03restart 后 durable+transient msg消息无
TC-CH07-04max-length=5 灌 8 drop-headready=5
TC-CH07-05x-queue-version=1声明失败
curl-s-upromo:promo_dev_2026\http://127.0.0.1:15672/api/queues/promo/q.cq.durable

durabletype(classic)、messages

值班检查单:支付相关队列必须同时满足:durable 为真、arguments 里有明确 queue-type、生产者 delivery_mode=2、数据目录在 volume 上。缺任何一项,重启演练都可能「偶发丢失」,事后无法复盘是谁的锅。溢出策略若是默认丢头,要在 Wiki 写明「最老订单可能无声消失」,并配深度告警;资金类应改为拒绝发布并让 Confirm 失败冒泡到 outbox。

经典队列的存储层 CQv2 把消息追加进段文件、用 index 记偏移,消息不在队列间共享。这意味着同一份 JSON 进两个队列就是两份磁盘。Fanout 放大的不只是内存,还有每条队列自己的 store。容量规划时按「副本份数」计,而不是按「逻辑消息条数」计。

再强调 exclusive 与滚动发布的冲突:支付消费者若用 exclusive 队列接活,滚动时旧连接断开队列即删,新实例看到的是另一张空队列,中间的消息直接消失。exclusive 只留给「谁声明谁独享、断连即焚」的 RPC。共享业务邮箱必须是 durable 非 exclusive。4.3 禁 transient 非 exclusive,就是防止第三种更糟的「共享却不落盘」。把这三种命运讲给新同事听,比让他们背参数表快。

声明幂等是另一件要讲清的事:queue_declare对已存在且参数等价的队列返回成功,这是消费者启动时「确保拓扑」的常规做法。参数不等价则 406,滚动发布会有一半实例起不来。所以改 max-length 或 DLX 必须作为明确的变更窗口:先扩新队列再切流量,或接受短暂删除。禁止在启动脚本里「先删后建」支付队列,那会把堆积一键清空。运维变更单里应写「是否删除队列」为单独勾选项,默认否。删除等于丢堆积,需双人复核。宁可多留一个旧队列,也不要在高峰误删。旧队列可改名归档,不可直接 Purge。归档队列加 TTL 或后续导出再删。


4. 项目总结

优点与缺点

对象优点缺点
Classic + durable + persistent低延迟、语义简单、适合单机无副本,节点挂即风险
exclusive 队列RPC 回调干净不能跨连接、不能当业务邮箱
非持久消息快、少磁盘重启即丢
打开 transient_nonexcl permit兼容老教程4.x 升级定时炸弹

优点:1)声明参数把命运写死可测。2)max-length 是防堆积第一闸。3)CQv2 去掉 CQv1 的历史坑。
缺点:1)durable 与 persistent 极易混。2)溢出默认丢头像无声。3)经典队列不能当支付的最终方案。

把四开关做成团队口诀:定义能否熬夜(durable)、货是否贵重(persistent)、是不是私人格子(exclusive)、人走摊撤不撤(auto-delete)。评审时逐项打勾,比看代码里有没有queue_declare有用。

适用场景

  • 单机通知、允许短暂丢失的非资金流。
  • RPC 回调 exclusive。
  • 用 max-length 保护 Broker。
  • 教学与故障演练(对照重启)。

不适用:资金、库存扣减的唯一真相源(用 quorum);无限回放(用 Stream);把 auto_delete 支付队列当「自动清理」。

注意事项

  • 4.3+ 默认禁 transient 非 exclusive;CQv1 移除。
  • 改 arguments 先删队列或 inequivalent。
  • 安全:谁能 declare/delete 队列是 configure 权限。
  • 容器必须有数据 volume,否则 durable 是假的。

常见踩坑(生产)

  1. 只 durable 队列,消息 delivery_mode=1,发布重启丢支付通知。根因:两层持久化只做了一层。
  2. max-length 默认 drop-head,客服找不到最早那单。根因:没选 reject-publish + 告警。
  3. CI 打开 permit 临时队列,预发忘记关。根因:用废弃特性当兼容层。

思考题

  1. 持久化消息在 Confirm 返回后、操作系统掉电前,经典队列是否保证已进磁盘?还缺哪一层?
  2. 同一经典队列,消费者未 Ack 的消息在重启后会出现什么标志?

(题 1 第 8 章;题 2 第 9 章redelivered。)

附录 C:第 6 章思考题参考答案

题 1:两绑定命中同一队列。
Broker 会去重到该队列一次(路由结果按队列去重是常见实现;Topic 的route/3注释写明调用方负责去重)。不要依赖「绑两次当重试」。业务去重仍靠 message_id。

题 2:一次发布进 Direct+Topic。
可选:交换机到交换机绑定(Alternate/Exchange-to-exchange)、Fanout 前置再由各队列转、应用双发、Shovel。e2e 绑定运维成本高;双发要幂等。第 21 章跨集群再用 Shovel。

延伸阅读与资源

SQLAlchemy 2.0从入门到进阶的实战之旅
Dify 从入门到进阶:LLM 应用平台实战修炼
Java 工程师进阶:从 JVM 生产排障到OpenJDK原理
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化)
Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Redis 实战修炼与原理进阶
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
MongoDB 实战进阶与内核修炼
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询