首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python 批量交接示例:任务改组后,旧批次如何逐项返回冲突

Python 批量交接示例:任务改组后,旧批次如何逐项返回冲突

原创
作者头像
用户11635299
修改于 2026-10-19 21:57:04
修改于 2026-10-19 21:57:04
70
举报

配送员打开A批次后,其中一件任务被改到B批次。配送员再点击“整批接手”,旧页面里的那件任务应返回冲突;A批次其他仍有效的任务则可以按约定继续处理。仅返回一个“批次成功”会隐藏这类差异。

下面用Python和SQLite保留任务身份与页面快照。改组只修改任务的批次和版本,不复制任务;批量接手按任务执行短事务,并保存每项结果。它演示允许部分成功的策略,不模拟实物交接确认。

1. 页面快照与当前任务分开

数据

本例内容

作用

task

id、batch、version、state、owner

当前任务事实

snapshot

页面读到的任务编号与版本

表达本次操作基于哪个状态

operation

request_id与task_id联合键、参数和结果

按任务回放本次批量操作结果

freeze_batch返回(task_id, version)。这个列表在一轮操作中保持不变,不能为了让重试成功而在后台静默换成最新批次。改组会递增task.version,旧页面仍携带原版本;claim_one同时检查任务编号、批次、状态和版本,旧集合中的任务因此不能被误接。

2. 逐项事务与结果回放的实现

operation把请求号与任务编号作为联合主键。同一请求号重试某个任务时,还要核对批次、接手人和预期版本;输入不一致就拒绝,不能把另一位配送员的请求当成原请求回放。参数用稳定JSON保存,本例直接比较文本,便于阅读。

每个task的更新与operation结果在同一事务里。accepted与conflict都保存:重试返回原来的逐项结果,不会因为任务后来改回来就把旧冲突变成新接手。确认最新任务后,应使用新的操作标识表达新的业务意愿。

代码语言:python
复制
import json
import sqlite3
from contextlib import closing


def freeze_batch(db, batch):
    return db.execute(
        "SELECT id, version FROM task WHERE batch=? ORDER BY id", (batch,)
    ).fetchall()


def move(db, task_id, expected, new_batch):
    with db:
        return db.execute(
            "UPDATE task SET batch=?, version=version+1 "
            "WHERE id=? AND version=? AND state='ready'",
            (new_batch, task_id, expected)
        ).rowcount == 1


def claim_one(db, request_id, batch, actor, task_id, expected):
    # actor is supplied by a trusted session in a real service.
    if actor not in {101, 202}:
        raise PermissionError("actor cannot claim")
    payload = json.dumps([batch, actor, expected], separators=(",", ":"))
    with db:
        prior = db.execute(
            "SELECT payload, result FROM operation WHERE request_id=? AND task_id=?",
            (request_id, task_id)
        ).fetchone()
        if prior:
            if prior[0] != payload:
                raise ValueError("same operation key with another payload")
            return prior[1]
        changed = db.execute(
            "UPDATE task SET state='claimed', owner=?, version=version+1 "
            "WHERE id=? AND batch=? AND version=? AND state='ready'",
            (actor, task_id, batch, expected)
        ).rowcount
        result = "accepted" if changed else "conflict"
        db.execute("INSERT INTO operation VALUES (?, ?, ?, ?)",
                   (request_id, task_id, payload, result))
        return result


def claim_batch(db, request_id, batch, actor, snapshot):
    return [(task_id, claim_one(db, request_id, batch, actor, task_id, version))
            for task_id, version in snapshot]


def main():
    with closing(sqlite3.connect(":memory:")) as db:
        db.executescript("""
        CREATE TABLE task (id INTEGER PRIMARY KEY, batch TEXT,
          version INTEGER, state TEXT, owner INTEGER);
        CREATE TABLE operation (request_id TEXT, task_id INTEGER,
          payload TEXT, result TEXT, PRIMARY KEY(request_id, task_id));
        INSERT INTO task VALUES (1, 'A', 1, 'ready', NULL);
        INSERT INTO task VALUES (2, 'A', 1, 'ready', NULL);
        """)
        snapshot_a = freeze_batch(db, "A")
        assert move(db, 1, 1, "B")
        first = claim_batch(db, "req-A", "A", 101, snapshot_a)
        assert first == [(1, "conflict"), (2, "accepted")]
        assert claim_batch(db, "req-A", "A", 101, snapshot_a) == first
        assert db.execute("SELECT COUNT(*) FROM task").fetchone()[0] == 2
        assert db.execute("SELECT batch, owner FROM task WHERE id=1").fetchone() == ("B", None)
        snapshot_b = freeze_batch(db, "B")
        assert claim_batch(db, "req-B", "B", 202, snapshot_b) == [(1, "accepted")]
        assert not move(db, 2, 2, "C")
        try:
            claim_batch(db, "req-A", "A", 202, snapshot_a)
        except ValueError:
            pass
        else:
            raise AssertionError("payload conflict was accepted")
        print("batch A:", first)
        print("replay: identical item results")
        print("task count after regroup: 2")
        print("batch B: task 1 accepted by 202")
        print("claimed task regroup: rejected")


if __name__ == "__main__":
    main()

3. 用固定顺序复现过期批次

代码语言:txt
复制
python demo.py

本次使用Python 3.12.10、SQLite 3.49.1执行,得到:

代码语言:txt
复制
batch A: [(1, 'conflict'), (2, 'accepted')]
replay: identical item results
task count after regroup: 2
batch B: task 1 accepted by 202
claimed task regroup: rejected

代码先保存A批次快照,再把任务1移到B,然后执行旧A快照。任务1返回conflict,任务2返回accepted;重复req-A仍返回完全相同的列表。数据库中任务始终只有两行,改组没有产生第二份任务1。

随后读取B的新快照,使用req-B由202接手任务1。这是一次明确的新操作。任务2已经被101接手,即使给出它当前的版本,再尝试move也会因state不再是ready而失败。最后复用req-A但更换接手人,参数核对会将其拒绝。

4. 部分成功意味着怎样恢复

claim_batch在每项提交后才继续下一项。如果程序在任务2之前退出,任务1的事务不会被撤销;调用方需要保留完整快照与原请求号,再沿原列表重试。已处理项回放,未处理项首次执行。若调用方丢掉旧快照、重新查询后仍使用同一请求号,含义就发生了变化。

一个任务发生数据库异常会中断本次函数,调用方不能把“没有返回整批列表”解释成“整批都没提交”。如业务要求全成全败,应改为有界的整体事务,并重新评估锁竞争;不能在这份逐项提交实现上加一个总成功标签就获得原子性。

本地示例只用固定交错顺序验证过期状态,没有运行多线程竞争。真实服务还要把actor绑定到认证会话,核对校区、阶段、资格与接收确认;代码中的两个人员编号仅用于演示授权检查位置。请求结果保留期限也要覆盖实际重试窗口。

作者:微订(上海逊柯计算机科技有限公司)。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 1. 页面快照与当前任务分开
  • 2. 逐项事务与结果回放的实现
  • 3. 用固定顺序复现过期批次
  • 4. 部分成功意味着怎样恢复
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档