实现幂等性的常用方式

目录


什么是幂等性

幂等性(Idempotency)是指:同一个操作执行一次和多次,产生的结果是一样的

在分布式系统和网络通信中,幂等性尤为重要,因为网络请求可能超时、失败导致重试,如果没有幂等性保证,重试可能导致数据重复或状态混乱。

示例

操作类型 示例 执行 1 次 执行 10 次 幂等?
幂等操作 x = 5 x = 5 x = 5
非幂等操作 x = x + 1 x = 6 x = 15
幂等操作 DELETE FROM t WHERE id = 1 删除 1 条 删除 0 条(已删除)
非幂等操作 DELETE FROM t LIMIT 1 删除 1 条 删除 10 条

为什么需要幂等性

典型场景

业务 场景
🏦 支付系统 用户支付请求超时,重试不能导致重复扣款
📦 库存扣减 下单请求重试不能重复扣减库存
📬 消息消费 消息队列消息重投不能重复处理
🌐 接口调用 前端网络异常时自动重试接口
🔄 定时任务 任务重启不能重复执行
🔗 服务调用 RPC 调用重试不能导致数据重复

没有幂等性的后果

  • 重复扣款:用户支付 100 元,扣款 300 元
  • 库存超卖:100 件库存,下单 150 件
  • 数据不一致:订单状态混乱,对账失败
  • 业务逻辑错误:优惠券被重复使用

7 种实现方式

1. 数据库唯一约束

原理:利用数据库的唯一索引,保证同一资源不会重复插入或更新。

适用场景:创建订单、注册用户等需要保证数据唯一性的操作。
数据库 服务端 客户端 数据库 服务端 客户端 首次请求 重试请求 发送请求 (order_no=ORD001) INSERT INTO orders (order_no, ...) 成功 返回成功 发送请求 (order_no=ORD001) INSERT INTO orders (order_no, ...) 报错 (唯一约束冲突) 返回"已存在/成功"

实现方式

sql 复制代码
-- 为订单表添加业务唯一索引
CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号唯一',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    amount DECIMAL(10, 2) COMMENT '订单金额',
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '订单状态',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_user_order (user_id, order_no),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

优点

  • ✅ 简单可靠,数据库层面保证
  • ✅ 不需要额外存储
  • ✅ 可靠性最高,不会出现代码漏洞

缺点

  • ❌ 只适用于写操作
  • ❌ 性能受限于数据库索引
  • ❌ 唯一键设计需要谨慎,避免死锁

2. Token 令牌机制

原理:客户端先获取一个 token,执行操作时携带 token,服务端验证 token 后立即标记已使用,重复使用直接拒绝。

适用场景:防止表单重复提交、防止重复下单、验证码等一次性操作。
Redis 服务端 客户端 Redis 服务端 客户端 首次提交 重复提交 GET /token (请求令牌) SET token:uuid-xxx unused 返回 uuid-xxx POST /order (携带 token=uuid-xxx) GET token:uuid-xxx unused SET token:uuid-xxx used (原子操作) 执行业务逻辑 返回成功 POST /order (携带 token=uuid-xxx) GET token:uuid-xxx used 返回"请勿重复提交"

代码示例

java 复制代码
/**
 * 创建订单(幂等)
 * 使用原子操作避免并发问题
 */
@PostMapping
public Result<OrderVO> createOrder(
        @RequestBody CreateOrderDTO order,
        @RequestParam String token) {

    String redisKey = "order:token:" + token;

    // 原子操作:将 unused 标记为 used
    // 只有当 key 存在且值为 "unused" 时才设置成功
    Boolean marked = redisTemplate.opsForValue().compareAndSet(
        redisKey,
        "unused",
        "used"
    );

    if (marked == null || !marked) {
        return Result.fail("请勿重复提交订单或 token 已过期");
    }

    try {
        OrderVO orderVO = orderService.create(order);
        return Result.success(orderVO);
    } catch (Exception e) {
        // 业务失败,可根据逻辑选择是否释放 token
        tokenService.releaseToken(token);
        throw e;
    }
}

优点

  • ✅ 适用于读操作和写操作
  • ✅ 时间窗口可控
  • ✅ 用户体验好

缺点

  • ❌ 需要额外的存储(Redis)
  • ❌ 实现稍复杂

3. 状态机/版本号机制

原理:为资源添加版本号或状态字段,更新时检查版本号,只有版本号匹配才能更新成功。

适用场景:订单状态流转、账户余额更新等有状态变化的操作。
支付成功
发货
确认收货
取消
PENDING
PAID
SHIPPED
COMPLETED
CANCELED

并发更新演示(乐观锁)
数据库 请求 B 请求 A 数据库 请求 B 请求 A SELECT (version=0) SELECT (version=0) UPDATE SET status=PAID, version=1 WHERE version=0 影响行数: 1 (成功) UPDATE SET status=PAID, version=1 WHERE version=0 影响行数: 0 (失败)

优点

  • ✅ 天然防止并发问题(乐观锁)
  • ✅ 不需要额外存储
  • ✅ 适合分布式环境

缺点

  • ❌ 需要业务本身有状态流转
  • ❌ 冲突需要业务处理

4. 分布式锁

原理:对资源加锁,同一时间只有一个请求能执行,其他请求阻塞或直接拒绝。

适用场景:防止并发重复操作、热点数据并发控制。
Redis 请求 B 请求 A Redis 请求 B 请求 A SETNX lock:product:1 (尝试加锁) OK (获取成功) SETNX lock:product:1 (尝试加锁) FAIL (已被占用) 返回"系统繁忙" 执行业务逻辑 (扣库存) DEL lock:product:1 (释放锁) OK

实现建议 :使用 Redisson 的 tryLock 并开启看门狗机制,确保锁的安全。


5. 缓存结果

原理:将操作结果缓存起来,相同请求直接返回缓存结果。

适用场景:读操作、耗时计算、不经常变化的查询。
数据库 Redis 客户端 数据库 Redis 客户端 首次查询 后续查询 GET product:1 null (Miss) SELECT * FROM product WHERE id=1 {id:1, name: "..."} SET product:1 {json} GET product:1 {json} (Hit)

优点

  • ✅ 提升性能,减少数据库压力
  • ✅ 天然幂等

6. 幂等键(Idempotency Key)

原理:客户端为每个请求生成唯一的幂等键,服务端以幂等键为维度存储请求结果。

适用场景:支付系统、API 服务、外部接口调用。
Redis 服务端 客户端 Redis 服务端 客户端 Header: Idempotency-Key: k1 Header: Idempotency-Key: k1 POST /payment (首次) GET k1 null 执行支付业务 SET k1 {result} 返回支付结果 POST /payment (重试) GET k1 {result} 直接返回之前的结果

优点

  • ✅ 灵活控制幂等范围
  • ✅ 业界通用标准 (如 Stripe API)

7. 消息去重表

原理:为消费者创建去重表,记录已处理的消息 ID。

适用场景:消息队列消费、异步任务。
去重表 消费者 消息队列 生产者 去重表 消费者 消息队列 生产者 消息重投 发送消息 (msgId: 001) 投递消息 (msgId: 001) INSERT INTO dedup (msgId) OK 执行业务逻辑 投递消息 (msgId: 001) INSERT INTO dedup (msgId) FAIL (Duplicate Key) 跳过处理


性能对比

方式 性能 可靠性 适用场景
缓存结果 最高 ⭐⭐⭐ 读操作
幂等键 ⭐⭐⭐⭐ API 服务
Token 机制 ⭐⭐⭐⭐ 防重复提交
数据库唯一约束 中等 ⭐⭐⭐⭐⭐ 唯一性保证
状态机/版本号 ⭐⭐⭐⭐ 状态流转
分布式锁 ⭐⭐⭐ 热点并发

原理分析

1. 乐观锁 vs 悲观锁

悲观锁
读取数据时加锁
等待获取锁成功
执行业务操作
释放锁
乐观锁
读取数据时不加锁
检查版本号
更新时校验版本号
冲突则重试/报错

2. 原子操作的重要性

Redis 请求 B 请求 A Redis 请求 B 请求 A 原子操作 (CAS) SET token:1 used IF unused 1 (成功) SET token:1 used IF unused 0 (失败)


进阶:异常处理与全链路幂等

1. 幂等操作的事务一致性

在分布式系统中,幂等标记与业务操作不在同一事务中,需处理不一致性。

解决方案:

  • 手动补偿 :在 catch 块中,如果业务失败,回退/删除幂等标记。
  • 两阶段标记processing -> completed / failed

2. 全链路幂等传递

将幂等键(Idempotency Key)贯穿整个微服务链路。
第三方支付 支付中心 订单中心 客户端 第三方支付 支付中心 订单中心 客户端 createOrder (idKey: k1) pay(orderNo) (idKey: k1) payRequest (out_trade_no: k1)

3. Fencing Token (击剑令牌)

针对分布式锁过期的极端场景,在数据库更新时带上令牌版本。

sql 复制代码
UPDATE account SET balance = balance - 100, last_token = 101 
WHERE id = 1 AND last_token < 101;

不同场景的选择建议

场景 推荐方式
写操作/唯一性 数据库唯一约束
表单提交 Token 机制
状态流转 状态机/版本号
热点并发 分布式锁
API/支付 幂等键
消息消费 消息去重表

幂等性设计原则

  1. 底层兜底:数据库唯一索引是最后防线。
  2. 组合拳:上层防重,底层幂等。
  3. 全链路透传:确保幂等上下文不丢失。
  4. 监控告警:监控冲突率,及时发现攻击或逻辑错误。

总结

幂等性是分布式系统的基石。通过 数据库约束、Token、版本号、锁、缓存、幂等键、去重表 这 7 种武器,配合全链路透传异常补偿,我们可以构建出极其健壮的业务系统。

选择合适的方案,让系统更可靠。 🚀

相关推荐
ALex_zry10 小时前
Redis Cluster 分布式缓存架构设计与实践
redis·分布式·缓存
为什么不问问神奇的海螺呢丶12 小时前
n9e categraf rabbitmq监控配置
分布式·rabbitmq·ruby
TTBIGDATA16 小时前
【Atlas】Atlas Hook 消费 Kafka 报错:GroupAuthorizationException
hadoop·分布式·kafka·ambari·hdp·linq·ranger
m0_6873998418 小时前
telnet localhost 15672 RabbitMQ “Connection refused“ 错误表示目标主机拒绝了连接请求。
分布式·rabbitmq
陌上丨19 小时前
生产环境分布式锁的常见问题和解决方案有哪些?
分布式
新新学长搞科研19 小时前
【智慧城市专题IEEE会议】第六届物联网与智慧城市国际学术会议(IoTSC 2026)
人工智能·分布式·科技·物联网·云计算·智慧城市·学术会议
泡泡以安19 小时前
Scrapy分布式爬虫调度器架构设计说明
分布式·爬虫·scrapy·调度器
没有bug.的程序员20 小时前
RocketMQ 与 Kafka 深度对垒:分布式消息引擎内核、事务金融级实战与高可用演进指南
java·分布式·kafka·rocketmq·分布式消息·引擎内核·事务金融
上海锟联科技21 小时前
250MSPS DAS 在地铁监测中够用吗?——来自上海锟联科技的工程实践
分布式·科技·分布式光纤传感·das解调卡·光频域反射·das
岁岁种桃花儿21 小时前
深度解析DolphinScheduler核心架构:搭建高可用Zookeeper集群
linux·分布式·zookeeper