消息总线选型决定最终一致性效果,中小团队宜用think-queue+redis,事件定义与补偿逻辑须固化于composer包,composer.lock是跨服务行为一致的唯一可信源。

消息总线选型直接影响最终一致性落地效果
用 Composer 管理分布式事件一致性,第一步不是写代码,而是选对消息总线。RocketMQ、RabbitMQ、Redis Stream 本质不互通,RocketMQTemplate 和 PhpAmqpLibConnectionAMQPStreamConnection 的 API 差异大到没法抽象成一个通用包——强行封装只会让错误更隐蔽。
常见踩坑点:
- 在 TP6 项目里
composer require aliyuncs/aliyun-openapi-php-sdk后直接调 RocketMQ HTTP 接口,结果发现没做幂等校验,重复消费导致库存扣两次 - 用
symfony/messenger配 RabbitMQ,但没配failure_transport,消息失败后静默丢弃,状态永远不收敛 - 选 Redis Stream 做轻量总线,却用
redis.xreadgroup不带NOACK,消费者崩溃后消息永久卡在 pending list
建议:中小团队优先用 topthink/think-queue + Redis(已内置重试、失败队列、延迟投递),避免过早引入 Kafka/RocketMQ 运维负担;要上云原生,就直接用厂商 SDK(如 aws/aws-sdk-php 对接 SQS),别自己封装“统一消息门面”。
Composer 包如何承载事件定义与补偿逻辑
最终一致性不是靠“装一个包”实现的,而是靠包里明确的契约:事件结构、补偿接口、幂等键生成规则。这些必须固化在 Composer 包中,而不是散落在业务代码里。
实操要点:
- 每个事件类型建独立包,比如
acme/order-created-event,含OrderCreatedEvent类和CompensateOrderCreation接口 -
composer.json中声明"autoload": {"psr-4": {"Acme\OrderEvent\": "src/"}},确保调度器能自动加载,不靠手动require - 幂等键必须由事件本身携带,例如
$event->getIdempotencyKey()返回order_id:12345,不能依赖外部缓存生成 - 补偿逻辑不能访问原始事务上下文(如 DB 连接句柄),只能通过事件 payload 和配置参数完成回滚
错误示例:thinkphp/distributed-transaction 这类包试图在 Db::transaction() 外套一层“跨库提交”,实际运行时因连接隔离失效,反而掩盖了真正该拆分的边界。
composer.lock 是跨服务事件行为一致的唯一可信源
三个服务 A/B/C 都监听 order.created 事件,但 A 用 monolog/monolog v2.10 记日志,B 用 v3.5,C 用 v3.7——表面看都能跑,但当补偿逻辑依赖日志格式做状态判断时,版本差异会直接导致收敛失败。
关键动作:
- 每个微服务仓库必须提交自己的
composer.lock,禁止共享或忽略 - CI 构建时固定执行
composer install --no-dev --prefer-dist --optimize-autoloader,跳过 dev 包,禁用自动 autoload 扫描 - 用
--classmap-authoritative强制 autoloader 只认 classmap,防止不同包同名类相互覆盖(比如两个包都定义了AppEventHandler) - 检查
composer check-platform-reqs输出,确保所有节点都有ext-redis或ext-amqp,缺一个扩展,事件监听进程就起不来
最易被忽略的是:本地开发用 path repo 临时引用私有包,上线前没切回真实仓库地址,导致 CI 拉不到 tag,composer install 直接报 Could not find package,整个事件链中断。











