重试不重复下单还不够:同一幂等键换了参数,应该返回什么?

AI SaaS 工程诊断实战(九)

请求超时了,客户端不知道服务端有没有创建订单,于是带同一个键再试。查到旧结果就返回,看起来已经实现幂等。

但第二次请求把数量从10改成20了呢?如果仍返回第一次的订单,调用方可能把它理解成"20已经保存";如果重新执行,同一个键又产生不同结果。

这篇用 Python 与 SQLite 的构造演示,把键的作用域、请求内容和本地事务放在一起检查。不访问真实业务,不发送外部请求,AI 辅助整理。它只验证单连接顺序调用,不是完整并发或分布式幂等方案。

键需要同时绑定范围和请求含义

本例使用 (tenant, req_key) 作为记录主键。不同租户即使碰巧用了 K1,也不应互相拿到结果。

每条记录保存本次 payload 的指纹和原始响应。同范围、同键、同指纹,返回已有响应;同键但指纹不同,明确冲突,不把新参数静默当成旧请求。

指纹计算前要先验证并规范业务输入。本例 payload 只有正整数 quantity,使用排序键的 JSON 编码计算 SHA-256。真实业务若有金额精度、默认值、字段别名或数组顺序,需要先定义什么算同一个请求,不能以为 sort_keys 会自动理解业务等价。

本地写入与幂等记录放在同一事务

只有业务记录、没有请求记录,重试仍可能重复执行。只有请求记录、业务却失败,也可能让后续一直拿到不存在的结果。

因此这个演示在一个 SQLite 事务里插入订单与请求记录,退出成功后才返回。通过教学故障开关,在订单插入后、请求记录插入前抛出异常,再直接查询行数,检查订单有没有被回滚。

Python sqlite3 的事务上下文说明可查 官方文档。本例没有把邮件、模型调用等外部副作用放进事务,也不声称数据库回滚能撤回外部动作。

完整演示

保存为 idempotency.py,Python 3.12 执行 python idempotency.py,仅使用标准库。tenant 在示例里由测试传入;生产服务必须从可信认证上下文取得,不能相信请求体自报身份。

python 复制代码
import json
import sqlite3
import hashlib


def fingerprint(payload):
    text = json.dumps(payload, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
    return hashlib.sha256(text.encode('utf-8')).hexdigest()


def submit(db, tenant, key, payload, fail=False):
    if set(payload) != {'quantity'} or type(payload['quantity']) is not int or payload['quantity'] <= 0:
        raise ValueError('invalid payload')
    digest = fingerprint(payload)
    with db:
        old = db.execute('SELECT digest,response FROM requests WHERE tenant=? AND req_key=?',
                         (tenant, key)).fetchone()
        if old:
            if old[0] != digest:
                raise ValueError('idempotency key conflict')
            return json.loads(old[1])
        cursor = db.execute('INSERT INTO orders(tenant,quantity) VALUES (?,?)',
                            (tenant, payload['quantity']))
        response = dict(order_id=cursor.lastrowid, quantity=payload['quantity'])
        if fail:
            raise RuntimeError('injected before request record')
        db.execute('INSERT INTO requests VALUES (?,?,?,?)',
                   (tenant, key, digest, json.dumps(response)))
    return response


if __name__ == '__main__':
    db = sqlite3.connect(':memory:')
    db.executescript('''
        CREATE TABLE orders(id INTEGER PRIMARY KEY, tenant TEXT, quantity INTEGER);
        CREATE TABLE requests(tenant TEXT,req_key TEXT,digest TEXT,response TEXT,
                              PRIMARY KEY(tenant,req_key));
    ''')
    first = submit(db, 'T1', 'K1', {'quantity': 10})
    retry = submit(db, 'T1', 'K1', {'quantity': 10})
    try:
        submit(db, 'T1', 'K1', {'quantity': 20})
        conflict = False
    except ValueError:
        conflict = True
    other_tenant = submit(db, 'T2', 'K1', {'quantity': 10})
    before = db.execute('SELECT COUNT(*) FROM orders').fetchone()[0]
    try:
        submit(db, 'T1', 'K2', {'quantity': 30}, fail=True)
    except RuntimeError:
        pass
    after = db.execute('SELECT COUNT(*) FROM orders').fetchone()[0]
    recovered = submit(db, 'T1', 'K2', {'quantity': 30})
    count = db.execute('SELECT COUNT(*) FROM orders').fetchone()[0]
    assert first == retry and conflict
    assert first['order_id'] != other_tenant['order_id']
    assert before == after == 2 and count == 3
    print(json.dumps(dict(first=first, retry=retry, conflict=conflict,
                         other_tenant=other_tenant, rollback_count=after,
                         recovered=recovered, total_orders=count), indent=2))
    db.close()

实际运行结果:第一次和同参数重试返回同一个订单;改数量被拒绝;另一个租户同用 K1 创建自己的订单;故障注入前后订单数仍为2;之后重试 K2 成功,最终共3条。

这里还没回答的几个问题

第一是并发。两个工作进程同时查不到记录时怎样处理?唯一约束、事务隔离、锁等待、冲突后的重新读取都需要按实际数据库设计并执行检查。当前顺序样例通过,不能推出并发安全。

第二是保留期。键过期以后可否重新执行,旧客户端重试会怎样,需要业务决定。本例没有实现过期清理。

第三是外部副作用。如果订单创建后还要向外部系统发送动作,需要可靠投递和对方去重等额外设计,不能把本例称作端到端 exactly-once。

第四是响应版本。已有响应是否仍能被新客户端理解,也需要协议管理,不能静默把旧响应改成另一次结果。

一组适合写进验收的输入

同租户、同键、同参数;同租户、同键、不同参数;不同租户、同键;业务写入与请求登记之间失败;记录有效期边界;多连接同时首次提交。

本篇实际执行的是前四类中的本地顺序路径,后两类是需要继续补测的方向。把已跑和待跑分开,才能知道当前补丁支持哪种结论。

------ Linksass|AI SaaS 独立开发者

相关推荐
用户019027581611 小时前
如何用 Python 计算 CR 能量指标(中间意愿指标)?
python
曲径海盗1 小时前
【Python 学习第 34 天】条件判断 if/else
python·学习笔记·编程入门
五网1 小时前
用Python搭建创业项目财务测算模型:CAC、LTV、盈亏平衡点一键算出(附完整代码)
python
用户6602676118791 小时前
登录成功就能读取任意任务 ID 吗?给 SaaS 读写加上租户范围
python
用户7624117567051 小时前
第十五章 数据分析
python·数据分析
2601_961963382 小时前
电子合同检索与全文搜索怎么测、怎么选:六个指标 + 五家平台对比 + 八个自测问题
数据库·后端·python·django·智能合约
SEO_juper2 小时前
hreflang 审计工具:外贸多语言站谷歌SEO的隐性杀手
开发语言·人工智能·爬虫·python·seo
打工仔折腾 AI2 小时前
从零开始大模型微调:基于 PyTorch 与 ChatGLM 的 LoRA 实践指南
人工智能·pytorch·后端·python·深度学习·性能优化·ai agent 实战
用户0332126663672 小时前
Python 自动化办公:如何从零快速生成结构清晰的 Excel 报表
python·excel