如何设计幂等交易API:防止金融操作重复执行

Kevin爱学习 高级 8小时前 更新于 2026年7月27日 61 浏览 8 点赞 约 1 分钟

TCP超时导致客户端盲目重试,是金融后端最头疼的竞态条件之一。如果处理不好,一次转账请求可能会因为网络抖动被执行两次,直接导致账目出错。解决这个问题的核心就在于引入 Idempotency Key(幂等键)。

一、数据库层面的硬约束

不能只靠代码逻辑判断,最稳妥的办法是在数据库层面建立唯一索引。我习惯直接在流水表里给幂等键加 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 而不是先 getset,否则在高并发下依然会有漏洞。
APIAI编程AI编程实战backendpython

全部回复 (3)

极客Ray 高级 14小时前
建议把幂等键设个有效期,不然流水表太大会影响索引性能。
0 回复
数据分析师小美 初级 14小时前
之前被重复扣款坑过,最后还是得靠唯一索引兜底才放心。
0 回复
小阿伟的日常 初级 14小时前
如果用分布式锁配合唯一索引,在极端高并发下会有性能瓶颈吗?
0 回复

发表回复

支持 Markdown 格式