跳转到内容
123xiao | 无名键客

《微服务架构中分布式事务的实战落地:基于 Saga 模式的设计、实现与避坑指南》

字数: 0 阅读时长: 1 分钟

微服务架构中分布式事务的实战落地:基于 Saga 模式的设计、实现与避坑指南

在单体应用里,事务这件事通常不太让人焦虑:一个数据库连接、一个本地事务、一次提交或回滚,问题就解决了。

但一旦进入微服务架构,情况会立刻变味。下单服务、库存服务、支付服务、积分服务各自有自己的数据库,团队还可能独立部署、独立扩缩容。这时候,一个“用户下单成功”背后,往往意味着多个服务之间的一串状态变更。只靠本地事务,根本兜不住整个业务流程。

很多团队第一次遇到这个问题时,都会下意识想到“两阶段提交(2PC)”。理论上很完整,实践里却经常因为耦合高、性能差、可用性弱,被架构师一票否决。于是,Saga 就成了微服务场景里最常见、也最容易“看起来懂了,实际一上线就踩坑”的分布式事务方案。

这篇文章我会从业务设计视角 + 工程实现视角,带你把 Saga 模式走一遍:它为什么适合微服务、应该怎么实现、代码怎么写、哪些坑最容易在生产环境里炸出来,以及如何做性能和安全收口。


背景与问题

先看一个非常典型的业务链路:电商下单。

  1. 订单服务创建订单
  2. 库存服务扣减库存
  3. 支付服务完成支付
  4. 营销服务发放优惠券或积分

如果这些动作都成功,业务成立;但只要中间有一步失败,比如支付失败,就需要把前面的状态“撤回来”。

问题是:

  • 订单服务不能直接用本地事务控制库存库
  • 库存服务也不能顺手回滚支付库
  • 服务之间通过 HTTP、RPC 或消息通信,天然存在延迟、重试、超时、重复调用
  • 网络抖动下,“调用失败”并不等于“对方没执行”

这就是分布式事务的本质难点:跨服务、跨存储、跨网络的不确定性

为什么很多团队最后选择 Saga

Saga 不追求传统数据库事务里的强一致,而是用:

  • 一组本地事务
  • 配套的补偿动作
  • 最终一致性机制

来完成跨服务业务流程。

它特别适合:

  • 微服务自治
  • 服务独立数据库
  • 高并发读写
  • 能容忍短时间最终一致

不太适合:

  • 要求绝对实时强一致的核心账务场景
  • 补偿动作难定义或不可逆的流程
  • 业务上无法接受“中间态可见”的场景

一句话总结:Saga 是工程上的现实主义,不是理论上的完美主义。


方案对比与取舍分析

在进入实现前,先把常见方案摆清楚。很多设计争论,其实不是“谁更高级”,而是“谁更适合当前业务”。

方案一致性性能复杂度适用场景
2PC / XA强一致传统集中式、数据库支持完整 XA
TCC强一致偏强很高核心资金、库存冻结类场景
Saga最终一致微服务业务流程编排
本地消息表 + 最终一致最终一致异步事件驱动场景

Saga 与 TCC 的关键区别

很多人容易把 Saga 和 TCC 混着用,它们其实不是一个思路:

  • TCC:Try / Confirm / Cancel,强调资源预留和显式确认,更像“先冻结,后提交”
  • Saga:每一步先提交本地事务,后续失败时执行补偿,更像“先往前走,出错再回撤”

如果库存是“扣了就不好恢复”的强约束资源,TCC 可能更合适;
如果是订单、优惠券、流程状态这类适合补偿的业务,Saga 通常更实用。


核心原理

Saga 主要有两种实现方式:

  1. Choreography(事件编排 / 事件驱动)
  2. Orchestration(中心编排 / 流程驱动)

1. 事件驱动式 Saga

每个服务监听前一个服务发出的事件,完成自己的本地事务后再发出新事件。

优点:

  • 服务解耦
  • 扩展性好
  • 更贴近事件驱动架构

缺点:

  • 流程分散,排查困难
  • 业务链长时很难看全貌
  • 补偿链复杂

2. 中心编排式 Saga

由一个协调器(Orchestrator)统一驱动流程:

  • 调用订单服务
  • 调用库存服务
  • 调用支付服务
  • 某一步失败则按逆序执行补偿

优点:

  • 流程清晰
  • 容易审计和排查
  • 更适合大多数团队落地

缺点:

  • 协调器会成为流程中心
  • 编排层设计不好容易变成“大泥球”

在中级团队的真实落地里,我更推荐先用编排式 Saga 落地第一版。原因很简单:可控、可观测、方便排障。等团队对事件驱动和一致性治理成熟了,再拆到事件风格也不迟。


一张图看懂 Saga 的执行与补偿

flowchart LR
    A[创建订单] --> B[扣减库存]
    B --> C[执行支付]
    C --> D[增加积分]

    C -.失败.-> B1[支付补偿]
    B1 --> A1[库存补偿]
    A1 --> A2[订单补偿]

    D -.失败.-> D1[积分补偿]
    D1 --> C1[支付补偿]
    C1 --> B2[库存补偿]
    B2 --> A3[订单补偿]

这里要注意一个细节:补偿不是数据库 rollback
它是一个新的业务动作,比如:

  • 扣库存的补偿不是事务回滚,而是“释放库存”
  • 创建订单的补偿不是 JDBC rollback,而是“将订单状态置为已取消”
  • 发积分的补偿不是撤销 SQL 事务,而是“冲正积分流水”

这也是很多实现失败的根因之一:把“业务补偿”误当成“技术回滚”。


Saga 状态机设计:真正决定是否可维护

如果你的 Saga 只是“调用接口 + try/catch + 失败后反调几个接口”,那只能算 demo,不算可运维的生产方案。

生产上,最好明确一个事务状态机。

stateDiagram-v2
    [*] --> Started
    Started --> OrderCreated
    OrderCreated --> InventoryReserved
    InventoryReserved --> PaymentCompleted
    PaymentCompleted --> Finished

    OrderCreated --> Compensating
    InventoryReserved --> Compensating
    PaymentCompleted --> Compensating

    Compensating --> Cancelled
    Finished --> [*]
    Cancelled --> [*]

建议至少记录这些字段:

  • saga_id
  • business_id,如 order_id
  • state
  • step
  • payload
  • retry_count
  • last_error
  • created_at
  • updated_at

为什么这一步很关键?

因为一旦线上出现:

  • 协调器重启
  • 调用超时
  • 消息重复投递
  • 补偿执行一半中断

你必须靠状态表恢复现场,而不是靠人肉翻日志。


实战设计:一个可落地的订单 Saga

下面用一个“订单-库存-支付”的简化案例,演示中心编排式 Saga 的实现思路。

业务规则

  • 创建订单成功后,订单状态为 PENDING
  • 扣库存成功后,库存减少
  • 支付成功后,订单状态改为 PAID
  • 如果支付失败:
    • 释放库存
    • 取消订单

设计原则

  1. 每个本地事务独立提交
  2. 每个动作都必须有幂等保障
  3. 补偿动作也必须幂等
  4. 状态变化必须可追踪
  5. 外部调用默认会超时、失败、重复

实战代码(可运行)

为了方便直接跑起来,下面我用 Python 写一个简化版 Saga 编排器。它不依赖真实数据库和消息队列,但完整体现了:

  • 本地事务步骤
  • 失败补偿
  • 幂等控制
  • Saga 状态追踪

你可以直接保存为 saga_demo.py 运行。

from dataclasses import dataclass, field
from typing import Dict, Set
import uuid


# ===== 模拟数据库 =====
orders: Dict[str, dict] = {}
inventory: Dict[str, int] = {"apple": 10}
payments: Dict[str, dict] = {}

# 幂等记录
processed_actions: Set[str] = set()


@dataclass
class SagaContext:
    saga_id: str
    order_id: str
    product_id: str
    quantity: int
    amount: int
    state: str = "STARTED"
    logs: list = field(default_factory=list)

    def log(self, message: str):
        self.logs.append(message)
        print(f"[{self.saga_id}] {message}")


def idempotent(action_key: str) -> bool:
    if action_key in processed_actions:
        return False
    processed_actions.add(action_key)
    return True


# ===== 订单服务 =====
def create_order(ctx: SagaContext):
    action_key = f"create_order:{ctx.order_id}"
    if not idempotent(action_key):
        ctx.log(f"create_order 幂等跳过: {ctx.order_id}")
        return

    orders[ctx.order_id] = {
        "order_id": ctx.order_id,
        "status": "PENDING",
        "product_id": ctx.product_id,
        "quantity": ctx.quantity,
        "amount": ctx.amount,
    }
    ctx.state = "ORDER_CREATED"
    ctx.log(f"订单创建成功: {ctx.order_id}")


def cancel_order(ctx: SagaContext):
    action_key = f"cancel_order:{ctx.order_id}"
    if not idempotent(action_key):
        ctx.log(f"cancel_order 幂等跳过: {ctx.order_id}")
        return

    order = orders.get(ctx.order_id)
    if order and order["status"] != "CANCELLED":
        order["status"] = "CANCELLED"
        ctx.log(f"订单取消成功: {ctx.order_id}")


def mark_order_paid(ctx: SagaContext):
    action_key = f"mark_paid:{ctx.order_id}"
    if not idempotent(action_key):
        ctx.log(f"mark_order_paid 幂等跳过: {ctx.order_id}")
        return

    order = orders.get(ctx.order_id)
    if not order:
        raise Exception("订单不存在,无法更新为已支付")

    order["status"] = "PAID"
    ctx.log(f"订单更新为已支付: {ctx.order_id}")


# ===== 库存服务 =====
def reserve_inventory(ctx: SagaContext):
    action_key = f"reserve_inventory:{ctx.order_id}"
    if not idempotent(action_key):
        ctx.log(f"reserve_inventory 幂等跳过: {ctx.order_id}")
        return

    stock = inventory.get(ctx.product_id, 0)
    if stock < ctx.quantity:
        raise Exception("库存不足")

    inventory[ctx.product_id] -= ctx.quantity
    ctx.state = "INVENTORY_RESERVED"
    ctx.log(f"库存扣减成功: {ctx.product_id} -{ctx.quantity}")


def release_inventory(ctx: SagaContext):
    action_key = f"release_inventory:{ctx.order_id}"
    if not idempotent(action_key):
        ctx.log(f"release_inventory 幂等跳过: {ctx.order_id}")
        return

    inventory[ctx.product_id] = inventory.get(ctx.product_id, 0) + ctx.quantity
    ctx.log(f"库存释放成功: {ctx.product_id} +{ctx.quantity}")


# ===== 支付服务 =====
def process_payment(ctx: SagaContext, force_fail: bool = False):
    action_key = f"process_payment:{ctx.order_id}"
    if not idempotent(action_key):
        ctx.log(f"process_payment 幂等跳过: {ctx.order_id}")
        return

    if force_fail:
        raise Exception("支付失败:模拟第三方渠道异常")

    payments[ctx.order_id] = {
        "order_id": ctx.order_id,
        "status": "SUCCESS",
        "amount": ctx.amount
    }
    ctx.state = "PAYMENT_COMPLETED"
    ctx.log(f"支付成功: {ctx.order_id}")


def refund_payment(ctx: SagaContext):
    action_key = f"refund_payment:{ctx.order_id}"
    if not idempotent(action_key):
        ctx.log(f"refund_payment 幂等跳过: {ctx.order_id}")
        return

    payment = payments.get(ctx.order_id)
    if payment and payment["status"] == "SUCCESS":
        payment["status"] = "REFUNDED"
        ctx.log(f"支付退款成功: {ctx.order_id}")
    else:
        ctx.log(f"无需退款: {ctx.order_id}")


# ===== Saga 编排器 =====
class OrderSagaOrchestrator:

    def execute(self, product_id: str, quantity: int, amount: int, force_payment_fail: bool = False):
        order_id = str(uuid.uuid4())
        saga_id = str(uuid.uuid4())
        ctx = SagaContext(
            saga_id=saga_id,
            order_id=order_id,
            product_id=product_id,
            quantity=quantity,
            amount=amount
        )

        try:
            create_order(ctx)
            reserve_inventory(ctx)
            process_payment(ctx, force_fail=force_payment_fail)
            mark_order_paid(ctx)
            ctx.state = "FINISHED"
            ctx.log("Saga 执行完成")
            return ctx
        except Exception as e:
            ctx.log(f"Saga 执行失败: {e}")
            self.compensate(ctx)
            ctx.state = "CANCELLED"
            return ctx

    def compensate(self, ctx: SagaContext):
        ctx.log("开始执行补偿逻辑")

        if ctx.state == "PAYMENT_COMPLETED":
            refund_payment(ctx)
            release_inventory(ctx)
            cancel_order(ctx)
        elif ctx.state == "INVENTORY_RESERVED":
            release_inventory(ctx)
            cancel_order(ctx)
        elif ctx.state == "ORDER_CREATED":
            cancel_order(ctx)

        ctx.log("补偿逻辑执行结束")


if __name__ == "__main__":
    orchestrator = OrderSagaOrchestrator()

    print("\n=== 场景1:成功下单 ===")
    ctx1 = orchestrator.execute("apple", 2, 100, force_payment_fail=False)
    print("订单数据:", orders.get(ctx1.order_id))
    print("库存数据:", inventory)
    print("支付数据:", payments.get(ctx1.order_id))

    print("\n=== 场景2:支付失败,触发补偿 ===")
    ctx2 = orchestrator.execute("apple", 3, 150, force_payment_fail=True)
    print("订单数据:", orders.get(ctx2.order_id))
    print("库存数据:", inventory)
    print("支付数据:", payments.get(ctx2.order_id))

运行后你会看到什么

  • 成功场景下:
    • 订单状态变成 PAID
    • 库存减少
    • 支付记录成功
  • 支付失败场景下:
    • 订单被取消
    • 库存被释放
    • 支付若未成功则无需退款

这个 demo 虽然简单,但已经具备了 Saga 落地最核心的骨架。


生产环境里应该怎么拆

上面的单文件代码,只是帮助你理解逻辑。真正落地时,通常会拆成下面的结构:

sequenceDiagram
    participant Client as 客户端
    participant Orch as Saga协调器
    participant Order as 订单服务
    participant Inventory as 库存服务
    participant Payment as 支付服务

    Client->>Orch: 提交下单请求
    Orch->>Order: 创建订单
    Order-->>Orch: 成功
    Orch->>Inventory: 扣减库存
    Inventory-->>Orch: 成功
    Orch->>Payment: 发起支付
    Payment-->>Orch: 失败/超时
    Orch->>Inventory: 执行库存补偿
    Inventory-->>Orch: 成功
    Orch->>Order: 执行订单补偿
    Order-->>Orch: 成功
    Orch-->>Client: 返回最终状态

推荐组件划分

  • Saga Orchestrator
    • 负责步骤编排
    • 持久化 Saga 状态
    • 失败重试与恢复
  • 业务服务
    • 只维护自己的本地事务
    • 暴露执行接口和补偿接口
  • 事务状态表
    • 保存 Saga 执行进度
  • 消息队列
    • 用于异步重试、延迟补偿、事件通知
  • 监控系统
    • 追踪异常 Saga、补偿次数、超时率

落地时最关键的几个设计点

1. 幂等性不是加分项,是必选项

无论是正向动作还是补偿动作,都必须支持幂等。原因很直接:

  • 调用方会重试
  • MQ 会重复投递
  • 协调器宕机恢复后会重复推进
  • 网络超时可能导致“调用方以为失败,服务端其实成功”

常见幂等做法

  • 业务唯一键,如 order_id
  • 请求号 / 幂等号
  • 去重表
  • 状态机防重
  • 唯一索引兜底

例如库存扣减,不要只写:

UPDATE inventory SET stock = stock - 1 WHERE product_id = 'apple';

最好结合业务幂等号记录动作:

CREATE TABLE inventory_txn (
  txn_id VARCHAR(64) PRIMARY KEY,
  order_id VARCHAR(64) NOT NULL,
  product_id VARCHAR(64) NOT NULL,
  quantity INT NOT NULL,
  action VARCHAR(32) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

2. 补偿动作必须是“业务可逆”的

这是 Saga 最容易被忽略的边界。

并不是所有动作都能补偿。

可补偿的例子

  • 订单创建 → 订单取消
  • 库存扣减 → 库存释放
  • 发积分 → 冲正积分
  • 发优惠券 → 撤销优惠券

难补偿甚至不可补偿的例子

  • 短信已发送
  • 邮件已送达
  • 外部清结算已提交
  • 物流已出库
  • 用户已看到并消费了某个中间状态

这类场景需要你在业务上改造流程,比如:

  • 先冻结资源而不是直接提交
  • 将不可逆动作放到 Saga 成功后异步执行
  • 允许人工介入冲正

如果业务本身不可逆,Saga 不是银弹。


3. 超时处理不能只看“失败”,要考虑“不确定”

我之前踩过一个很典型的坑:支付接口超时,协调器认为失败,于是开始补偿库存和订单;结果 3 秒后支付渠道回调说其实扣款成功了。于是系统里出现了:

  • 订单已取消
  • 用户却真的被扣钱了

这类问题本质上不是“支付失败”,而是支付结果未知

正确处理方式

把远程调用结果拆成三类:

  • 成功
  • 明确失败
  • 未知(超时、网络断开、响应丢失)

对“未知”状态,通常不要立刻补偿,而应该:

  1. 将 Saga 标记为 PENDING_CONFIRM
  2. 通过回查接口、支付回调、延迟任务确认最终结果
  3. 再决定是否推进或补偿

这是生产系统里非常重要的一条经验。


4. 补偿顺序必须逆序执行

为什么要逆序?

因为后面的动作,通常依赖前面的动作存在。
比如:

  1. 创建订单
  2. 扣减库存
  3. 完成支付

如果支付失败,正确补偿顺序应该是:

  1. 退款支付
  2. 释放库存
  3. 取消订单

这和函数调用栈很像:正向是压栈,补偿是出栈。


常见坑与排查

下面这部分,是我认为最有实战价值的内容。很多文章讲到原理就结束了,但真正让系统稳定下来的,往往是这些“脏活累活”。

坑 1:补偿接口把库存加多了

现象

  • 一个订单失败后,库存被释放了两次
  • 数据越跑越多

根因

  • 补偿接口没有幂等
  • 协调器重试 + MQ 重复消费叠加

排查路径

  1. 查补偿日志是否被执行多次
  2. 查库存流水表是否有重复 release 记录
  3. 查补偿接口是否只靠“调用次数”而非“业务状态”判断

修复建议

  • 为每次补偿动作引入唯一事务号
  • 库存变更必须记录流水
  • 以“事务号是否处理过”作为幂等判断标准

坑 2:订单取消了,但支付后来成功

现象

  • 用户支付成功,但订单已关闭
  • 客服开始介入,人肉退款

根因

  • 把远程超时当成明确失败
  • 没有设计“结果未知”的中间状态

排查路径

  1. 查看支付请求日志和回调日志的时间差
  2. 对比协调器超时时间与第三方平均响应时间
  3. 查看是否存在“先补偿后回调成功”的竞态

修复建议

  • 引入 PENDING_CONFIRM
  • 超时场景靠回调或主动查询确认
  • 补偿前先做状态二次确认

坑 3:协调器重启后,Saga 卡住

现象

  • 某些事务永远停留在 INVENTORY_RESERVED
  • 数据不一致长期无人处理

根因

  • Saga 状态未持久化
  • 重启后内存上下文丢失
  • 没有恢复任务扫描未完成事务

排查路径

  1. 查看 Saga 状态表中长时间未更新的记录
  2. 检查服务重启时间点
  3. 检查是否存在恢复调度任务

修复建议

  • 每一步执行后立即落库状态
  • 定时任务扫描超时事务
  • 基于状态机恢复执行或触发补偿

坑 4:补偿成功了,但前台仍然看到脏状态

现象

  • 用户先看到订单处理中,随后又变成取消
  • 前端报“状态跳变异常”

根因

  • 最终一致性带来的中间态暴露
  • 前台没有按业务状态做正确展示

修复建议

  • 明确向前端暴露“处理中”“确认中”“失败补偿中”等状态
  • 对用户可见页面进行状态收敛
  • 不要假设后端状态只有“成功 / 失败”两种

安全/性能最佳实践

Saga 不只是“能跑”,还要考虑安全边界和吞吐稳定性。

安全最佳实践

1. 补偿接口必须鉴权

很多团队觉得“补偿接口是内部接口,不重要”,这是危险的。
如果补偿接口被误调用,轻则库存错乱,重则资金冲正。

建议:

  • 内网调用也要做服务鉴权
  • 使用 mTLS、签名或网关认证
  • 接口参数必须校验 saga_id / order_id / action

2. 防止重放攻击

如果你的 Saga 是基于消息驱动或 HTTP 回调,必须避免同一个请求被恶意重放。

建议:

  • 每个动作使用唯一请求号
  • 请求带时间戳和签名
  • 服务端校验是否已处理

3. 审计日志不能缺

所有关键动作必须留痕:

  • 谁发起的
  • 发起时间
  • 入参是什么
  • 执行结果如何
  • 补偿是否触发
  • 最终一致状态是什么

对于资金、库存、权益场景,这些日志是追责和修复的基础。


性能最佳实践

1. 不要让 Saga 协调器同步阻塞太久

长事务最怕阻塞线程池。
如果支付、风控、积分链路都同步串行,很容易把协调器拖死。

建议:

  • 将长耗时步骤异步化
  • 协调器只推进状态,不长时间占用工作线程
  • 对外返回“处理中”,再通过轮询或回调通知最终结果

2. 控制补偿风暴

当下游服务故障时,可能同时触发大量补偿请求,反过来把系统彻底压垮。

建议:

  • 补偿任务做限流
  • 按业务优先级分队列
  • 对同一资源加串行化或分片控制

3. 状态表和流水表要考虑容量

很多团队一开始只关注业务表,忽略事务日志表,几个月后发现:

  • saga_txn 表暴涨
  • 查询慢
  • 恢复任务全表扫描越来越重

一个粗略的容量估算方法

假设:

  • 日订单量 100 万
  • 每笔 Saga 平均 4 次状态更新
  • 每条状态记录 1 KB

那么每天事务状态写入约:

1000000 * 4 * 1KB = 4GB / 天

如果保留 90 天:

4GB * 90 = 360GB

这还没算索引、日志、补偿流水。

所以建议:

  • 热数据与历史数据分离
  • 状态表按时间或业务分片
  • 恢复任务只扫“未完成 + 最近窗口”的数据
  • 归档策略提前设计,不要等表炸了再补

一套比较稳的工程落地清单

如果你准备在项目里上 Saga,我建议至少确认下面这些点。

设计阶段

  • 每个步骤是否都有明确补偿动作
  • 是否存在不可逆操作
  • 是否定义了中间态和未知态
  • 是否明确补偿顺序

开发阶段

  • 正向接口幂等
  • 补偿接口幂等
  • Saga 状态持久化
  • 失败原因可追踪
  • 超时与重试策略已配置

测试阶段

  • 模拟下游超时
  • 模拟重复请求
  • 模拟 MQ 重复消费
  • 模拟协调器重启
  • 模拟补偿执行到一半中断

运维阶段

  • 未完成事务有监控
  • 补偿失败有告警
  • 长时间停滞事务可人工处理
  • 关键状态变化有审计日志

什么时候不该用 Saga

这点也很重要。不是所有分布式事务都该拿 Saga 硬套。

如果你的业务有下面特征,就要慎用:

  • 要求绝对实时强一致
  • 补偿代价极高
  • 补偿无法业务逆转
  • 中间态对用户不可见、也不可接受
  • 外部系统不支持回查、幂等和冲正

例如核心总账、清结算、证券撮合等场景,往往更适合:

  • TCC
  • 严格账务分录
  • 对账 + 人工兜底机制
  • 专门的事务平台

Saga 更适合流程型业务,而不是所有强一致业务。


总结

Saga 模式之所以在微服务架构里流行,不是因为它最“完美”,而是因为它在一致性、性能、可用性、工程复杂度之间做了一个相对现实的平衡。

真正落地时,你要记住这几件事:

  1. Saga 的本质是本地事务 + 业务补偿,不是数据库回滚
  2. 编排式 Saga 更适合大多数团队先落地
  3. 幂等、状态持久化、未知态处理,是生产可用的三大前提
  4. 补偿不是兜底魔法,前提是业务动作可逆
  5. 不要只设计 happy path,要从超时、重复、重启、乱序开始设计

如果你现在正准备在订单、库存、支付这类链路上引入 Saga,我的建议是:

  • 第一版先做中心编排
  • 先把状态机和补偿设计清楚
  • 先覆盖“超时 + 重试 + 恢复”这些脏场景
  • 最后再优化成事件驱动或平台化能力

能稳定处理异常的 Saga,才是真的 Saga。只在成功路径上跑通的,充其量只是一个好看的流程图。


分享到:

下一篇
《Web逆向实战:中级开发者如何定位并复现前端签名算法实现接口自动化调用》