做煤炉自动代拍这块业务,技术上最有挑战的地方,我觉得是队列和超时控制。不像普通的电商下单,点一下就完了。代拍是实时竞价,晚个几百毫秒可能就被别人抢走了。但又不能无脑并发,不然系统容易崩。
为什么需要队列
煤炉秒切的场景,大家应该都不陌生。商品刚上架,价格很低,一堆人盯着抢。用户在我们的系统里设置好自动出价,一旦触发条件,系统就得立刻去下单。
如果请求来了就直接处理,会有几个问题:
高峰期请求太多,直接把煤炉的 API 打爆,或者把我们自己的服务压垮
同一个商品可能有多个用户出价,需要按顺序处理,不然会乱
有些请求处理时间长,会阻塞后面的请求
所以必须用队列来削峰填谷,把请求攒起来,慢慢处理。
队列选型
最开始我们用的是 Redis 的 List 结构,简单粗暴。左边进右边出,够用。但后来业务量上来了,发现几个问题:
没有优先级,紧急的代拍请求跟普通的混在一起
消息丢了就丢了,没有确认机制
不支持延迟消息,有些场景需要定时出价
后来换成了 RabbitMQ,这些问题就都解决了。
python
import pika
import json
class BidQueue:
def __init__(self):
self.connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost')
)
self.channel = self.connection.channel()
# 普通代拍队列
self.channel.queue_declare(queue='bid_normal', durable=True)
# 紧急代拍队列(优先级高)
self.channel.queue_declare(queue='bid_urgent', durable=True)
# 死信队列(处理失败的消息)
self.channel.queue_declare(queue='bid_dead_letter', durable=True)
def publish_bid(self, bid_data: dict, urgent: bool = False):
queue = 'bid_urgent' if urgent else 'bid_normal'
self.channel.basic_publish(
exchange='',
routing_key=queue,
body=json.dumps(bid_data),
properties=pika.BasicProperties(
delivery_mode=2, # 持久化
priority=10 if urgent else 5
)
)
def consume_bids(self, callback):
# 消费普通队列
self.channel.basic_qos(prefetch_count=1)
self.channel.basic_consume(
queue='bid_normal',
on_message_callback=callback,
auto_ack=False
)
# 消费紧急队列
self.channel.basic_consume(
queue='bid_urgent',
on_message_callback=callback,
auto_ack=False
)
self.channel.start_consuming()
这里有几个细节。一个是 durable 和 delivery_mode,消息持久化。RabbitMQ 挂了重启,消息不会丢。另一个是 prefetch_count=1,意思是消费者一次只拿一条消息,处理完了再拿下一条。这样不会出现某个消费者堆了一堆消息没处理,其他消费者闲着的情况。
超时控制
代拍请求不能无限期地等下去。煤炉的商品可能很快就被别人拍走了,等太久再去拍也没意义。所以每个代拍请求都要有超时时间。
我们的超时控制分两层:队列等待超时和处理超时。
队列等待超时,就是消息在队列里待了太久还没被消费,就直接丢弃。这个可以用 RabbitMQ 的消息 TTL 来做。
处理超时,就是消费者拿到消息开始处理了,但处理时间太长,也要有个上限。
python
import signal
from functools import wraps
def timeout(seconds):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
def handler(signum, frame):
raise TimeoutError(f"函数执行超时: {seconds}秒")
old_handler = signal.signal(signal.SIGALRM, handler)
signal.alarm(seconds)
try:
result = func(*args, **kwargs)
finally:
signal.alarm(0)
signal.signal(signal.SIGALRM, old_handler)
return result
return wrapper
return decorator
@timeout(10) # 10秒超时
def process_bid(bid_data):
# 实际的代拍逻辑
adapter = get_adapter(bid_data['platform'])
result = adapter.place_bid(
bid_data['item_id'],
bid_data['price']
)
return result
用 signal 来做超时控制,简单直接。但要注意,这个方法在多线程环境下不太好用,因为 signal 是进程级别的。如果是多线程的话,可以用 concurrent.futures 来做。
python
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeout
executor = ThreadPoolExecutor(max_workers=10)
def process_bid_with_timeout(bid_data, timeout=10):
future = executor.submit(process_bid, bid_data)
try:
return future.result(timeout=timeout)
except FutureTimeout:
future.cancel()
raise TimeoutError(f"代拍处理超时: {timeout}秒")
重试机制
代拍失败了,要不要重试?这个得看情况。
如果是网络抖动导致的失败,可以重试。但如果是商品已经被拍走了、价格超过了用户的预算,重试也没用。
我们的策略是:区分错误类型,可重试的错误才重试,而且要有退避策略。
python
import time
import random
def is_retryable_error(error: Exception) -> bool:
# 网络错误、超时、5xx错误,可以重试
retryable_errors = [
ConnectionError,
TimeoutError,
Server5xxError,
RateLimitError,
]
return any(isinstance(error, e) for e in retryable_errors)
def process_bid_with_retry(bid_data, max_retries=3):
last_error = None
for attempt in range(max_retries + 1):
try:
return process_bid(bid_data)
except Exception as e:
last_error = e
if not is_retryable_error(e):
# 不可重试的错误,直接抛出去
raise
if attempt < max_retries:
# 指数退避 + 随机抖动
wait_time = (2 ** attempt) + random.uniform(0, 1)
time.sleep(wait_time)
raise last_error
指数退避加随机抖动,这个是标准做法。退避是为了不给服务端太大压力,抖动是为了避免多个请求同时重试,造成 "惊群效应"。
并发控制
煤炉的 API 有调用频率限制,我们自己的服务也有处理能力上限。所以消费者的并发数不能太高。
我们用的是信号量来控制并发。
python
import threading
# 最多同时处理5个代拍请求
semaphore = threading.Semaphore(5)
def safe_process_bid(bid_data):
with semaphore:
return process_bid_with_retry(bid_data)
这个数字怎么定的?不是拍脑袋来的。我们做过压测,也观察了煤炉那边的限流阈值,慢慢调出来的。太高了容易被限流,太低了处理不过来,用户体验差。
还有一点,不同平台的并发数要分开控制。比如雅虎代拍的限制和煤炉的不一样,不能共用一个信号量。
监控与告警
队列系统跑起来之后,监控很重要。我们主要监控这几个指标:
队列长度:堆积了多少消息
消费速度:每秒处理多少条
失败率:处理失败的比例
平均处理时间:一条消息处理多久
这些指标都接到 Prometheus 和 Grafana 上,设好告警阈值。比如队列长度超过 1000 了,或者失败率超过 5% 了,就发告警给运维。
有一次晚上,煤炉那边的 API 出问题了,响应特别慢。我们的队列很快就堆积了,告警及时发出来,运维同学起来处理,没造成太大影响。要是没有监控,等用户投诉了才发现,那就晚了。
小结
队列和超时控制,说起来都是基础技术,但真要做好,细节特别多。从选型到实现,从重试到监控,每一步都得想清楚。
我们做 bidfans 系统的时候,光煤炉自动代拍这块的队列,就优化了好几个版本。从最开始的简单 Redis 队列,到现在的 RabbitMQ + 优先级 + 死信 + 重试 + 监控,中间踩了不少坑。
不过也正是这些细节,决定了系统的稳定性。用户设置了自动出价,系统就得靠谱地帮他拍到。做日本代购、日淘这些业务,用户信任是最重要的。
如果你也在做类似的系统,或者对雅虎代拍、煤炉秒切的技术实现感兴趣,欢迎在评论区交流。