Files
zhuangang/topics/backend/mq/examples/delay-cancel/delay_cancel.py
T

85 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()))