本文是 Agent 工程里的经典架构 系列的一篇。文中的业务场景和代码为讲解设计,产品能力以所链接的官方资料为准。

采购员上午看到报价一百元,发起审批,下午经理同意了。真正提交订单时,报价已经变成一百二十元。如果程序只记得“审批通过”,就可能按过期条件执行。这里没有很高的 QPS,也会出现并发问题,因为现实世界在等待期间一直变化。

Optimistic Offline Lock处理的是跨多个系统事务的业务操作:不在用户思考期间一直占着锁,而是在提交时检测冲突。这里的 offline 指业务过程脱离单次数据库事务,不是说系统断网运行。它适合等待较长、冲突概率相对可控的场景。

我的理解是,审批应绑定具体条件,而不能只是一个布尔值。订单编号、金额上限、币种、版本和有效期都是可能的条件。批准的是哪份事实,最终执行是否仍满足这份事实,需要程序明确比较。一个“approved=true”很难独立表达这些约束。

机制图:把并发校验与最终写入放进同一原子操作。

下面用 SQLite 的条件更新演示。两个处理器都读到版本一,第一次改价成功,版本推进到二;第二次仍带着版本一提交,影响行数为零。校验和更新在同一条语句中完成,避免“先查版本、再普通更新”之间被其他请求插入。

import sqlite3

conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE quotes (id TEXT PRIMARY KEY, price INT, version INT)")
conn.execute("INSERT INTO quotes VALUES ('Q1', 10000, 1)")
conn.commit()


def change_price(quote_id, expected_version, new_price):
    with conn:
        changed = conn.execute(
            "UPDATE quotes SET price=?, version=version+1 "
            "WHERE id=? AND version=?",
            (new_price, quote_id, expected_version),
        ).rowcount
    return changed == 1


first_version = second_version = 1
assert change_price("Q1", first_version, 11000)
assert not change_price("Q1", second_version, 9000)
price, version = conn.execute(
    "SELECT price, version FROM quotes WHERE id='Q1'"
).fetchone()
assert (price, version) == (11000, 2)
approved_ceiling = 10500
assert price > approved_ceiling  # 需要重新审批
conn.close()
print("旧版本提交被拒绝,新价格超过原审批上限")

完整示例可下载,使用 Python 3.9 或以上版本运行:python3 example.py。只依赖标准库,不调用真实业务服务。

不能忽略 rowcount。SQL 没有抛异常,并不意味着业务更新成功。影响零行意味着记录不存在、条件不成立或版本已经变化,需要转成明确的业务结果。如果统一返回“操作成功”,乐观锁就只剩下一段无人使用的查询条件。

发生冲突后,也不能机械地把版本改成最新再重试。假设审批的上限是一百元,新报价已经超出上限,重新读取后需要重新判断,可能还要重新审批。技术上的重试要服从业务语义。对于同一申请,重试仍应保留操作编号,避免冲突处理过程中重复创建其他效果。

在高并发场景下,乐观方式减少了长时间等待锁的成本,但热点记录可能频繁冲突。一个热门库存项有大量同时写入时,重读和重试也会消耗资源。需要根据实际竞争情况考虑排队、分片、预留机制或更短的事务。乐观与悲观的选择是冲突成本的权衡,不是谁永远更先进。

还有一个容易误用的地方:在本地数据库校验报价版本,然后调用供应商下单,这两步之间报价仍然可能变化。本地乐观锁不能锁住远程系统。如果供应商支持带版本或报价单编号的条件下单,应让最终接受订单的一方原子地校验条件;否则需要业务上约定价格有效期、允许波动范围和异常处理办法。

Agent 的工作时长越长,这个问题越突出。它可能隔天恢复,也可能在审批期间部署过新代码。持久执行能记得昨天批准了什么,但不能证明今天依然可以执行。关键写入前的重新校验,应放在执行工具中,不依赖模型是否想到了“再检查一下”。

版本号还要有清楚的作用范围。如果审批只关心金额与币种,却把“最后查看时间”也计入同一版本,每次无关更新都会导致审批失效。可以按业务聚合设计版本,或者比较真正影响决策的字段,但不能为了降低冲突率忽略关键条件。

需要区分“记录曾经变化”和“当前值恰好一样”。价格从一百变成一百二,再变回一百,比较当前价格可能看不出变化,递增版本则能够识别这段历史。具体需要哪种判断,要看审批约定关心值本身,还是关心原始报价是否仍有效。

对于落地项目,我会把“读到的版本”“批准的约束”“提交的条件”放在一张表里核对。再设计一个故障用例:暂停任务,修改关键数据,恢复任务,看它是否正确拒绝旧决策。这个测试能够直接验证业务边界,比单纯把模型输出设得更保守更有说服力。

上一篇 · 系列目录 · 下一篇