使用幂等性在 Django 中缓解两将军问题

发布: (2026年3月1日 GMT+8 02:19)
7 分钟阅读
原文: Dev.to

Source: Dev.to

使用幂等 API 设计缓解分布式不确定性

金融科技架构视角

在构建支付系统时,最终会面临一个令人头疼的问题:

我们到底已经向客户收费了吗?

最糟糕的是,
有时系统真的不知道。

这不是 bug,而是分布式系统的现实——两将军问题:两个参与方无法在不可靠的网络上保证达成一致。

在金融科技中,这表现为:

Merchant → Backend → Payment Provider
  • 充值成功
  • 网络中断
  • 后端未收到确认
  • 客户端重试

现在怎么办?

  • 若盲目重试,会导致双重收费。
  • 若不重试,可能会失去收入。

这时 幂等性 成为核心的架构模式。

为什么这在金融科技中很重要

在国际支付系统中:

  • 移动网络不稳定
  • 客户双击
  • 工作人员在交易中途崩溃

如果您的后端不是幂等的,您将会:

  • 对客户进行双重收费
  • 产生会计不一致
  • 引发对账噩梦
  • 失去商户信任

像 Stripe 这样的大型提供商已经正式化了幂等性,因为这个问题是结构性的,而非偶然的。

目标:确定性重试

我们无法保证通过 HTTP 实现 一次性(exactly‑once)执行。
我们 可以 保证:

相同的请求键始终产生相同的结果。

这就是架构上的转变。我们不是去解决不确定性,而是让重试变得安全。

Source:

在 Django 中设计幂等性

这不仅仅是一个简单的 if exists 检查,而是要在 数据库边界 强制正确性。

1. 要求提供幂等性键

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

该键代表一次逻辑业务操作。

2. 使用唯一约束持久化键

# models.py
from django.db import models

class IdempotencyRecord(models.Model):
    key = models.CharField(max_length=255, unique=True)
    request_hash = models.CharField(max_length=64)
    response_body = models.JSONField(null=True, blank=True)
    response_status = models.IntegerField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

为什么重要

  • unique=True 能防止并发冲突。
  • 数据库成为真相来源。
  • 竞争条件从应用逻辑转移到原子约束上。

3. 对请求负载进行哈希

import hashlib

def hash_request(request):
    """Return a SHA‑256 hash of the raw request body."""
    return hashlib.sha256(request.body).hexdigest()

为什么要这样做?
如果有人使用相同的键但携带不同的负载,这将非常危险。我们必须检测:

  • 相同的键
  • 不同的业务意图

并予以拒绝。哈希在 Django 接收到请求时由服务器生成。

request payload → server‑side hash computation
                → database storage
                → hash comparison on retries
                → deterministic decision (replay or reject)

4. 原子化处理

from django.db import transaction, IntegrityError
from django.http import JsonResponse
from .models import IdempotencyRecord

@transaction.atomic
def process_payment(request):
    key = request.headers.get("Idempotency-Key")
    if not key:
        return JsonResponse({"error": "Missing Idempotency-Key"}, status=400)

    request_hash = hash_request(request)

    try:
        # 第一次看到该键
        record = IdempotencyRecord.objects.create(
            key=key,
            request_hash=request_hash
        )
        first_request = True
    except IntegrityError:
        # 键已存在 – 获取已有行进行更新
        record = IdempotencyRecord.objects.select_for_update().get(key=key)
        first_request = False

    if not first_request:
        # 重复请求 – 确保负载匹配
        if record.request_hash != request_hash:
            return JsonResponse({"error": "Payload mismatch"}, status=409)
        return JsonResponse(record.response_body, status=record.response_status)

    # ----- 副作用:外部扣费 -----
    charge = external_payment_call()   # <-- implement your provider call

    response = {"payment_id": charge.id}

    # 存储成功响应以供后续重放
    record.response_body = response
    record.response_status = 200
    record.save()

    return JsonResponse(response, status=200)

此实现保证了什么

  • 第一次请求会执行副作用(扣费)。
  • 重复请求返回已存储的结果。
  • 不会产生重复扣费。
  • 客户端可以安全重试。

在架构层面实现的目标

  • 模拟 exactly‑once 语义。
  • 接受最终一致性。
  • 将正确性转移到确定性回放中。
  • 使系统对网络故障具有弹性。

并未 消除分布式不确定性;而是围绕它进行工程设计。

金融科技关键边缘案例

1. 外部扣费后数据库崩溃

如果扣费成功但数据库提交失败,仍会出现不一致。

缓解措施

  • 使用提供商的 webhook 确认结算。
  • 运行对账作业,将外部日志与内部记录进行比对。
  • 采用基于账本的会计模型,使每个事件不可变。

2. 密钥过期

幂等性表会无限增长。

解决方案

  • 添加 TTL/清理作业。
  • 常见保留期限:
    • 支付操作保留 24‑48 小时。
    • 对可能需要审计的金融转账保留更长时间(数周)。

3. 可观测性

在生产环境的金融科技系统中:

  • 每个幂等性键都可追踪(在日志中记录键和请求 ID)。
  • 每次重试都记录时间戳和结果。
  • 指标跟踪重放频率、冲突率和延迟影响。

没有可观测性的幂等性是不完整的。

结束思考

在金融科技领域,正确性是不可妥协的。幂等 API 设计为您提供一种确定性、可审计的方式来处理分布式系统不可避免的不确定性——将可怕的“我们是否已经收费?”问题转化为可预测、可安全重试的工作流。

概览

Ess 并不是关于编写干净的代码,而是关于设计在故障情况下能够确定性行为的系统。

故障场景

  • 网络会掉线。
  • 客户端会重试。
  • 工作进程会崩溃。

关键原则

幂等性是让你的 Django 系统在不可靠的世界中保持财务安全的方式。

0 浏览
Back to Blog

相关文章

阅读更多 »

我构建的每个服务都会死

这正是重点。我是 Ontime Payments 的高级软件工程师,这是一家 fintech 初创公司,提供直接从工资中支付账单的服务。我们有意……

设计 URL Shortener

设计一个 URL 短链接服务是最受欢迎的系统设计面试题之一。它看起来很简单,但它考察你对可扩展性、数据库……的理解。