idempotency

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Idempotent 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
undefined
python
undefined

idempotency.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)
undefined
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)
undefined

FastAPI Middleware

FastAPI 中间件

python
undefined
python
undefined

fastapi_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)
        raise
undefined
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}"

    # 检查已有记录
    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)
        raise
undefined

Decorator Pattern

装饰器模式

python
undefined
python
undefined

idempotent_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 decorator
from 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 decorator

Usage

使用示例

@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)
undefined

Client-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

最佳实践

  1. Include request details in key: Method + path + idempotency key
  2. Set appropriate TTLs: Lock TTL < Response TTL
  3. Handle 409 gracefully: Client should wait and retry
  4. Don't cache server errors: Allow retry on 5xx
  5. Use UUIDs for keys: Clients should generate unique keys
  1. 在键中包含请求详情:请求方法 + 路径 + 幂等键
  2. 设置合适的TTL:锁的TTL < 响应的TTL
  3. 优雅处理409响应:客户端应等待后重试
  4. 不缓存服务端错误:允许对5xx错误进行重试
  5. 使用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响应中暴露内部状态
  • 考虑按用户对键进行命名空间隔离