配送员打开A批次后,其中一件任务被改到B批次。配送员再点击“整批接手”,旧页面里的那件任务应返回冲突;A批次其他仍有效的任务则可以按约定继续处理。仅返回一个“批次成功”会隐藏这类差异。
下面用Python和SQLite保留任务身份与页面快照。改组只修改任务的批次和版本,不复制任务;批量接手按任务执行短事务,并保存每项结果。它演示允许部分成功的策略,不模拟实物交接确认。
数据 | 本例内容 | 作用 |
|---|---|---|
task | id、batch、version、state、owner | 当前任务事实 |
snapshot | 页面读到的任务编号与版本 | 表达本次操作基于哪个状态 |
operation | request_id与task_id联合键、参数和结果 | 按任务回放本次批量操作结果 |
freeze_batch返回(task_id, version)。这个列表在一轮操作中保持不变,不能为了让重试成功而在后台静默换成最新批次。改组会递增task.version,旧页面仍携带原版本;claim_one同时检查任务编号、批次、状态和版本,旧集合中的任务因此不能被误接。
operation把请求号与任务编号作为联合主键。同一请求号重试某个任务时,还要核对批次、接手人和预期版本;输入不一致就拒绝,不能把另一位配送员的请求当成原请求回放。参数用稳定JSON保存,本例直接比较文本,便于阅读。
每个task的更新与operation结果在同一事务里。accepted与conflict都保存:重试返回原来的逐项结果,不会因为任务后来改回来就把旧冲突变成新接手。确认最新任务后,应使用新的操作标识表达新的业务意愿。
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()python demo.py本次使用Python 3.12.10、SQLite 3.49.1执行,得到:
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但更换接手人,参数核对会将其拒绝。
claim_batch在每项提交后才继续下一项。如果程序在任务2之前退出,任务1的事务不会被撤销;调用方需要保留完整快照与原请求号,再沿原列表重试。已处理项回放,未处理项首次执行。若调用方丢掉旧快照、重新查询后仍使用同一请求号,含义就发生了变化。
一个任务发生数据库异常会中断本次函数,调用方不能把“没有返回整批列表”解释成“整批都没提交”。如业务要求全成全败,应改为有界的整体事务,并重新评估锁竞争;不能在这份逐项提交实现上加一个总成功标签就获得原子性。
本地示例只用固定交错顺序验证过期状态,没有运行多线程竞争。真实服务还要把actor绑定到认证会话,核对校区、阶段、资格与接收确认;代码中的两个人员编号仅用于演示授权检查位置。请求结果保留期限也要覆盖实际重试窗口。
作者:微订(上海逊柯计算机科技有限公司)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。