影刀RPA店群自动化教程:Python动态优先级队列与浏览器资源抢占实战

在这里插入图片描述


店群矩阵自动化突破运营极限!


在这里插入图片描述

当“上货”和“回复客户”同时等待执行时,系统必须知道谁先谁后。

否则,一个无关紧要的数据采集任务,可能卡住一条差评回复整整十分钟。

店群运营中,任务的紧急程度天然不同。
客服回复慢一分钟,可能多一条差评;大促活动的报名晚十分钟,可能直接错过入口。
但常规的定时数据采集早五分钟晚五分钟,几乎没有影响。
在这里插入图片描述

temu店群自动化报活动案例

然而在早期架构中,我们所有任务共用一套FIFO队列。
先提交的先执行,完全不管业务含义。
这导致了多次紧急任务被普通任务长时间阻塞的事故。

我们后来设计了一套动态优先级队列,并结合浏览器资源抢占机制,让高优任务能在几秒内拿到执行权。
这篇文章就完整复盘这套机制的工程实践。
在这里插入图片描述


一、任务优先级的定义:不是简单的高中低

最初我们给每个任务类型设了一个固定优先级,写死在配置文件里:

在这里插入图片描述
在这里插入图片描述

task_priority:
  reply_customer: 10    # 最高
    campaign_signup: 9
      upload_product: 5
        collect_data: 3       # 最低
        ```
但很快就发现问题:同样是 `reply_customer`,回复一个刚刚投诉的客户和回复一个三天前的咨询,紧急程度完全不同。  
纯静态优先级无法体现任务内部的差异。

我们改进为**基础优先级 + 动态加权**的模式。  
最终优先级分数由多个因子加权计算:

```python
def calculate_priority(task):
    base = PRIORITY_BASE.get(task.type, 5)
        # 等待时间加权:每等待1分钟加0.5分
            wait_minutes = (time.time() - task.created_at) / 60
                wait_bonus = wait_minutes * 0.5
                    # 业务紧急系数:如客服消息的剩余回复时间
                        urgency = task.urgency_factor or 0
                            # 失败重试加权:失败次数越多,优先级适当提升,避免饥饿
                                retry_bonus = task.retry_count * 0.3
                                    
                                        return base + wait_bonus + urgency + retry_bonus
                                        ```
随着等待时间增加,低优先级任务的分数会逐步上升,避免永远得不到执行。  
同时,高紧急系数(如差评回复窗口仅剩2分钟)能瞬间把任务推上队首。

---

## 二、基于Redis Sorted Set的优先级队列

优先级队列的实现选用了Redis的Sorted Set,成员为任务序列化字符串,分数即为优先级。

```python
import json
import time
import asyncio

class PriorityTaskQueue:
    def __init__(self, redis_client, queue_key="task:priority_queue"):
            self.redis = redis_client
                    self.queue_key = queue_key
    async def enqueue(self, task: dict):
            score = self._calculate_score(task)
                    # 使用 task_id + 微秒时间戳 保证成员唯一
                            member = json.dumps(task)
                                    await self.redis.zadd(self.queue_key, {member: score})
    def _calculate_score(self, task: dict) -> float:
            base = task.get("base_priority", 5)
                    wait_sec = time.time() - task.get("created_at", time.time())
                            wait_bonus = wait_sec / 120 * 0.5  # 每2分钟加0.5
                                    urgency = task.get("urgency", 0)
                                            retry_bonus = task.get("retry_count", 0) * 0.3
                                                    return base + wait_bonus + urgency + retry_bonus
    async def dequeue(self) -> dict:
            # 原子操作:取出最高优先级(分数最大)的任务
                    result = await self.redis.zpopmax(self.queue_key, count=1)
                            if result:
                                        return json.loads(result[0][0])
                                                return None
    async def peek_high_priority(self, threshold: float):
            # 查看分数高于阈值的任务,不取出
                    return await self.redis.zrangebyscore(
                                self.queue_key, min=threshold, max='+inf', withscores=True
                                        )
                                        ```
每次入队时重新计算优先级分数并更新Sorted Set。  
出队时使用 `ZPOPMAX`,保证取到的永远是当前分数最高的任务。

---

## 三、资源抢占:高优任务不能等

有了优先级队列,高优任务可以快速排到队首。  
但如果当前所有Worker的浏览器实例都被低优任务占满,高优任务仍然要等。

**真正的紧急任务,不应该排队。**

我们引入了浏览器资源抢占机制。  
高优任务(优先级分数超过某阈值)在入队时,会触发一个抢占检查:

1. 确定任务目标店铺的浏览器实例是否被占用
2. 2. 如果占用者是一个低优先级任务,且该任务处于可安全挂起的阶段(如页面加载中、执行非关键步骤),则强制挂起低优先级任务
3. 3. 低优先级任务状态保存到Redis,浏览器实例被释放并分配给高优任务
4. 4. 高优任务完成后,被挂起的低优任务恢复执行
```python
class ResourcePreemptor:
    PREEMPT_SCORE_THRESHOLD = 8.0  # 超过此分值的任务具备抢占资格
    def __init__(self, browser_pool, task_state_store):
            self.browser_pool = browser_pool
                    self.state_store = task_state_store
    async def try_preempt(self, shop_id: str, incoming_task: dict) -> bool:
            if incoming_task.get("priority_score", 0) < self.PREEMPT_SCORE_THRESHOLD:
                        return False
        current_holder = self.browser_pool.get_holder(shop_id)
                if current_holder is None:
                            return False  # 资源空闲,无需抢占
        holder_score = current_holder.get("priority_score", 0)
                if holder_score >= incoming_task["priority_score"]:
                            return False  # 当前任务优先级不低,不抢占
        if not current_holder.get("preemptible", False):
                    return False  # 当前任务处于不可中断的临界区
        # 执行抢占
                await self._suspend_task(current_holder)
                        self.browser_pool.force_release(shop_id)
                                logger.info(f"Preempted shop {shop_id} from task {current_holder['task_id']}")
                                        return True
    async def _suspend_task(self, task_info: dict):
            # 保存任务上下文:当前步骤、已执行指令索引、数据上下文
                    await self.state_store.save(task_info["task_id"], task_info["context"])
                            # 通知影刀流程暂停(通过gRPC信号)
                                    await self._send_pause_signal(task_info["task_id"])
                                    ```
低优先级任务被挂起时,会保存当前执行进度(如已完成的步骤序号、页面状态等)。  
被挂起的任务会在优先级老化或被抢占任务完成后重新恢复。

---

## 四、抢占的安全边界:不是所有任务都能打断

抢占有风险。如果低优任务正在执行写操作(如提交表单、支付),强制中断可能导致脏数据。

我们为影刀指令序列中的每个步骤定义了 `interruptible` 属性。

```json
{
  "steps": [
      {"action": "navigate", "interruptible": true},
          {"action": "type_text", "interruptible": true},
              {"action": "upload_file", "interruptible": true},
                  {"action": "click", "locator": "#submit-btn", "interruptible": false},
                      {"action": "wait_element", "locator": ".success-msg", "interruptible": true}
                        ]
                        }
                        ```
`interruptible: false` 的步骤是不可抢占的临界区。  
抢占检查时,如果当前执行步骤标记为不可中断,拒绝抢占。

这保证了“提交订单”、“确认支付”等关键操作不会被中途打断。

---

## 五、优先级老化与饥饿避免

单纯依赖优先级可能导致低优先级任务长期被“饿死”。  
虽然我们在计算分数时加入了等待时间加权,但还需要一个硬性兜底:

当某个任务在队列中等待超过30分钟,无论其优先级多低,都会被强制提升至可执行队列的前端,并标记为“不可被抢占”。

```python
async def aging_check_loop(self):
    while True:
            # 查找等待超过30分钟的任务
                    cutoff = time.time() - 1800
                            aged_tasks = await self.redis.zrangebyscore(
                                        self.queue_key, min='-inf', max='+inf', withscores=True
                                                )
                                                        for member, score in aged_tasks:
                                                                    task = json.loads(member)
                                                                                if task.get("created_at", 0) < cutoff:
                                                                                                # 提升优先级至最高,并标记不可抢占
                                                                                                                task["priority_boosted"] = True
                                                                                                                                task["preemptible"] = False
                                                                                                                                                new_score = 100.0
                                                                                                                                                                await self.redis.zadd(self.queue_key, {json.dumps(task): new_score})
                                                                                                                                                                        await asyncio.sleep(60)
                                                                                                                                                                        ```
这种机制保证了长时间等待的任务最终一定能获得执行机会,避免因业务优先级导致运维事故。

---

## 六、跨平台差异处理

不同平台的任务,紧急程度判定逻辑不同。

- 拼多多客服回复有48小时窗口,但差评回复窗口短
- - TEMU上货任务在审核期结束前1小时,紧急度骤升
- - TikTok Shop直播中商品讲解需要在特定时段完成
我们为每个平台实现了一个 `UrgencyEstimator` 插件:

```python
class UrgencyEstimator:
    def estimate(self, task: dict) -> float:
            platform = task.get("platform")
                    if platform == "pdd":
                                return self._pdd_urgency(task)
                                        elif platform == "temu":
                                                    return self._temu_urgency(task)
                                                            elif platform == "tiktok":
                                                                        return self._tiktok_urgency(task)
                                                                                return 0.0
    def _pdd_urgency(self, task):
            if task["type"] == "reply_customer":
                        remaining_sec = task.get("deadline") - time.time()
                                    if remaining_sec < 3600:  # 1小时内
                                                    return 5.0
                                                            return 0.0
                                                            ```
这样新的平台加入时,只需实现对应的紧急度评估逻辑,优先级系统保持通用。

---

## 七、监控与告警

优先级队列和抢占机制需要被监控,否则出了“饿死”或“过度抢占”的问题难以发现。

关键指标:
- 队列中最高优先级分数与最低优先级分数的差距
- - 过去1小时内抢占次数
- - 等待超过30分钟的任务数量
- - 被抢占任务的恢复成功率
Grafana看板上,这些指标与常规任务执行成功率并列展示。  
一旦抢占次数激增,说明系统中有大量紧急任务,可能意味着上游运营策略出了问题。

---

## 八、踩坑实录

**分数精度问题。**  
Redis Sorted Set使用双精度浮点数存储分数。当等待时间加权导致分数极接近时,可能无法区分优先级。  
我们将分数计算基值设为整数,加权部分放在小数部分,并用足够精度,保证了区分度。

**抢占后恢复失败。**  
被挂起的低优任务在恢复时,偶尔会因为浏览器页面状态发生变化而导致元素定位失败。  
我们在恢复时加入了一个“页面重置”步骤:回到任务初始页面,重新执行已完成的非写操作步骤,跳过已完成的写操作步骤。  
这依赖于之前实现的指令序列化和幂等设计。

**抢占风暴。**  
有一次运营批量创建了大量紧急任务,系统在短时间内发生了数十次抢占,导致整体吞吐量反而下降。  
我们加了硬性限制:每个Worker每分钟最多允许2次抢占。超出则拒绝新的抢占请求,改为排队等待。

---

## 九、写在最后

自动化系统的任务调度,不是简单的“先来后到”。

当业务有轻重缓急之分时,技术架构必须能够识别、响应、保障。  
动态优先级和资源抢占,正是把“业务意识”注入到调度系统的一种方式。

> 自动化的目标不是做所有的事,而是在正确的时间,做最正确的事。
---

*作者:林焱*
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐