关于 idempotency-handling
帮助开发者实现幂等处理,确保重复执行操作时结果一致。内容包括键值与数据库存储模式,以及过期记录的清理方法。
主要功能
- 01在请求处理中使用幂等键
- 02在数据库记录请求与响应
- 03校验请求载荷哈希
- 04过期与清理策略
- 05原子操作建议
适用场景
- 01避免支付被重复处理
- 02处理重复的订单创建请求
- 03对 Webhook 或邮件操作去重
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ secondsky/ claude-skills --skill idempotency-handling安装后在 Claude Code 中输入/idempotency-handling确认已加载。
详细介绍
幂等性处理
确保无论执行多少次,操作产生的结果都相同。
幂等键模式
const redis = require('redis');
const client = redis.createClient();
async function idempotencyMiddleware(req, res, next) {
const key = req.headers['idempotency-key'];
if (!key) return next();
const cached = await client.get(`idempotency:${key}`);
if (cached) {
const { status, body } = JSON.parse(cached);
return res.status(status).json(body);
}
// Store original send
const originalSend = res.json.bind(res);
res.json = async (body) => {
await client.setEx(
`idempotency:${key}`,
86400, // 24 hours
JSON.stringify({ status: res.statusCode, body })
);
return originalSend(body);
};
next();
}
基于数据库的幂等性
CREATE TABLE idempotency_keys (
key VARCHAR(255) PRIMARY KEY,
request_hash VARCHAR(64) NOT NULL,
response JSONB,
status VARCHAR(20) DEFAULT 'processing',
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP DEFAULT NOW() + INTERVAL '24 hours'
);
CREATE INDEX idx_idempotency_expires ON idempotency_keys(expires_at);
async function processPayment(idempotencyKey, payload) {
const requestHash = crypto.createHash('sha256')
.update(JSON.stringify(payload)).digest('hex');
// Try to insert with 'processing' status - only one request will succeed
const insertResult = await db.query(
`INSERT INTO idempotency_keys (key, request_hash, status)
VALUES ($1, $2, 'processing')
ON CONFLICT (key) DO NOTHING
RETURNING *`,
[idempotencyKey, requestHash]
);
// If we inserted the row (rowCount === 1), we're responsible for processing
if (insertResult.rowCount === 1) {
try {
// Execute the payment
const result = await executePayment(payload);
// Update to completed with response
await db.query(
'UPDATE idempotency_keys SET status = $1, response = $2 WHERE key = $3',
['completed', JSON.stringify(result), idempotencyKey]
);
return result;
} catch (error) {
// Mark as failed on error
await db.query(
'UPDATE idempotency_keys SET status = $1, response = $2 WHERE key = $3',
['failed', JSON.stringify({ error: error.message }), idempotencyKey]
);
throw error;
}
}
// Another request is/was processing this key - check status
const existing = await db.query(
'SELECT * FROM idempotency_keys WHERE key = $1',
[idempotencyKey]
);
const row = existing.rows[0];
if (!row) {
throw new Error('Unexpected: idempotency key vanished');
}
// Verify request hasn't changed
if (row.request_hash !== requestHash) {
throw new Error('Idempotency key reused with different request');
}
// Check status
if (row.status === 'completed') {
return JSON.parse(row.response);
} else if (row.status === 'processing') {
throw new Error('Request already processing - retry later');
} else if (row.status === 'failed') {
const failedResponse = JSON.parse(row.response);
throw new Error(`Previous attempt failed: ${failedResponse.error}`);
}
throw new Error(`Unknown status: ${row.status}`);
}
适用场景
- 支付处理
- 创建订单
- Webhook 处理
- 发送电子邮件
- 任何重复操作会导致问题的场景
最佳实践
- 对变更操作要求提供幂等键
- 验证请求正文是否与已存储的请求匹配
- 设置合适的 TTL(通常为 24 小时)
- 使用原子数据库操作
- 实施清理任务,防止表过度膨胀
TTL 清理策略
为防止表无限增长,请定期清理已过期的键:
选项 1:定时数据库任务(PostgreSQL)
-- Run hourly via pg_cron or external scheduler
DELETE FROM idempotency_keys
WHERE expires_at < NOW()
LIMIT 1000; -- Batch delete to avoid long locks
选项 2:应用清理任务(Node.js)
// Run via cron or job scheduler (e.g., node-cron, Bull)
async function cleanupExpiredKeys() {
try {
const result = await db.query(
'DELETE FROM idempotency_keys WHERE expires_at < NOW()'
);
console.log(`Cleaned up ${result.rowCount} expired idempotency keys`);
} catch (error) {
console.error('Cleanup job failed:', error);
}
}
// Schedule to run every hour
cron.schedule('0 * * * *', cleanupExpiredKeys);
选项 3:应用清理任务(Python)
import asyncio
from datetime import datetime
async def cleanup_expired_keys():
"""Remove expired idempotency keys to prevent table bloat."""
try:
result = await db.execute(
"DELETE FROM idempotency_keys WHERE expires_at < $1",
datetime.now()
)
print(f"Cleaned up {result} expired idempotency keys")
except Exception as e:
print(f"Cleanup job failed: {e}")
# Run with APScheduler, Celery, or similar
# scheduler.add_job(cleanup_expired_keys, 'interval', hours=1)
清理最佳实践:
- 在流量较低的时段运行清理,以尽量减少锁竞争
- 对大型表使用批量删除(
LIMIT 1000) - 监控清理任务的执行情况和失败情况
- 考虑按 created_at 对表进行分区,以便更轻松地清理
- 如果表大小出现异常增长,请设置告警
评论
支持 Markdown