如何设计幂等交易API:防止金融操作重复执行
TCP超时导致客户端盲目重试,是金融后端最头疼的竞态条件之一。如果处理不好,一次转账请求可能会因为网络抖动被执行两次,直接导致账目出错。解决这个问题的核心就在于引入 Idempotency Key(幂等键)。
下一篇
纯前端页面真的能绝对安全吗?我随便写个只有 index. →
一、数据库层面的硬约束
不能只靠代码逻辑判断,最稳妥的办法是在数据库层面建立唯一索引。我习惯直接在流水表里给幂等键加 UNIQUE 约束,这样即使并发请求穿透了缓存,数据库也会在最后一道防线拦截掉重复记录。
CREATE TABLE journal_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
idempotency_key VARCHAR(255) UNIQUE NOT NULL,
amount DECIMAL(18, 4) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);二、Redis 分布式锁 + 状态缓存实战
在 FastAPI 框架中,可以用 Redis 的 set nx 实现一个简单的状态机:请求进来先占坑(IN_PROGRESS),处理完再更新结果(DONE)。这样既能防止并发冲突,又能让重复请求直接拿到之前的执行结果,而不需要重新跑一遍昂贵的业务逻辑。
from fastapi import FastAPI, Header, HTTPException, status
import redis
app = FastAPI()
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
@app.post("/api/v1/journal", status_code=201)
def post_transaction(payload: dict, idempotency_key: str = Header(...)):
# 使用 nx=True 实现原子性的 检查并设置
lock_acquired = r.set(f"idemp:{idempotency_key}", "IN_PROGRESS", nx=True, ex=3600)
if not lock_acquired:
status_val = r.get(f"idemp:{idempotency_key}")
if status_val == "IN_PROGRESS":
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Transaction in progress. Please wait."
)
# 如果状态是已完成,直接返回缓存的结果
return {"status": "success", "cached": True, "data": status_val}
try:
# 这里执行实际的数据库事务操作
result_data = {"transaction_id": "tx_abc123", "processed": True}
# 更新状态为已完成并存储响应内容
r.set(f"idemp:{idempotency_key}", str(result_data), ex=86400)
return {"status": "success", "cached": False, "data": result_data}
except Exception as e:
# 失败时记得删除锁,允许客户端重试
r.delete(f"idemp:{idempotency_key}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Transaction failed."
)三、实操中的踩坑点
在做并发测试时(建议用 pytest 配合 httpx 跑多线程),有几个细节得注意:
- 过期时间: Redis 里的幂等键不能永久保存,通常设置 24 小时过期,足够覆盖绝大多数重试场景。
- 异常回滚: 如果业务逻辑报错,必须要把 Redis 里的
IN_PROGRESS状态删掉,否则该请求会被永久锁定在“处理中”,导致客户端永远无法重试成功。 - 原子性: 必须使用
set nx而不是先get再set,否则在高并发下依然会有漏洞。