Stamp 装饰消息的原理
解读
“Stamp 装饰消息”在国内 PHP 面试里通常不是指前端 CSS 的“邮戳水印”,而是指基于“Stamp 消息戳”设计模式对业务消息(或事件)进行无侵入式增强。
核心思路:把“消息”当成一个普通 PHP 对象,把“与业务无关的横切能力(traceId、重试次数、延迟时间、租户ID、灰度标签等)”封装成一个个轻量的 Stamp 对象,通过 SplObjectStorage 或数组挂载到消息体上;下游消费者、中间件、队列驱动可以按类型取出对应 Stamp 完成各自逻辑,而原始消息类保持纯粹。
该模式在 Symfony Messenger 组件里被官方固化,但在国内 Laravel/ThinkPHP/自研框架中同样可用,用来解决“消息随着业务迭代不断加字段”导致的 BC 问题,也是面试官区分“只会用队列”与“懂消息架构”的一把尺子。
知识点
- 消息对象(Message):包含业务载荷的纯数据类,不包含任何基础设施信息。
- Stamp 接口:空接口,仅作类型标识,允许框架通过 instanceof 快速过滤。
- 具体 Stamp:
BusNameStamp、DelayStamp、RetryCountStamp、TenantIdStamp、TraceIdStamp、AckStamp 等,每个 Stamp 只关心一个维度。 - Envelope(信封):聚合“消息 + Stamp 集合”的不可变对象,提供 with(Stamp)、without(string) 方法返回新实例;国内团队常把 Envelope 当成“消息上下文”塞到 Redis/RMQ。
- 中间件链:BusMiddleware 按 FIFO 顺序处理 Envelope,典型实现有
- ValidateMiddleware:取出 ValidationStamp 做参数校验;
- DelayMiddleware:取出 DelayStamp 把任务推到延迟队列;
- RetryMiddleware:取出 RetryCountStamp 决定重试策略;
- TraceLogMiddleware:取出 TraceIdStamp 写入 ELK。
- 序列化:PHP 自带 serialize/unserialize 会连带 Stamp 一起落地;生产环境推荐使用 JSON+类型映射表,避免对象体积膨胀。
- 不可变原则:Envelope 每次 with()/without() 都返回新实例,防止并发消费时 Stamp 被意外篡改。
- 与 Laravel 的兼容:Laravel 的 Queue::pushRaw 只认字符串,可在 Job 的 __construct 里把 Envelope 序列化后放进 $job->payload,消费端 JobMiddleware 按 Stamp 类型恢复延迟、重试逻辑。
- 性能注意:Stamp 数量 < 10 个时内存增量可忽略;海量小消息场景建议把 TraceId 等通用 Stamp 压缩成位图或二进制,减少网络包大小。
- 测试技巧:PHPUnit 里断言 Envelope 是否携带某 Stamp 时,用 assertInstanceOf 而非裸数组,防止重构时字段名变更导致用例失效。
答案
Stamp 装饰消息的原理可以概括为“把横切关注点从业务消息中剥离,以对象方式动态挂载”。
具体步骤:
- 定义一个空接口 Stamp,所有具体戳都实现它;
- 创建不可变 Envelope 类,内部用 SplObjectStorage 保存 Stamp 实例,提供 with(Stamp): self 与 getAllStamps(): array;
- 派发消息时,调度器把业务对象包装成 Envelope,并按场景附加 BusNameStamp、DelayStamp 等;
- 中间件链依次处理 Envelope,各中间件通过 $envelope->getStamp(DelayStamp::class) 取出自己关心的戳,完成延迟、重试、日志、多租户路由等逻辑;
- 序列化层将整个 Envelope 落盘或入队;消费端反序列化后再次进入中间件链,直到最终 Handler 拿到纯净的业务消息对象执行;
- 整个过程对业务代码零侵入,新增能力只需新增 Stamp 与对应中间件,无需改动原有消息类,符合开闭原则。
拓展思考
- 顺序敏感场景:如果 DelayMiddleware 在 RetryMiddleware 之后,可能导致“已延迟的消息被重试中间件再次计算延迟”,国内大厂通常把 DelayMiddleware 放在链最前端,并通过 FrozenEnvelope 防止重复延迟。
- 多队列路由:利用 RoutingKeyStamp 保存业务方指定的 RMQ routing_key,在 SendMiddleware 里根据该戳把消息发到不同 Exchange,实现“一条业务消息多队列广播”而无需继承多个 Job 类。
- 安全隔离:在 SaaS 多租户系统里,TenantIdStamp 参与签名计算,消费端验证签名不匹配直接拒绝,可防止“租户 A 的消息被租户 B 的工作节点误消费”这一国内合规审计高频问题。
- 降级策略:大促期间若 TraceLogStamp 造成序列化体积翻倍,可在线通过 ConfigStamp 动态关闭非必要 Stamp,实现“热降级”而无需发版。
- 与 Swoole/FPM 协程共存:在协程环境下,Envelope 的不可变特性避免了协程间共享引用导致的 Stamp 竞态;若使用可变数组存储 Stamp,需要借助 Co\mutex 保护,性能下降 8% 左右。