缓存、消息队列与事件流、基于数据库的任务队列、定时调度与持久工作流、分布式锁、限流、搜索引擎、对象存储怎么选、怎么用才不出错,以及消费端的重复、乱序、恢复测试。
引入新的中间件前写一份决策记录,写清需求、为什么现有数据库或服务不够、考虑过的替代、退出路径和许可证
怎么检查:评审新增中间件的 PR 时,检查是否附带决策记录(ADR)且五项都有内容
为什么、来源
为什么:中间件一旦上线就很难撤掉;没写清理由的引入,往往只是跟风,最后变成没人敢动的依赖。
给缓存、队列、搜索、调度新增可靠性要求前,先写清容量、积压上限、可丢多少数据、多久恢复和谁负责运维
怎么检查:决策记录或运维手册里有这几项;评审时核对
为什么、来源
为什么:口头承诺的可靠性没人兑现;中间件出事时才发现没人值班、没人会恢复。
每个中间件记录实际使用的版本、镜像摘要和发行包许可证;生产环境不用 latest 这类浮动标签
怎么检查:CI 扫描部署清单,发现浮动标签或缺少镜像摘要时失败;用 SBOM 工具生成组件清单
为什么、来源
为什么:浮动标签会在不知情时换掉版本甚至换掉许可证;断网环境里还会直接拉不到镜像。
许可证按实际下载和分发的发行包与版本核对,不只看源码仓库的 LICENSE;每次升级重新核对
怎么检查:升级时对照官方许可证页面和发行包内的许可文件,记录核对结果;许可扫描工具找候选
为什么、来源
为什么:同一个项目的源码和二进制、社区版和企业版、新旧版本可能是不同许可证(如 Elasticsearch、Redis、Dragonfly)。
- 本主题的选型表只标许可证;能不能用、随产品分发时要做什么,按 27 的口径判断。
- 同时确认是否有联网激活、使用量上报或用量上限(如 Dragonfly 的生产 vCPU 上限)。
断网交付的项目,交付前实测中间件在断网下的冷启动、重启、故障恢复和升级,所需镜像、依赖、分词词典都预先备好
怎么检查:在断网的验收环境里跑一遍启动、重启、恢复和升级,保留记录
为什么、来源
为什么:「能私有化部署」不等于能在断网下跑起来;漏一个依赖或一次联网检查,现场就起不来。
新增专用中间件时,写清它挂了业务怎么降级;不能降级的,写明事故处理办法
怎么检查:运维手册里有降级或事故处理一节;故障演练时验证
为什么、来源
为什么:每多一个依赖就多一个单点,没想过降级的话,缓存挂了整个系统一起挂。
缓存不得作为订单、资金、审批、权限、审计记录的唯一存储;缓存全部丢失后系统必须能从权威数据恢复
怎么检查:测试环境清空缓存后跑核心流程的集成测试,结果必须正确
为什么、来源
为什么:缓存会过期、会被逐出、会丢数据,放在里面的「真相」迟早会丢。
每类缓存键写清负责人、过期时间、键的版本、租户范围、失效方式和缓存不可用时的行为;不设「永不过期」的默认值
怎么检查:缓存键定义集中在一处(代码常量或配置文件),评审时逐类核对;静态检查找没有过期时间的写入
为什么、来源
为什么:没人负责、没有过期时间的键会无限增长,结构变化后旧数据还会被当成新数据读出来。
- 键名示例:
<应用>:<环境>:<租户>:<领域>:<对象>:<编号>:v<版本>。
- 用户输入进键名前要转义,避免冒号和控制字符搅乱键空间;不把高基数字段放进键名。
- 涉及个人信息时,键里用内部编号,不用手机号、邮箱。
多租户系统的缓存键必须包含租户标识,并有跨租户读取的负例测试
怎么检查:集成测试:租户 A 写入后,用租户 B 的身份读同一业务键,必须读不到
为什么、来源
为什么:漏了租户的缓存键会把一个客户的数据直接返回给另一个客户。
更新权威数据后必须有明确的缓存一致性策略(更新后删缓存、带版本写入或绕过缓存),并测试写后读和并发竞态
怎么检查:集成测试覆盖「写后立即读」和「并发写与回填缓存交错」两种场景
为什么、来源
为什么:「写库后删缓存」在并发下仍可能被迟到的回填覆盖成旧值,不测就发现不了。
- 常规热点数据用旁路缓存(cache-aside):读时没命中再查库回填,写库后删除缓存。
- 用版本号或时间戳挡住迟到的旧值回填。
- 写后读一致性要求高的地方(如付款后查订单状态)直接读库。
热点缓存处理穿透、击穿和雪崩:负缓存加短过期时间、同键重建合并、过期时间加随机抖动
怎么检查:压测时模拟大量不存在的键、热点键过期、批量同时过期,观察数据库负载
为什么、来源
为什么:缓存一失效,流量全打到数据库上,往往比没有缓存还糟。
- 穿透(查不存在的数据):先校验参数,再用短时负缓存;布隆过滤器只用于高频且数据集稳定的场景。
- 击穿(热点键过期):同一个键只让一个请求去重建(singleflight),能容忍旧值时先返回旧值再后台刷新。
- 雪崩(大批键同时过期):过期时间加随机抖动,热点预热,数据库前加限流。
缓存登录态或授权结果时必须能及时撤销;负缓存和敏感数据缓存都要有过期时间
怎么检查:测试:撤销用户权限或注销后,在约定时间内缓存不再放行
为什么、来源
为什么:权限撤销了缓存还放行,等于没撤销。
写数据库和发消息要同时成功时,用事务 outbox 或等价机制,不在提交事务后直接发消息
怎么检查:代码评审找「提交后直接 publish」的写法;故障注入测试在提交后、发送前杀掉进程,事件不能丢
为什么、来源
为什么:提交成功、发送失败,事件就永久丢了;反过来先发后提交,又会发出从未发生的事件。
- outbox 表的设计见 17。转发程序拿到中间件的发送确认后才把记录标为已发送;转发可能重复,由消费者去重。
- 也可以用变更数据捕获(CDC,读数据库变更日志)代替轮询 outbox。
持久化的消息必须带唯一事件编号、事件类型和版本、发生时间、租户和追踪编号;大载荷只传引用
怎么检查:契约测试用 JSON Schema 校验消息样例必须包含这些字段
为什么、来源
为什么:没有编号没法去重,没有版本没法演进,没有追踪编号出了问题查不到链路。
对外发布的事件信封(CloudEvents)和事件目录的要求见 16;这里是内部队列消息的最低要求。
消费者必须按业务键幂等:同一条消息处理多次,结果和处理一次相同
怎么检查:集成测试把同一条消息重复投递、并发投递,检查业务结果不变
为什么、来源
为什么:RabbitMQ、NATS JetStream、Kafka 的常规投递都是至少一次,重复不可避免。
- 先完成幂等的业务处理,再确认(ACK)消息;处理和确认不在同一个事务里时,接受「可能重复」,用业务唯一约束兜底。
- 消费方的去重记录(inbox 表)设计见 17;幂等语义见 16。
不得以中间件的「恰好一次」功能代替业务幂等;对外支付、HTTP 调用、数据库更新的只发生一次由业务唯一性保证
怎么检查:评审有外部副作用的消费者,确认有业务幂等键或下游的幂等键
为什么、来源
为什么:Kafka 的事务、JetStream 的去重窗口只管中间件内部,管不到外部系统。
消息和任务的重试必须有次数上限、指数退避加随机抖动、死信或隔离队列,以及人工补偿和重放入口
怎么检查:检查队列配置的最大投递次数和死信设置;故障注入测试一条永远失败的消息,确认它最终进入死信而不是无限循环
为什么、来源
为什么:一条「毒消息」无限重试会堵住整个队列,还会反复触发副作用。
重放死信要有权限控制和记录,避免误操作重复触发副作用。
需要按顺序处理时,写明排序键和顺序范围(同一个业务对象内有序),并测试重平衡和重试后的顺序;不承诺全局有序
怎么检查:集成测试在消费者扩缩容、消息重试时检查同一对象的处理顺序
为什么、来源
为什么:分区重平衡和重试都会打乱顺序;全局有序几乎都做不到,也很少真正需要。
消费者上线前必须通过重复投递、乱序投递和处理中崩溃恢复三类测试
怎么检查:CI 用真实中间件容器跑集成测试:同一消息投两次、打乱顺序投递、处理中途杀掉消费者再重启,检查最终业务状态正确且无丢失
为什么、来源
为什么:只测成功路径的消费者,一遇到重投、乱序、进程被杀就会丢任务或重复执行。
- 重复:同一消息投两次以上,业务结果与一次相同。
- 乱序:同一对象的旧事件晚到时,不能覆盖新状态(用版本号判断)。
- 恢复:处理中
kill -9 消费者或断开与中间件的连接,任务最终被处理且只产生一次副作用;租约过期的任务能被别的工作进程接走。
- 中间件短暂不可用时,生产者和消费者能自动恢复,不丢已确认的消息。
不可丢的任务不得用 Core NATS 或其他不持久的发布订阅承载
怎么检查:评审消息通道配置,确认不可丢任务走的是持久化通道(JetStream、RabbitMQ 持久队列、数据库队列)
为什么、来源
为什么:Core NATS 是「发出即忘」,订阅者离线时消息直接丢失。
基于 PostgreSQL 的任务队列用现成的库(按语言见选型),不自己写领取、租约、重试和死信逻辑
怎么检查:评审时检查是否有自建的任务表加轮询代码;有的话要求说明现成库为何不适用
为什么、来源
为什么:领取、租约过期回收、最大次数、死信、清理归档、优先级公平这些细节很多,自己写很容易漏,现成库都已做好并经过大量使用。
- 这些库都基于
FOR UPDATE SKIP LOCKED 在短事务里领取任务;其原理和事务要求见 17。
- 好处是任务可以和业务数据在同一个事务里写入,不需要 outbox。
- 吞吐、长积压或高频轮询会带来表膨胀和数据库负载,出现这些信号时再评估专用队列。
确有理由自己实现数据库任务领取时,必须有租约到期时间、持有者令牌、续约与过期回收、最大尝试次数和死信状态
怎么检查:集成测试:领取后杀掉工作进程,租约过期后任务被重新领取;超过最大次数后进入死信;用旧令牌提交结果被拒绝
为什么、来源
为什么:没有租约,工作进程一崩任务就永远卡在「处理中」;没有令牌,过期的工作进程还会覆盖新结果。
租约和 fencing token 的数据库实现见 17;这里只列必须具备的能力。
领取任务和持有数据库锁的事务里不做网络调用、大模型调用或其他耗时操作
怎么检查:代码评审检查事务块内是否有 HTTP、RPC、大模型调用;监控长事务和锁等待
为什么、来源
为什么:事务里等外部服务,锁就一直不放,别的请求排队,数据库连接也被占住。
每个定时任务声明唯一编号、时区、错过触发后的处理方式和是否允许重叠执行,并测试夏令时切换
怎么检查:调度配置用 schema 校验这四项必填;对使用有夏令时时区的任务写切换日的测试
为什么、来源
为什么:不写时区会在服务器换时区后错时;不写重叠策略,上一次没跑完下一次又开始,会重复处理。
- 多实例部署时,同一个定时任务同一时刻只能有一个实例触发(用调度器自带的集群模式或数据库锁)。
- 定时任务本身也要幂等,补跑和重复触发都不能出错。
只有跨天等待、等人审批或外部回调、需要补偿的长流程才引入持久工作流引擎;简单定时和单步异步任务不用
怎么检查:决策记录里写明流程的等待时长、步骤数和补偿需求
为什么、来源
为什么:Temporal 这类引擎本身就是一套要运维的分布式系统,为几个 cron 引入得不偿失。
使用 Temporal 时,Workflow 代码必须确定性;网络、数据库、大模型等外部操作放进 Activity,Activity 自己要幂等
怎么检查:CI 跑 Temporal SDK 的回放测试(用历史事件重放 Workflow),并开启 SDK 的确定性检查
为什么、来源
为什么:Temporal 靠重放事件历史恢复流程,Workflow 里有随机数、当前时间或直接 I/O 就会重放失败。
修改在途 Workflow 的逻辑要用 SDK 的版本机制,不能直接改代码让旧实例重放出错。
不自己写跨重启的「有状态工作流框架」;流程简单就用数据库状态机加任务队列,复杂就用成熟的工作流引擎
怎么检查:评审时识别自建的流程引擎、定时器和补偿框架
为什么、来源
为什么:状态重放、补偿、升级兼容、定时器竞态都很难做对,维护成本远超预期。
防重复创建、防重复执行这类业务唯一性,靠数据库唯一约束和条件状态更新,不只靠内存锁或 Redis 锁
怎么检查:检查数据模型里对应字段有唯一约束;并发测试同时提交两次,只成功一次
为什么、来源
为什么:锁会过期、客户端会卡顿,过期后两个实例都以为自己拿到了锁。
跨系统的强互斥必须使用 fencing token,由下游拒绝旧令牌的写入;不以 Redlock 宣称保证外部一致性
怎么检查:实验:让锁持有者暂停到租约过期,别的实例拿到新锁后,旧持有者恢复写入必须被拒绝
为什么、来源
为什么:进程停顿、网络延迟都可能让过期的持有者继续写;只有下游检查令牌才能挡住。
- 能放进同一个数据库的互斥,优先用行锁或事务级 advisory lock(见 17)。
- 需要独立协调服务时,etcd 的租约和修订号可以当 fencing token 用。
- Valkey/Redis 的带过期锁只做「软互斥」(重复了也不出大错的场景),释放时要校验是自己的锁。
多副本部署的全局限流必须用共享的配额源(网关或共享存储的原子计数),不得每个副本各自计数后当成全局配额
怎么检查:压测:多个副本同时承压,统计放行总数不超过配额
为什么、来源
为什么:每个副本各算各的,N 个副本就放行 N 倍流量。
限流的读取、判断、写入必须是原子操作;超限时返回明确的响应和指标,并写清限流存储不可用时放行还是拒绝
怎么检查:并发测试同一个键的计数准确;评审代码不存在「读取→加一→写回」分开执行;评审配置里的失败策略
为什么、来源
为什么:非原子的计数在并发下会多放行;故障时放行还是拒绝要事先定,涉及成本和安全的一般选拒绝。
- 先定按什么维度限(IP、用户、租户、密钥、接口、业务操作),用什么算法(令牌桶、滑动窗口),突发上限多少。
- 区分「请求速率」和「并发数」两种限制。
- 超限返回 429 和
Retry-After,格式见 16。
搜索索引是派生数据:必须能从权威数据全量重建,有重建手册并演练过
怎么检查:定期在测试环境清空索引后按手册全量重建,记录耗时和结果
为什么、来源
为什么:索引会损坏、分词器升级会要求重建;不能重建就等于把真相放在了索引里。
- 索引文档带来源编号、租户和来源版本号,用来对账和防旧数据覆盖。
- 增量更新走 outbox、CDC 或可靠的批量同步;用别名切换新旧索引,支持回退。
- 监控索引延迟和漏数。
搜索查询必须在检索层强制租户和权限过滤,并有「无权文档搜不到」的负例测试
怎么检查:集成测试用无权用户搜索已知存在的文档,结果里不得出现,包括计数、高亮和联想
为什么、来源
为什么:搜索结果、联想词和计数都可能泄露别人的数据。
中文搜索选型和分词器升级前,用本业务的真实查询集评测召回和排序,覆盖短词、专有名词、中英数混合、同义词和权限负例
怎么检查:维护一份带标注答案的查询集,评测时统计前 10 条召回率(Recall@10)、排序质量(NDCG@10)和查询耗时,结果附在决策记录里
参考值:查询集规模没有标准;参考值是每类至少几十条、总数一两百条起,阈值由业务负责人确认。
为什么、来源
为什么:「支持中文」不代表搜得准;PostgreSQL 的 pg_trgm 按三字切分,对一两个汉字的短查询效果可能很差。
- 业务编号、精确匹配不要用全文检索,用普通索引或前缀索引。
- 中文子串匹配可以用
pg_bigm(按两字切分);要分词时评估 zhparser 等扩展,或专用引擎的中文分析器。
- 查询集示例:「发票」(短词)、「售后工单」(业务术语)、「A320B 型号」(中英数混合)、「退款/退货」(同义词)。
- 国产或兼容 PostgreSQL 的数据库,扩展能否用要逐个实测(见 17)。
中间件升级和迁移前做快照,写清回滚或前滚办法;迁移时新旧并行、对账通过后再下线旧系统
怎么检查:升级计划里有快照、回滚步骤和对账口径;在预发环境演练
为什么、来源
为什么:索引格式、持久化格式、工作流状态的升级往往不可逆;直接切换出问题就回不去。
- 影子读或影子消费时不得触发外部副作用。
- 缓存通常可以直接丢;搜索可以从权威数据重建;高价值消息要定好重放起点;在途的长流程不能直接丢掉旧平台的状态。
- 不要只因「性能更好」就换中间件,先用业务数据压测对比。
业务代码按 S3 兼容接口访问对象存储,不依赖某个实现的私有接口;自建对象存储选仍在维护的项目
怎么检查:依赖检查找特定实现的专有 SDK 和管理接口调用;选型评审确认项目仍在发布安全修复
为什么、来源
为什么:MinIO 社区版仓库已归档、不再修复漏洞,绑死在一个实现上就很难迁走。
没有符合条件的规则。
缓存
Valkey;没有明确收益时不加缓存,直接查库 · Valkey 为 BSD-3-Clause;Redis 8 起为 AGPLv3 / SSPLv1 / RSALv2 三选一,7.2 及以前为 BSD-3-Clause,7.4 到 8 之前为 RSALv2 / SSPLv1;Dragonfly 为 DSAL
备选:Redis Open Source(需要 Redis 8 及以后的特有功能,且按交付方式选定了三种许可证之一);进程内缓存(单实例、数据量小、可以容忍各副本短暂不一致)
不推荐:Dragonfly;把缓存当权威库
基于 PostgreSQL 的任务队列(按语言)
Go 用 River;Node 用 pg-boss 或 Graphile Worker;Python 用 Procrastinate;Elixir 用 Oban;Java 用 db-scheduler 或 JobRunr · River 为 MPL-2.0;pg-boss、Graphile Worker、Procrastinate 为 MIT;Oban、db-scheduler 为 Apache-2.0;JobRunr 见待确认
备选:Celery / RQ(Python)、BullMQ(Node)、Asynq(Go)等基于 Redis 的成熟队列库(已有 Valkey/Redis 且不需要和业务同事务写入任务;各语言细节见 10–15);RabbitMQ 或 NATS JetStream(跨服务、跨团队消费,或数据库负载已和队列明显相关)
不推荐:自己写任务表加轮询;用 Redis List 手写队列;一开始就上 Kafka 或 Pulsar
工作队列(一个任务处理一次、确认、重试、路由)
RabbitMQ(用仲裁队列) · RabbitMQ 为 MPL-2.0(以官方仓库为准,见待确认);NATS 为 Apache-2.0
备选:NATS JetStream(团队已熟悉 NATS,或同时需要轻量发布订阅和请求响应);数据库任务队列库(任务和业务同库同事务)
不推荐:Core NATS;把 RabbitMQ 普通队列当长期回放日志
事件流(可回放、多消费组、大吞吐)
outbox 加 NATS JetStream 起步;确有长期回放、多个独立消费组、分区大吞吐需求时用 Apache Kafka · Kafka、Pulsar、RocketMQ、NATS 为 Apache-2.0;Redpanda 社区版为 BSL,企业功能另需许可
备选:Apache Pulsar(多租户、分层存储、跨地域复制是硬需求);Apache RocketMQ(团队熟悉且需要它的事务消息、延时消息等业务消息类型);Redpanda(要兼容 Kafka 接口又想运维更轻;先审许可证)
不推荐:只因「高并发」就上 Kafka;把中间件的恰好一次当成外部副作用只一次
定时调度
平台自带的定时触发(Kubernetes CronJob、systemd timer)或任务队列库自带的定时功能 · Quartz、PowerJob、Airflow 为 Apache-2.0;XXL-JOB 为 GPL-3.0
备选:Quartz(集群模式用 JDBC 存储)(Java 服务需要应用内的集群调度);PowerJob 或 XXL-JOB(Java 团队需要带管理界面的分布式调度;XXL-JOB 是 GPL-3.0,随产品分发前按 27 审);Apache Airflow(数据管道、批处理 DAG)
不推荐:用 Airflow 做订单超时这类在线业务;多实例各自跑同一个 cron
持久工作流
Temporal(确有长流程时) · Temporal、DBOS、Hatchet 为 MIT;Flowable 为 Apache-2.0
备选:DBOS(已用 PostgreSQL、想以库的形式在应用内获得持久执行,不另部署服务);Hatchet(以后台任务为主、兼带简单工作流;服务端成熟度先自行验证);Flowable(需要 BPMN 流程建模、业务人员参与流程设计(Java))
不推荐:为几个定时任务部署 Temporal;自写跨重启工作流框架;Inngest 自托管作为默认
分布式锁与协调
数据库唯一约束加条件状态更新;同库互斥用行锁或事务级 advisory lock
备选:etcd 租约加修订号做 fencing(跨系统强互斥、需要独立的协调服务);Valkey 带过期时间和持有者校验的锁(重复执行也不出大错的软互斥)
不推荐:没有续约、释放时不校验持有者的 SETNX 锁;用 Redlock 宣称保证外部一致
限流
网关或服务网格自带的限流;需要在应用里做全局配额时,用 Valkey 原子脚本实现令牌桶
备选:进程内令牌桶(单实例,或只做本副本的保护);数据库计数(请求量很低)
不推荐:多副本各自内存计数当全局配额;读取、加一、写回分开执行
应用内搜索
PostgreSQL 全文检索加 pg_trgm;中文短词用 pg_bigm · pg_trgm 随 PostgreSQL(PostgreSQL License);pg_bigm 见待确认;ParadeDB 社区版为 AGPL-3.0
备选:zhparser 等中文分词扩展(需要中文分词且能在目标数据库上编译部署;许可证逐项审);ParadeDB pg_search(想在 PostgreSQL 里获得 BM25 排序;社区版为 AGPL-3.0)
不推荐:未用中文查询评测就宣称 PostgreSQL 全文检索够用
独立搜索引擎
OpenSearch · OpenSearch 为 Apache-2.0;Elasticsearch 源码可选 AGPLv3 / SSPL 1.0 / ELv2,官方默认发行包为 ELv2;Typesense 为 GPL-3.0;Meilisearch 社区版 MIT、企业版另有许可(见待确认)
备选:Meilisearch 社区版(中小规模、面向用户的即时搜索和容错;集群高可用等功能在企业版,先核对边界);Typesense(轻量即时搜索;GPL-3.0,随产品分发前按 27 审);Elasticsearch(团队已有经验或要用其商业功能;先按发行包核对许可证)
不推荐:小系统只为模糊搜索就上整套搜索集群;把索引当唯一真相
自建对象存储
能用云厂商或私有云提供的 S3 兼容对象存储就用托管的;必须自建时选 Ceph RGW 或 SeaweedFS · SeaweedFS 为 Apache-2.0;Garage 为 AGPL-3.0;Ceph 见待确认;MinIO 社区版为 AGPL-3.0(已归档)
备选:Garage(小规模、多地域部署;AGPL-3.0);继续使用 MinIO 商业版(已购买商业授权)
不推荐:新项目选用 MinIO 社区版
✕ 错误做法:用 Redis SETNX 锁防止订单重复
✓ 应该:数据库唯一约束或条件更新;跨系统强互斥用 fencing token
✕ 错误做法:事务提交后直接发消息
✓ 应该:事务 outbox 或 CDC
✕ 错误做法:「Kafka 是恰好一次,所以支付只扣一次」
✓ 应该:业务幂等记录加下游的幂等键
✕ 错误做法:用 Core NATS 承载不可丢的任务
✓ 应该:JetStream、RabbitMQ 持久队列或数据库任务队列库
✕ 错误做法:自己写任务表和轮询代码
✓ 应该:用对应语言的 PostgreSQL 任务队列库
✕ 错误做法:每次查数据库前都先查一遍缓存
✓ 应该:按实际延迟和数据库瓶颈决定要不要缓存
✕ 错误做法:所有键都设同一个固定过期时间
✓ 应该:过期时间加随机抖动,热点键单飞重建
✕ 错误做法:把 PostgreSQL 全文检索说成中文分词很好
✓ 应该:用真实中文查询评测,必要时用 pg_bigm、分词扩展或专用引擎
✕ 错误做法:为几个定时任务部署 Temporal 或 Airflow
✓ 应该:cron 或任务队列库的定时功能
✕ 错误做法:客户内网直接拉 latest 镜像
✓ 应该:固定镜像摘要,离线物料预先备好
✕ 错误做法:只看源码仓库的 LICENSE 就下结论
✓ 应该:按实际使用的发行包、版本和部署方式核对
✕ 错误做法:消费者只测了成功路径
✓ 应该:测重复、乱序、崩溃恢复和中间件断连