煤炉自动代拍系统的队列设计与超时控制机制

做煤炉自动代拍这块业务,技术上最有挑战的地方,我觉得是队列和超时控制。不像普通的电商下单,点一下就完了。代拍是实时竞价,晚个几百毫秒可能就被别人抢走了。但又不能无脑并发,不然系统容易崩。

为什么需要队列

煤炉秒切的场景,大家应该都不陌生。商品刚上架,价格很低,一堆人盯着抢。用户在我们的系统里设置好自动出价,一旦触发条件,系统就得立刻去下单。

如果请求来了就直接处理,会有几个问题:

高峰期请求太多,直接把煤炉的 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 + 优先级 + 死信 + 重试 + 监控,中间踩了不少坑。

不过也正是这些细节,决定了系统的稳定性。用户设置了自动出价,系统就得靠谱地帮他拍到。做日本代购、日淘这些业务,用户信任是最重要的。

如果你也在做类似的系统,或者对雅虎代拍、煤炉秒切的技术实现感兴趣,欢迎在评论区交流。

相关推荐
一支绝命钩1 小时前
FPGA工程Git常用操作手册
git
fthux3 小时前
“装闭”,让装修套路“装”不下去
人工智能·ai·开源·github·open source
不搞学术柒柒5 小时前
Git新功能完整开发提交流程
git
用户84913717547166 小时前
想做护眼工具却脑子一片空白?我用 OpenSpec 把模糊想法聊成了 v0.1
github·vibecoding
wangruofeng7 小时前
git-filter-repo 把 .git 从 112MB 砍到 1.4MB,但漏推 tag 让 clone 又胖回来
github·devops
峰向AI8 小时前
Block 放出大招!Buzz:一个中继统一代码、聊天、CI 全流程
github
dong_junshuai8 小时前
每天一个开源项目#47 4.4K Stars 的 LikeC4:让架构图随代码进化
github
午安~婉10 小时前
Git中SSH连接
前端·git·gitee
888CC++10 小时前
VS Code Git 工作树:解锁多分支并行开发新体验
git