idempotency
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseIdempotent API Operations
幂等API操作
Safely handle retries without duplicate side effects.
安全处理重试,避免重复产生副作用。
When to Use This Skill
何时使用该方案
- Payment processing (charges, refunds)
- Order creation and fulfillment
- Any operation with side effects
- APIs that may be retried by clients
- Webhook handlers
- 支付处理(收费、退款)
- 订单创建与履约
- 任何有副作用的操作
- 可能被客户端重试的API
- Webhook处理器
What is Idempotency?
什么是幂等性?
An operation is idempotent if executing it multiple times produces the same result as executing it once.
Request 1: POST /orders {item: "book"} → Order #123 created
Request 2: POST /orders {item: "book"} → Order #123 returned (not #124)
(same idempotency key)如果一个操作执行多次的结果与执行一次的结果相同,那么这个操作就是幂等的。
请求1: POST /orders {item: "book"} → 订单#123已创建
请求2: POST /orders {item: "book"} → 返回订单#123(而非#124)
(使用相同的Idempotency-Key)Architecture
架构设计
┌─────────────────────────────────────────────────────┐
│ Client Request │
│ Idempotency-Key: abc-123 │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Check Idempotency Store │
│ │
│ Key exists? │
│ ├─ Yes, completed → Return cached response │
│ ├─ Yes, in-progress → Return 409 Conflict │
│ └─ No → Continue processing │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Lock & Process Request │
│ │
│ 1. Acquire lock (set key as "processing") │
│ 2. Execute operation │
│ 3. Store response │
│ 4. Return response │
└─────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────┐
│ 客户端请求 │
│ Idempotency-Key: abc-123 │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 检查幂等性存储 │
│ │
│ 键是否存在? │
│ ├─ 是,已完成 → 返回缓存响应 │
│ ├─ 是,处理中 → 返回409冲突 │
│ └─ 否 → 继续处理 │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 加锁并处理请求 │
│ │
│ 1. 获取锁(将键设置为"processing") │
│ 2. 执行操作 │
│ 3. 存储响应 │
│ 4. 返回响应 │
└─────────────────────────────────────────────────────┘TypeScript Implementation
TypeScript 实现
Idempotency Store
幂等性存储
typescript
// idempotency-store.ts
import { Redis } from 'ioredis';
interface IdempotencyRecord {
status: 'processing' | 'completed';
response?: {
statusCode: number;
body: unknown;
headers?: Record<string, string>;
};
createdAt: number;
completedAt?: number;
}
interface IdempotencyConfig {
redis: Redis;
keyPrefix?: string;
lockTtlMs?: number; // How long to hold processing lock
responseTtlMs?: number; // How long to cache completed responses
}
class IdempotencyStore {
private redis: Redis;
private keyPrefix: string;
private lockTtl: number;
private responseTtl: number;
constructor(config: IdempotencyConfig) {
this.redis = config.redis;
this.keyPrefix = config.keyPrefix || 'idempotency:';
this.lockTtl = config.lockTtlMs || 60000; // 1 minute
this.responseTtl = config.responseTtlMs || 86400000; // 24 hours
}
async get(key: string): Promise<IdempotencyRecord | null> {
const data = await this.redis.get(this.keyPrefix + key);
return data ? JSON.parse(data) : null;
}
async acquireLock(key: string): Promise<boolean> {
const record: IdempotencyRecord = {
status: 'processing',
createdAt: Date.now(),
};
// SET NX = only set if not exists
const result = await this.redis.set(
this.keyPrefix + key,
JSON.stringify(record),
'PX',
this.lockTtl,
'NX'
);
return result === 'OK';
}
async complete(
key: string,
response: IdempotencyRecord['response']
): Promise<void> {
const record: IdempotencyRecord = {
status: 'completed',
response,
createdAt: Date.now(),
completedAt: Date.now(),
};
await this.redis.set(
this.keyPrefix + key,
JSON.stringify(record),
'PX',
this.responseTtl
);
}
async release(key: string): Promise<void> {
await this.redis.del(this.keyPrefix + key);
}
}
export { IdempotencyStore, IdempotencyRecord, IdempotencyConfig };typescript
// idempotency-store.ts
import { Redis } from 'ioredis';
interface IdempotencyRecord {
status: 'processing' | 'completed';
response?: {
statusCode: number;
body: unknown;
headers?: Record<string, string>;
};
createdAt: number;
completedAt?: number;
}
interface IdempotencyConfig {
redis: Redis;
keyPrefix?: string;
lockTtlMs?: number; // 处理锁的持有时长
responseTtlMs?: number; // 缓存已完成响应的时长
}
class IdempotencyStore {
private redis: Redis;
private keyPrefix: string;
private lockTtl: number;
private responseTtl: number;
constructor(config: IdempotencyConfig) {
this.redis = config.redis;
this.keyPrefix = config.keyPrefix || 'idempotency:';
this.lockTtl = config.lockTtlMs || 60000; // 1分钟
this.responseTtl = config.responseTtlMs || 86400000; // 24小时
}
async get(key: string): Promise<IdempotencyRecord | null> {
const data = await this.redis.get(this.keyPrefix + key);
return data ? JSON.parse(data) : null;
}
async acquireLock(key: string): Promise<boolean> {
const record: IdempotencyRecord = {
status: 'processing',
createdAt: Date.now(),
};
// SET NX = 仅当键不存在时设置
const result = await this.redis.set(
this.keyPrefix + key,
JSON.stringify(record),
'PX',
this.lockTtl,
'NX'
);
return result === 'OK';
}
async complete(
key: string,
response: IdempotencyRecord['response']
): Promise<void> {
const record: IdempotencyRecord = {
status: 'completed',
response,
createdAt: Date.now(),
completedAt: Date.now(),
};
await this.redis.set(
this.keyPrefix + key,
JSON.stringify(record),
'PX',
this.responseTtl
);
}
async release(key: string): Promise<void> {
await this.redis.del(this.keyPrefix + key);
}
}
export { IdempotencyStore, IdempotencyRecord, IdempotencyConfig };Express Middleware
Express 中间件
typescript
// idempotency-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { IdempotencyStore } from './idempotency-store';
interface IdempotencyOptions {
store: IdempotencyStore;
headerName?: string;
methods?: string[];
paths?: RegExp[];
}
function idempotencyMiddleware(options: IdempotencyOptions) {
const {
store,
headerName = 'Idempotency-Key',
methods = ['POST', 'PUT', 'PATCH'],
paths = [/.*/],
} = options;
return async (req: Request, res: Response, next: NextFunction) => {
// Only apply to specified methods
if (!methods.includes(req.method)) {
return next();
}
// Only apply to specified paths
if (!paths.some(p => p.test(req.path))) {
return next();
}
const idempotencyKey = req.headers[headerName.toLowerCase()] as string;
// No key provided - proceed without idempotency
if (!idempotencyKey) {
return next();
}
// Create a unique key combining the idempotency key with request details
const fullKey = `${req.method}:${req.path}:${idempotencyKey}`;
// Check for existing record
const existing = await store.get(fullKey);
if (existing) {
if (existing.status === 'processing') {
// Request is still being processed
return res.status(409).json({
error: 'Conflict',
message: 'A request with this idempotency key is already being processed',
});
}
if (existing.status === 'completed' && existing.response) {
// Return cached response
res.status(existing.response.statusCode);
if (existing.response.headers) {
for (const [key, value] of Object.entries(existing.response.headers)) {
res.setHeader(key, value);
}
}
res.setHeader('X-Idempotent-Replayed', 'true');
return res.json(existing.response.body);
}
}
// Try to acquire lock
const acquired = await store.acquireLock(fullKey);
if (!acquired) {
// Another request just acquired the lock
return res.status(409).json({
error: 'Conflict',
message: 'A request with this idempotency key is already being processed',
});
}
// Capture the response
const originalJson = res.json.bind(res);
let responseBody: unknown;
res.json = (body: unknown) => {
responseBody = body;
return originalJson(body);
};
// Store response after it's sent
res.on('finish', async () => {
if (res.statusCode >= 200 && res.statusCode < 500) {
// Store successful responses and client errors (but not server errors)
await store.complete(fullKey, {
statusCode: res.statusCode,
body: responseBody,
});
} else {
// Release lock for server errors (allow retry)
await store.release(fullKey);
}
});
next();
};
}
export { idempotencyMiddleware, IdempotencyOptions };typescript
// idempotency-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { IdempotencyStore } from './idempotency-store';
interface IdempotencyOptions {
store: IdempotencyStore;
headerName?: string;
methods?: string[];
paths?: RegExp[];
}
function idempotencyMiddleware(options: IdempotencyOptions) {
const {
store,
headerName = 'Idempotency-Key',
methods = ['POST', 'PUT', 'PATCH'],
paths = [/.*/],
} = options;
return async (req: Request, res: Response, next: NextFunction) => {
// 仅应用于指定的请求方法
if (!methods.includes(req.method)) {
return next();
}
// 仅应用于指定的路径
if (!paths.some(p => p.test(req.path))) {
return next();
}
const idempotencyKey = req.headers[headerName.toLowerCase()] as string;
// 未提供键 - 不启用幂等性,继续处理
if (!idempotencyKey) {
return next();
}
// 生成唯一键,组合幂等键与请求详情
const fullKey = `${req.method}:${req.path}:${idempotencyKey}`;
// 检查是否存在已有记录
const existing = await store.get(fullKey);
if (existing) {
if (existing.status === 'processing') {
// 请求仍在处理中
return res.status(409).json({
error: 'Conflict',
message: '使用该幂等键的请求正在处理中',
});
}
if (existing.status === 'completed' && existing.response) {
// 返回缓存的响应
res.status(existing.response.statusCode);
if (existing.response.headers) {
for (const [key, value] of Object.entries(existing.response.headers)) {
res.setHeader(key, value);
}
}
res.setHeader('X-Idempotent-Replayed', 'true');
return res.json(existing.response.body);
}
}
// 尝试获取锁
const acquired = await store.acquireLock(fullKey);
if (!acquired) {
// 另一个请求刚刚获取了锁
return res.status(409).json({
error: 'Conflict',
message: '使用该幂等键的请求正在处理中',
});
}
// 捕获响应内容
const originalJson = res.json.bind(res);
let responseBody: unknown;
res.json = (body: unknown) => {
responseBody = body;
return originalJson(body);
};
// 响应发送完成后存储结果
res.on('finish', async () => {
if (res.statusCode >= 200 && res.statusCode < 500) {
// 存储成功响应和客户端错误(不存储服务端错误)
await store.complete(fullKey, {
statusCode: res.statusCode,
body: responseBody,
});
} else {
// 服务端错误时释放锁(允许重试)
await store.release(fullKey);
}
});
next();
};
}
export { idempotencyMiddleware, IdempotencyOptions };Usage
使用示例
typescript
// app.ts
import express from 'express';
import { Redis } from 'ioredis';
import { IdempotencyStore } from './idempotency-store';
import { idempotencyMiddleware } from './idempotency-middleware';
const app = express();
const redis = new Redis();
const idempotencyStore = new IdempotencyStore({ redis });
// Apply to all POST/PUT/PATCH requests
app.use(idempotencyMiddleware({
store: idempotencyStore,
methods: ['POST', 'PUT', 'PATCH'],
}));
// Or apply to specific routes
app.post('/orders',
idempotencyMiddleware({
store: idempotencyStore,
paths: [/^\/orders$/],
}),
async (req, res) => {
const order = await createOrder(req.body);
res.status(201).json(order);
}
);typescript
// app.ts
import express from 'express';
import { Redis } from 'ioredis';
import { IdempotencyStore } from './idempotency-store';
import { idempotencyMiddleware } from './idempotency-middleware';
const app = express();
const redis = new Redis();
const idempotencyStore = new IdempotencyStore({ redis });
// 应用于所有POST/PUT/PATCH请求
app.use(idempotencyMiddleware({
store: idempotencyStore,
methods: ['POST', 'PUT', 'PATCH'],
}));
// 或仅应用于特定路由
app.post('/orders',
idempotencyMiddleware({
store: idempotencyStore,
paths: [/^\/orders$/],
}),
async (req, res) => {
const order = await createOrder(req.body);
res.status(201).json(order);
}
);Python Implementation
Python 实现
python
undefinedpython
undefinedidempotency.py
idempotency.py
import json
import time
from typing import Optional, Dict, Any
from dataclasses import dataclass
import redis
from functools import wraps
@dataclass
class IdempotencyRecord:
status: str # 'processing' | 'completed'
response: Optional[Dict[str, Any]] = None
created_at: float = 0
completed_at: Optional[float] = None
class IdempotencyStore:
def init(
self,
redis_client: redis.Redis,
key_prefix: str = "idempotency:",
lock_ttl_ms: int = 60000,
response_ttl_ms: int = 86400000,
):
self.redis = redis_client
self.key_prefix = key_prefix
self.lock_ttl = lock_ttl_ms
self.response_ttl = response_ttl_ms
def get(self, key: str) -> Optional[IdempotencyRecord]:
data = self.redis.get(self.key_prefix + key)
if not data:
return None
parsed = json.loads(data)
return IdempotencyRecord(**parsed)
def acquire_lock(self, key: str) -> bool:
record = {
"status": "processing",
"created_at": time.time(),
}
result = self.redis.set(
self.key_prefix + key,
json.dumps(record),
px=self.lock_ttl,
nx=True,
)
return result is True
def complete(self, key: str, response: Dict[str, Any]) -> None:
record = {
"status": "completed",
"response": response,
"created_at": time.time(),
"completed_at": time.time(),
}
self.redis.set(
self.key_prefix + key,
json.dumps(record),
px=self.response_ttl,
)
def release(self, key: str) -> None:
self.redis.delete(self.key_prefix + key)undefinedimport json
import time
from typing import Optional, Dict, Any
from dataclasses import dataclass
import redis
from functools import wraps
@dataclass
class IdempotencyRecord:
status: str # 'processing' | 'completed'
response: Optional[Dict[str, Any]] = None
created_at: float = 0
completed_at: Optional[float] = None
class IdempotencyStore:
def init(
self,
redis_client: redis.Redis,
key_prefix: str = "idempotency:",
lock_ttl_ms: int = 60000,
response_ttl_ms: int = 86400000,
):
self.redis = redis_client
self.key_prefix = key_prefix
self.lock_ttl = lock_ttl_ms
self.response_ttl = response_ttl_ms
def get(self, key: str) -> Optional[IdempotencyRecord]:
data = self.redis.get(self.key_prefix + key)
if not data:
return None
parsed = json.loads(data)
return IdempotencyRecord(**parsed)
def acquire_lock(self, key: str) -> bool:
record = {
"status": "processing",
"created_at": time.time(),
}
result = self.redis.set(
self.key_prefix + key,
json.dumps(record),
px=self.lock_ttl,
nx=True,
)
return result is True
def complete(self, key: str, response: Dict[str, Any]) -> None:
record = {
"status": "completed",
"response": response,
"created_at": time.time(),
"completed_at": time.time(),
}
self.redis.set(
self.key_prefix + key,
json.dumps(record),
px=self.response_ttl,
)
def release(self, key: str) -> None:
self.redis.delete(self.key_prefix + key)undefinedFastAPI Middleware
FastAPI 中间件
python
undefinedpython
undefinedfastapi_idempotency.py
fastapi_idempotency.py
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
class IdempotencyMiddleware(BaseHTTPMiddleware):
def init(
self,
app,
store: IdempotencyStore,
header_name: str = "Idempotency-Key",
methods: list = None,
):
super().init(app)
self.store = store
self.header_name = header_name
self.methods = methods or ["POST", "PUT", "PATCH"]
async def dispatch(self, request: Request, call_next):
if request.method not in self.methods:
return await call_next(request)
idempotency_key = request.headers.get(self.header_name)
if not idempotency_key:
return await call_next(request)
full_key = f"{request.method}:{request.url.path}:{idempotency_key}"
# Check existing
existing = self.store.get(full_key)
if existing:
if existing.status == "processing":
raise HTTPException(
status_code=409,
detail="Request with this idempotency key is being processed",
)
if existing.status == "completed" and existing.response:
return JSONResponse(
content=existing.response["body"],
status_code=existing.response["status_code"],
headers={"X-Idempotent-Replayed": "true"},
)
# Acquire lock
if not self.store.acquire_lock(full_key):
raise HTTPException(
status_code=409,
detail="Request with this idempotency key is being processed",
)
try:
response = await call_next(request)
# Cache successful responses
if 200 <= response.status_code < 500:
body = b""
async for chunk in response.body_iterator:
body += chunk
self.store.complete(full_key, {
"status_code": response.status_code,
"body": json.loads(body),
})
return JSONResponse(
content=json.loads(body),
status_code=response.status_code,
)
else:
self.store.release(full_key)
return response
except Exception:
self.store.release(full_key)
raiseundefinedfrom fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
class IdempotencyMiddleware(BaseHTTPMiddleware):
def init(
self,
app,
store: IdempotencyStore,
header_name: str = "Idempotency-Key",
methods: list = None,
):
super().init(app)
self.store = store
self.header_name = header_name
self.methods = methods or ["POST", "PUT", "PATCH"]
async def dispatch(self, request: Request, call_next):
if request.method not in self.methods:
return await call_next(request)
idempotency_key = request.headers.get(self.header_name)
if not idempotency_key:
return await call_next(request)
full_key = f"{request.method}:{request.url.path}:{idempotency_key}"
# 检查已有记录
existing = self.store.get(full_key)
if existing:
if existing.status == "processing":
raise HTTPException(
status_code=409,
detail="使用该幂等键的请求正在处理中",
)
if existing.status == "completed" and existing.response:
return JSONResponse(
content=existing.response["body"],
status_code=existing.response["status_code"],
headers={"X-Idempotent-Replayed": "true"},
)
# 获取锁
if not self.store.acquire_lock(full_key):
raise HTTPException(
status_code=409,
detail="使用该幂等键的请求正在处理中",
)
try:
response = await call_next(request)
# 缓存成功响应
if 200 <= response.status_code < 500:
body = b""
async for chunk in response.body_iterator:
body += chunk
self.store.complete(full_key, {
"status_code": response.status_code,
"body": json.loads(body),
})
return JSONResponse(
content=json.loads(body),
status_code=response.status_code,
)
else:
self.store.release(full_key)
return response
except Exception:
self.store.release(full_key)
raiseundefinedDecorator Pattern
装饰器模式
python
undefinedpython
undefinedidempotent_decorator.py
idempotent_decorator.py
from functools import wraps
def idempotent(store: IdempotencyStore, key_func=None):
"""
Decorator for idempotent functions.
@idempotent(store, key_func=lambda args: args[0].order_id)
async def process_order(order: Order):
...
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# Generate key
if key_func:
key = key_func(args, kwargs)
else:
key = f"{func.__name__}:{hash(str(args) + str(kwargs))}"
# Check existing
existing = store.get(key)
if existing and existing.status == "completed":
return existing.response["result"]
# Acquire lock
if not store.acquire_lock(key):
raise Exception("Operation already in progress")
try:
result = await func(*args, **kwargs)
store.complete(key, {"result": result})
return result
except Exception:
store.release(key)
raise
return wrapper
return decoratorfrom functools import wraps
def idempotent(store: IdempotencyStore, key_func=None):
"""
幂等函数装饰器。
@idempotent(store, key_func=lambda args: args[0].order_id)
async def process_order(order: Order):
...
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# 生成键
if key_func:
key = key_func(args, kwargs)
else:
key = f"{func.__name__}:{hash(str(args) + str(kwargs))}"
# 检查已有记录
existing = store.get(key)
if existing and existing.status == "completed":
return existing.response["result"]
# 获取锁
if not store.acquire_lock(key):
raise Exception("操作正在进行中")
try:
result = await func(*args, **kwargs)
store.complete(key, {"result": result})
return result
except Exception:
store.release(key)
raise
return wrapper
return decoratorUsage
使用示例
@idempotent(store, key_func=lambda args, kwargs: f"order:{kwargs.get('order_id')}")
async def process_payment(order_id: str, amount: float):
return await stripe.charges.create(amount=amount)
undefined@idempotent(store, key_func=lambda args, kwargs: f"order:{kwargs.get('order_id')}")
async def process_payment(order_id: str, amount: float):
return await stripe.charges.create(amount=amount)
undefinedClient-Side Implementation
客户端实现
typescript
// idempotent-client.ts
class IdempotentClient {
private generateKey(): string {
return crypto.randomUUID();
}
async post<T>(url: string, data: unknown, options?: RequestInit): Promise<T> {
const idempotencyKey = this.generateKey();
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
...options?.headers,
},
body: JSON.stringify(data),
...options,
});
if (response.status === 409) {
// Request in progress, wait and retry
await new Promise(resolve => setTimeout(resolve, 1000));
return this.postWithKey(url, data, idempotencyKey, options);
}
return response.json();
}
private async postWithKey<T>(
url: string,
data: unknown,
idempotencyKey: string,
options?: RequestInit,
retries = 3
): Promise<T> {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
...options?.headers,
},
body: JSON.stringify(data),
...options,
});
if (response.status === 409 && retries > 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
return this.postWithKey(url, data, idempotencyKey, options, retries - 1);
}
return response.json();
}
}typescript
// idempotent-client.ts
class IdempotentClient {
private generateKey(): string {
return crypto.randomUUID();
}
async post<T>(url: string, data: unknown, options?: RequestInit): Promise<T> {
const idempotencyKey = this.generateKey();
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
...options?.headers,
},
body: JSON.stringify(data),
...options,
});
if (response.status === 409) {
// 请求正在处理中,等待后重试
await new Promise(resolve => setTimeout(resolve, 1000));
return this.postWithKey(url, data, idempotencyKey, options);
}
return response.json();
}
private async postWithKey<T>(
url: string,
data: unknown,
idempotencyKey: string,
options?: RequestInit,
retries = 3
): Promise<T> {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
...options?.headers,
},
body: JSON.stringify(data),
...options,
});
if (response.status === 409 && retries > 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
return this.postWithKey(url, data, idempotencyKey, options, retries - 1);
}
return response.json();
}
}Best Practices
最佳实践
- Include request details in key: Method + path + idempotency key
- Set appropriate TTLs: Lock TTL < Response TTL
- Handle 409 gracefully: Client should wait and retry
- Don't cache server errors: Allow retry on 5xx
- Use UUIDs for keys: Clients should generate unique keys
- 在键中包含请求详情:请求方法 + 路径 + 幂等键
- 设置合适的TTL:锁的TTL < 响应的TTL
- 优雅处理409响应:客户端应等待后重试
- 不缓存服务端错误:允许对5xx错误进行重试
- 使用UUID作为键:客户端应生成唯一键
Common Mistakes
常见错误
- Using sequential IDs (collisions across users)
- Caching server errors (prevents retry)
- Too short response TTL (client retries get new result)
- Not including request path in key (different endpoints collide)
- Forgetting to release lock on error
- 使用顺序ID(不同用户之间可能冲突)
- 缓存服务端错误(阻止重试)
- 响应TTL过短(客户端重试会得到新结果)
- 键中未包含请求路径(不同端点可能冲突)
- 错误时忘记释放锁
Security Considerations
安全注意事项
- Validate idempotency key format
- Rate limit by idempotency key
- Don't expose internal state in 409 responses
- Consider per-user key namespacing
- 验证幂等键的格式
- 按幂等键进行限流
- 不在409响应中暴露内部状态
- 考虑按用户对键进行命名空间隔离