使用幂等性在 Django 中缓解两将军问题
Source: Dev.to

金融科技架构视角
在构建支付系统时,最终会面临一个令人头疼的问题:
我们到底已经向客户收费了吗?
最糟糕的是,
有时系统真的不知道。
这不是 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 系统在不可靠的世界中保持财务安全的方式。