幂等性设计模式:幂等键、去重表、状态机、乐观锁、分布式锁、幂等性中间件

各位同学,今天我们来聊聊幂等性。这个词听起来有点学术,说白了就是——同一个操作,无论你调用多少次,结果都是一样的。我刚开始做分布式系统时,觉得这玩意儿可有可无。直到有一次线上事故,用户重复下单,库存扣了两次,财务对账对了一整夜……嗯,从那以后,我再也不敢忽视幂等性了。

幂等性不是某个单一的技术,而是一套组合拳。今天我把常用的六种模式串起来讲,你想想看,它们各自适合什么场景,又有什么坑。

核心观点:幂等性设计的本质,是把“非幂等操作”转化为“幂等操作”。不是所有接口天生幂等,但我们可以通过模式让它变得幂等。

1. 幂等键(Idempotency Key)

这是最直观的模式。客户端每次请求时,带上一个全局唯一的键。服务端收到请求后,先查这个键是否处理过。处理过就直接返回上次结果,没处理过就正常执行并记录。

我在项目中遇到过支付回调的场景。第三方支付平台会多次回调同一个订单,如果不做幂等,用户可能被重复扣款。我的做法是:用订单号+支付流水号拼接成幂等键,存到Redis里,设置24小时过期。

// 伪代码示例
public Response handlePayment(PaymentRequest request) {
    String idempotentKey = request.getOrderId() + "_" + request.getTransactionId();
    
    // 检查是否已处理
    if (redisClient.exists(idempotentKey)) {
        return redisClient.get(idempotentKey); // 返回上次结果
    }
    
    // 执行核心逻辑
    Response result = doPayment(request);
    
    // 记录结果,设置过期时间
    redisClient.setex(idempotentKey, 86400, result);
    return result;
}

小技巧:幂等键的过期时间要合理设置。太短可能重复处理,太长浪费存储。我个人习惯设置为业务超时时间的2倍。

2. 去重表(Deduplication Table)

去重表是幂等键的数据库版本。说白了,就是在数据库里建一张去重表,用唯一索引来保证幂等性。

举个例子,用户注册场景。用户点击注册按钮时,前端生成一个请求ID。后端收到请求后,先往去重表里插入一条记录(请求ID作为唯一键)。如果插入成功,说明是第一次请求,正常处理。如果插入失败(唯一键冲突),说明是重复请求,直接返回已处理。

CREATE TABLE idempotent_records (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    request_id VARCHAR(64) NOT NULL UNIQUE,
    status TINYINT NOT NULL DEFAULT 0,
    result TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 插入时捕获唯一键冲突
INSERT INTO idempotent_records (request_id, status) VALUES (?, 0);
-- 如果成功,继续业务逻辑
-- 如果抛出DuplicateKeyException,说明重复请求

注意:去重表需要定期清理,否则数据会无限膨胀。我曾经见过一个系统,去重表积累了上亿条数据,查询性能急剧下降。建议设置定时任务,清理超过业务超时时间的记录。

3. 状态机(State Machine)

状态机模式适用于有明确状态流转的业务。比如订单状态:待支付 → 已支付 → 已发货 → 已完成。每个状态只能向前流转,不能回退。

为什么状态机能保证幂等?因为同一个状态转换操作,只有第一次会生效。比如“支付成功”这个事件,订单状态从“待支付”变成“已支付”。如果重复触发,状态机发现当前状态已经是“已支付”,就不会再执行转换逻辑。

public enum OrderState {
    PENDING_PAYMENT,
    PAID,
    SHIPPED,
    COMPLETED;
    
    public OrderState next(Event event) {
        switch (this) {
            case PENDING_PAYMENT:
                if (event == Event.PAY_SUCCESS) return PAID;
                break;
            case PAID:
                if (event == Event.SHIP) return SHIPPED;
                break;
            // ... 其他状态转换
        }
        throw new IllegalStateException("Invalid state transition");
    }
}

我个人习惯在状态机里加一个版本号字段。每次状态变更时,检查版本号是否匹配。这样能防止并发场景下的状态覆盖问题。

4. 乐观锁(Optimistic Lock)

乐观锁适合读多写少的场景。它的核心思想是:更新数据时,带上一个版本号或时间戳。只有版本号匹配时,更新才成功。

你想想看,如果两个人同时操作同一条数据,谁先提交谁成功,后提交的会因为版本号不匹配而失败。这不就是天然的幂等吗?

-- 更新时带上版本号
UPDATE account 
SET balance = balance - 100, version = version + 1 
WHERE id = ? AND version = ?;

-- 如果影响行数为0,说明版本号不匹配,重试或报错

避坑指南:我曾经在库存扣减场景里用过乐观锁,但发现高并发下失败率太高。后来改成了“预扣库存+异步对账”的方案。乐观锁不是万能的,要根据业务选型。

5. 分布式锁(Distributed Lock)

分布式锁是解决并发幂等问题的利器。多个节点同时处理同一个请求时,只有拿到锁的节点才能执行,其他节点等待或放弃。

常用的分布式锁实现有Redis的SETNX、ZooKeeper的临时节点、数据库的悲观锁等。我个人偏爱Redis的Redlock算法,虽然实现复杂一点,但可靠性高。

// Redis分布式锁示例
String lockKey = "lock:order:" + orderId;
String requestId = UUID.randomUUID().toString();

// 尝试获取锁,过期时间10秒
Boolean locked = redisClient.setnx(lockKey, requestId, 10, TimeUnit.SECONDS);
if (locked) {
    try {
        // 执行业务逻辑
        processOrder(orderId);
    } finally {
        // 释放锁,只释放自己的锁
        if (requestId.equals(redisClient.get(lockKey))) {
            redisClient.del(lockKey);
        }
    }
} else {
    // 获取锁失败,重试或返回
}

注意:分布式锁不是银弹。锁的过期时间设置不当,可能导致死锁或锁提前释放。我建议在业务代码里加一个“续期”机制,防止业务执行时间超过锁的过期时间。

6. 幂等性中间件(Idempotency Middleware)

如果每个业务都自己实现幂等,那代码就太冗余了。成熟的团队会把幂等能力封装成中间件,对业务透明。

幂等性中间件通常包含以下几个组件:

  • 拦截器:在请求入口处拦截,提取幂等键
  • 存储层:存储已处理的请求和结果(Redis或数据库)
  • 一致性保障:确保幂等键的写入和业务操作在同一个事务里
  • 过期策略:自动清理过期数据

我参与过一个项目,把幂等中间件做成了Spring Boot Starter。业务方只需要在接口上加一个@Idempotent注解,配置幂等键的提取规则,剩下的交给中间件处理。效果非常好,开发效率提升了一大截。

@Idempotent(key = "#request.orderId", expire = 3600)
public Response createOrder(CreateOrderRequest request) {
    // 业务逻辑,不需要关心幂等
    return doCreateOrder(request);
}

知识体系总览

下面这张图,我把六种模式的关系和适用场景梳理了一下。你保存下来,以后做架构设计时可以参考。

幂等性设计模式知识体系 幂等性设计模式 幂等键 客户端生成唯一键 去重表 数据库唯一索引 状态机 状态单向流转 乐观锁 版本号/时间戳 分布式锁 Redis/ZooKeeper 幂等中间件 注解/拦截器封装 选择哪种模式?看业务场景:幂等键适合API调用,去重表适合数据库操作,状态机适合流程控制 乐观锁适合并发更新,分布式锁适合跨节点协调,中间件适合团队标准化

如何选择?

说了这么多,你可能会问:到底该用哪种?我的建议是:

  • API接口幂等:优先用幂等键,配合Redis存储。简单高效。
  • 数据库操作幂等:去重表或乐观锁。去重表适合插入场景,乐观锁适合更新场景。
  • 有状态流转的业务:状态机模式。比如订单、审批流程。
  • 跨节点并发控制:分布式锁。但要注意性能和死锁问题。
  • 团队标准化:封装成幂等中间件,统一管理。

最后说一句:幂等性设计不是一蹴而就的。我见过很多系统,一开始没做幂等,后来出了问题才补。与其事后补救,不如在设计阶段就把幂等性考虑进去。毕竟,一个可靠的系统,不是靠运气运行的。

公众号:蓝海资料掘金营,微信deep3321