开始前,会这些就够了
- 了解请求与响应
- 知道数据库能保存记录
遇到不熟悉的词,先看这 6 个解释
- HTTP 状态码
- 响应里的三位数字,用来说明这次请求处理到了哪种结果,例如成功、需要重定向或发生错误;程序会结合响应内容决定下一步怎样处理。
- 接口
- 程序对外提供功能时,约定怎样调用、传哪些信息、返回什么结果;调用方按这份约定使用功能,不必先了解服务内部每一行实现代码。
- 幂等
- 在约定的范围内,把同一个请求做一次或重复做多次,对目标业务产生的预期效果相同;它用来防止重复执行把一件事变成额外多办几次。
- 超时
- 调用方为一次等待设置上限,超过时间还没拿到所需结果,就结束这次等待并报告超时;这样能限制资源占用,但并没有自动确认对方最终做到了哪一步。
- 重试
- 遇到可能只是暂时性的失败后,按预定规则再次尝试同一项操作;规则通常限制次数、等待间隔和总耗时,也要考虑重复执行会不会多做一次业务。
- 事务
- 把一组数据库操作放进同一个提交范围,让它们作为一件事被确认或撤销,并按数据库规则控制其他事务看到的变化;它帮助处理做到一半时的失败。
假设一个练习商城已经保存订单,返回响应时网络却断了。页面只看到超时,于是重新提交。第二次请求也创建订单,用户最终看到两张。这个假设案例的关键不是网速慢,而是客户端无法仅凭超时知道服务器做到了哪一步。
超时可能发生在请求尚未到达、执行过程中,也可能发生在提交之后。把所有超时都当作失败重新开始,会把“再问一次结果”变成“再做一次购买”。
给同一次购买一个固定名字
幂等的意思是:按约定重复同一个操作,不再增加重复的业务效果。客户端可在第一次购买时生成请求标识,后续重试继续使用它。服务端用用户身份和这个标识查找已经处理的结果;用户真的想再买一杯时,则发起新的业务操作,使用新的标识。
不要只拿商品和数量判断重复。一个人完全可能连续买两份相同商品。也不要每次重试都生成新标识,否则服务端看见的全是新操作。同一个标识却换了商品或金额,应当拒绝或按明确规则处理,不能悄悄复用另一笔订单。
订单和去重记录要一起落地
如果先创建订单,随后才保存“此请求处理过”,两步之间崩溃仍会重复。反过来先标记完成,订单却没保存,也可能永远漏单。因此要把相关数据库变更放进同一事务,利用唯一约束挡住相同标识的重复记录,并明确处理中、完成和失败如何查询。
下面用 SQLite 内存数据库演示最小范围:单进程、单连接,只创建本地订单,不扣款也不发消息。第一次调用的结果故意不显示,模拟调用者没看到响应;相同标识再次调用时,得到相同订单。唯一约束负责记录层面的去重,事务负责把本次修改一起提交或撤回。
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, request_key TEXT UNIQUE NOT NULL, item TEXT NOT NULL)")
def create_order(key, item):
with db:
db.execute("INSERT INTO orders(request_key, item) VALUES (?, ?) ON CONFLICT(request_key) DO NOTHING", (key, item))
row = db.execute("SELECT id, item FROM orders WHERE request_key = ?", (key,)).fetchone()
if row[1] != item:
raise ValueError("same key, different item")
return row
create_order("purchase-1", "tea") # 调用者没有显示第一次结果
print(create_order("purchase-1", "tea"))
print(db.execute("SELECT COUNT(*) FROM orders").fetchone()[0])
db.close()
怎样运行:Python 3.10+ 标准库;SQLite 需为 3.24.0+。保存为 retry_demo.py,运行 python retry_demo.py。
运行后对照结果
(1, 'tea')
1
本地去重不能代替整个支付流程
真实订单还可能调用支付、发库存消息或发送通知。本地订单表只有一行,并不能证明外部扣款只有一次。每个会产生副作用的环节都要说明如何识别重复、如何查询不确定结果,以及失败后如何继续;需要时采用持久化任务记录等方式连接这些步骤。
去重记录也不是随便保留几分钟就够了。迟到的重试可能在记录清理后到达,被认成新请求。Stripe 的文档就明确描述了键的保存期、参数核对和保存响应的规则。这是该产品的接口约定,不能直接当作所有服务共有的保证。
能安全重试,还要有停止条件
即使不会重复下单,无限制重试仍会占连接、线程和服务器容量。应设总时间预算与次数,遇到明确的参数错误就修正请求;对暂时性故障采用适当间隔。重试结束仍不知道结果时,把状态显示为待确认,并提供查询路径,比武断显示“肯定没买成”更准确。
排查时把业务请求标识与订单号一起记录,重建第一次执行和后续重试的关系。这里的重点是让不确定结果可查,而不是承诺网络永不出错。
记住这三点
- 超时只说明调用者没有及时得到结果。
- 同一次购买的重试应使用同一标识,新的购买使用新标识。
- 数据库去重、外部副作用和重试预算是不同问题。
别把例子用过头
- 内存 SQLite 演示不包含多实例并发、持久化恢复或支付事务。
- 幂等不保证每次尝试都会成功,也不表示可以无限重试。
- 请求标识需要限定用户等作用范围,不要写入敏感个人信息。
自己试一小步
同一个请求标识先买茶,第二次改成咖啡,系统应该如何响应?
- 运行例子,再修改第二次调用的商品。
- 为练习画出请求发出、数据库提交、响应收到三个时刻。
- 分别在三个时刻之间标记断网,写出调用者知道和不知道的事实。
做到这些,就算完成
- 没有把商品相同当作同一购买的唯一依据。
- 能区分参数冲突、执行失败和执行结果未知。
- 说明外部扣款是否属于当前实验覆盖范围。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Making retries safe with idempotent APIs (新标签页)↗
支持超时的不确定结果、调用者请求标识、原子记录、迟到请求和参数冲突。
- Idempotent requests (新标签页)↗
支持该产品保存状态和响应、相同键参数核对、保存期与再次使用规则。
- UPSERT (新标签页)↗
支持 ON CONFLICT 唯一性冲突与 DO NOTHING 行为;文档明确 UPSERT 从 3.24.0 加入。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
Dubbo 中文:一次调用能等多久、失败后重试几次 (新标签页)↗
只读集群容错中的 Failover 和 Failfast,圈出哪些调用会重试,再检查写入接口是否允许重复执行。
查看学习准备与访问说明 →AWS:重试请求怎样避免重复办事 (新标签页)↗
只读请求编号、晚到请求和参数冲突三部分,写出重复订单请求的处理表。
查看学习准备与访问说明 →