AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

幂等、补偿事务与重复投递:把未知结果关进状态机

我的学习 →

正在读取学习记录…

补偿是对已经发生的业务动作执行相反的业务动作。预订名额后扣了款,出票却失败,就需要退款并释放名额。退款可能产生新的流水,名额也可能已经变化,因此补偿需要遵守业务规则,逐步确认完成情况。

幂等处理重复同一个请求时的效果。补偿处理的是前面成功、后面失败的跨步骤流程。两者常常同时出现:预订请求可能重复投递,退款补偿也可能重复投递,正向与反向动作都要识别同一次业务意图。

运行一次出票失败后的恢复

固定初始值是两个名额、500元余额。一次120元预订先占一个名额再扣款,同一预订再投递一次。出票阶段用固定失败触发补偿,第一次退款暂不可用,第二次恢复,随后再重复补偿一次。

forward 保存业务键与金额,journal 记录成功步骤,undone 记录已完成的补偿。这三份数据分别回答“请求是哪一次”“做过哪些事”“撤销到哪里”。

成功步骤与逆序补偿,退款失败留在待补偿状态;确认退款后才继续释放
图1:退款失败留在待补偿状态;确认退款后才继续释放。

保存完整程序并运行

先在 PowerShell 创建并进入学习目录:

Bash
mkdir "F:\学习\l4-compensation" -Force
cd "F:\学习\l4-compensation"

保存下面的文件后,在这个终端运行 dir 核对文件名,再执行本节给出的运行命令。

新建 F:\学习\l4-compensation 文件夹,把下面完整代码保存为 compensation_demo.py,然后在PowerShell运行:

Bash
cd "F:\学习\l4-compensation"
ls
python -X utf8 compensation_demo.py

先在 PowerShell 创建并进入学习目录:

Bash
mkdir "F:\学习\l4-compensation" -Force
cd "F:\学习\l4-compensation"

保存下面的文件后,在这个终端运行 dir 核对文件名,再执行本节给出的运行命令。

Python
"""预订失败后按逆序退款、释放名额,并安全重复处理补偿。"""


class Booking:
    def __init__(self):
        self.seats = 2
        self.balance = 500
        self.forward = {}
        self.undone = set()
        self.journal = []

    def reserve(self, key, amount):
        # 同一业务键绑定同一金额,重复投递不会再次占位或扣款。
        if key in self.forward:
            if self.forward[key] != amount:
                raise ValueError("业务键与金额冲突")
            return
        if type(amount) is not int or amount <= 0:
            raise ValueError("金额应为正整数")
        if self.seats < 1 or self.balance < amount:
            raise ValueError("名额或余额不足")
        self.forward[key] = amount
        self.seats -= 1
        self.journal.append((key, "seat"))
        self.balance -= amount
        self.journal.append((key, "charge"))

    def compensate(self, key, *, fail_refund=False):
        # 从后向前撤销已经成功的步骤,并分别记录补偿完成状态。
        for saved_key, step in reversed(self.journal):
            if saved_key != key or (key, step) in self.undone:
                continue
            if step == "charge":
                if fail_refund:
                    return "compensation_pending"
                self.balance += self.forward[key]
            else:
                self.seats += 1
            self.undone.add((key, step))
        return "compensated"


def run_demo():
    booking = Booking()
    booking.reserve("booking-42", 120)
    booking.reserve("booking-42", 120)
    lines = [f"重复预订后: 名额={booking.seats}, 余额={booking.balance}"]
    # 固定模拟出票失败;之前的预订、扣款已经发生。
    lines.append("出票失败: 开始逆序补偿")
    status = booking.compensate("booking-42", fail_refund=True)
    lines.append(f"退款服务暂不可用: {status}, 名额={booking.seats}, 余额={booking.balance}")
    status = booking.compensate("booking-42")
    lines.append(f"恢复后: {status}, 名额={booking.seats}, 余额={booking.balance}")
    booking.compensate("booking-42")
    lines.append(f"重复补偿后: 名额={booking.seats}, 余额={booking.balance}")
    return lines


if __name__ == "__main__":
    print("\n".join(run_demo()))

完整固定输出如下:

TXT
重复预订后: 名额=1, 余额=380
出票失败: 开始逆序补偿
退款服务暂不可用: compensation_pending, 名额=1, 余额=380
恢复后: compensated, 名额=2, 余额=500
重复补偿后: 名额=2, 余额=500

为什么第一次失败后仍保留名额

第一行余额为380、名额为1,说明同一预订投递两次只占一次名额、扣一次款。第三行返回 compensation_pending,表示退款待恢复;金额和名额保持原状。第四行退款成功后释放名额,恢复到2和500;最后一行重复补偿没有多退钱。

compensate 从 journal 的尾部向前遍历。正向顺序是占位、扣款,反向顺序是退款、释放名额。固定规则要求先退款再放位,因此退款失败立即停在这个步骤。如果业务允许独立释放,也可以采用另一条补偿顺序,但必须把这条规则写出来。

给正向动作和补偿各自一个标识

reserve 使用预订键识别同一意图,同键同金额直接返回,同键不同金额拒绝。用户想再买一张同价票时应使用新键。补偿则使用 (key, step) 识别反向动作,完成退款后把 ("booking-42", "charge") 存入集合;再次恢复时跳过已退款步骤。

外部退款服务需要自己的幂等键,例如 booking-42/refund。仅在本地集合记“已退款”无法消除崩溃窗口:外部服务可能退款成功,本地记录尚未写入。如果响应丢失,需要复用同一退款键或查退款流水,不能直接发起一笔新退款。

补偿失败时任务停在哪里

compensation_pending 是明确的业务状态,后台工作者可以稍后继续,超过恢复时限则交给人工处理。记录至少包含原订单、正向回执、补偿类型、补偿回执和下一次恢复时间。补偿成功后才能报告流程已撤销,不能把“已发出退款请求”当成“退款已到账”。

示例数据都在一个进程内。真实跨服务流程要在每一步成功后持久保存记录,并根据服务契约区分已失败、已成功和结果未知。若外部动作失败前未产生效果,不应登记成功步骤;若结果未知,先查明是否需要补偿。补偿也不能撤销已经发送给用户的信息,只能再发送更正通知。

重复投递怎样影响整个流程

把同一预订再次投递到正向入口,当前代码返回已有处理,不会自动重新预订已补偿的订单。若用户决定重新购买,这是新的业务意图,应产生新键和新记录。这让“恢复旧流程”与“发起新流程”保持可辨认。

配套测试检查正向去重、金额冲突、余额不足、补偿暂失败、恢复后结果和重复补偿。它们检查余额与名额的具体值,而不以“函数没抛异常”代替业务结论。关于服务端如何绑定业务意图,可以继续读幂等 API 设计。

改一处输入,核对机制变化

在 run_demo 中将两次 reserve("booking-42", 120) 的120一起改为100,运行 python -X utf8 compensation_demo.py。重复预订后余额变为400,第一次退款失败仍为400;恢复补偿后余额回到500、名额回到2。两处一起恢复120再重跑。

运行配套测试

将下面测试保存为同一文件夹的 test_compensation_demo.py。它使用固定场景检查状态与业务结果,可以直接运行:

Bash
python -X utf8 -m unittest -v test_compensation_demo.py
完整测试:test_compensation_demo.py
Python
import unittest
from compensation_demo import Booking, run_demo


class CompensationTests(unittest.TestCase):
    def test_duplicate_forward_and_compensation(self):
        booking = Booking()
        booking.reserve("k", 120)
        booking.reserve("k", 120)
        self.assertEqual((booking.seats, booking.balance), (1, 380))
        self.assertEqual(booking.compensate("k"), "compensated")
        booking.compensate("k")
        self.assertEqual((booking.seats, booking.balance), (2, 500))

    def test_failed_refund_preserves_progress(self):
        # 退款失败时尚不能释放名额;恢复后分别登记两个反向步骤。
        booking = Booking()
        booking.reserve("k", 120)
        self.assertEqual(booking.compensate("k", fail_refund=True), "compensation_pending")
        self.assertEqual(booking.undone, set())
        self.assertEqual((booking.seats, booking.balance), (1, 380))
        booking.compensate("k")
        self.assertEqual(booking.undone, {("k", "charge"), ("k", "seat")})

    def test_key_conflict_and_insufficient_balance(self):
        booking = Booking()
        booking.reserve("k", 120)
        with self.assertRaises(ValueError):
            booking.reserve("k", 130)
        with self.assertRaises(ValueError):
            booking.reserve("other", 400)

    def test_demo(self):
        self.assertEqual(len(run_demo()), 5)


if __name__ == "__main__":
    unittest.main()
课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.0.0

objective · 幂等、补偿事务与重复投递

围绕 L4-compensation 说明一个可验证的工程做法。

对应知识点:L4-compensation、L4-retry

选择一个答案
练习 2 / 4 · v1.0.0

short_answer · 幂等、补偿事务与重复投递

围绕 L4-compensation 说明一个可验证的工程做法。

对应知识点:L4-compensation、L4-retry

练习 3 / 4 · v1.0.0

coding · 幂等、补偿事务与重复投递

围绕 L4-compensation 说明一个可验证的工程做法。

对应知识点:L4-compensation、L4-retry

练习 4 / 4 · v1.0.0

scenario · 幂等、补偿事务与重复投递

围绕 L4-compensation 说明一个可验证的工程做法。

对应知识点:L4-compensation、L4-retry

图片查看器
100%

文章配图 · 按 Esc 关闭