# requires: aio_pika """RabbitMQ 延迟取消订单:消息先进带 TTL 的等待队列,过期后经死信交换机转到处理队列。 运行:AMQP_URL=amqp://guest:guest@localhost/ python delay_cancel.py 没有设置 AMQP_URL 时打印提示并正常退出。会创建并删除名字带随机后缀的交换机和队列。 """ import asyncio import json import os import sys import time import uuid import aio_pika DELAY_MS = 1000 # 演示用 1 秒;真实业务常见是 15~30 分钟 class OrderStore: """模拟数据库里的订单状态(真实项目里用条件 UPDATE 保证幂等)。""" def __init__(self) -> None: self.status: dict[str, str] = {} def cancel_if_unpaid(self, order_id: str) -> bool: # 等价于 UPDATE orders SET status='cancelled' WHERE id=? AND status='pending' if self.status.get(order_id) == "pending": self.status[order_id] = "cancelled" return True return False async def main() -> int: url = os.environ.get("AMQP_URL") if not url: print("未设置 AMQP_URL,跳过(示例:amqp://guest:guest@localhost/)") return 0 suffix = uuid.uuid4().hex[:8] conn = await aio_pika.connect_robust(url) async with conn: ch = await conn.channel() await ch.set_qos(prefetch_count=10) dlx = await ch.declare_exchange(f"order.dlx.{suffix}", aio_pika.ExchangeType.DIRECT, auto_delete=True) # 处理队列:真正执行“超时取消”的消费者监听这里 work_q = await ch.declare_queue(f"order.cancel.{suffix}", durable=True, auto_delete=True) await work_q.bind(dlx, routing_key="cancel") # 等待队列:没有消费者,消息在这里过期后转到死信交换机 wait_q = await ch.declare_queue(f"order.wait.{suffix}", durable=True, auto_delete=True, arguments={ "x-message-ttl": DELAY_MS, "x-dead-letter-exchange": dlx.name, "x-dead-letter-routing-key": "cancel", }) store = OrderStore() store.status = {"o1": "pending", "o2": "pending"} start = time.monotonic() for oid in ("o1", "o2", "o1"): # o1 故意发两次,模拟重复消息 await ch.default_exchange.publish( aio_pika.Message(json.dumps({"order_id": oid}).encode(), delivery_mode=aio_pika.DeliveryMode.PERSISTENT, message_id=f"cancel-{oid}"), routing_key=wait_q.name) store.status["o2"] = "paid" # o2 在超时前支付了 results: list[tuple[str, bool]] = [] async with work_q.iterator() as it: async for msg in it: async with msg.process(): # 正常结束自动 ack,抛异常则 nack oid = json.loads(msg.body)["order_id"] results.append((oid, store.cancel_if_unpaid(oid))) if len(results) == 3: break elapsed = time.monotonic() - start print("处理结果:", results, f"耗时 {elapsed:.1f}s") assert elapsed >= DELAY_MS / 1000 * 0.9, "消息应该在延迟之后才被处理" assert store.status == {"o1": "cancelled", "o2": "paid"}, store.status assert sorted(results) == [("o1", False), ("o1", True), ("o2", False)] # 重复消息不会重复取消 await wait_q.delete(if_unused=False, if_empty=False) await work_q.delete(if_unused=False, if_empty=False) print("延迟取消正确:未支付的被取消,已支付的不受影响,重复消息幂等") return 0 if __name__ == "__main__": sys.exit(asyncio.run(main()))