10.接口幂等

一、 问题概述与核心思路

在分布式系统中,由于网络抖动重试前端重复提交多点线程并发等原因,完全相同的请求可能会在短时间内多次到达服务端。幂等性设计的核心目标是:确保对于同一业务请求的多次调用,其对系统资源状态造成的影响,与仅调用一次完全一致

实现的关键在于,为每个请求赋予一个全局唯一的业务标识(如订单号、支付流水号或幂等Token),并在处理流程中基于该标识进行判重和协调。

二、 分层防御策略与具体实现

以下防御策略可组合使用,构成纵深防御体系。

1. 网关层:快速过滤

网关层作为流量的第一入口,适合进行粗粒度的重复请求拦截。

  • 实现原理:提取请求的“指纹”,通常由 IP + 请求路径 + 核心业务参数(如用户ID+订单ID) 经过MD5或SHA生成。将此指纹作为Key,在Redis中设置一个短暂的过期时间(如1-5秒)。
  • 代码示例
    // 伪代码示例:使用Spring Cloud Gateway的全局过滤器
    public class DedupFilter implements GlobalFilter {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            String requestFingerprint = generateFingerprint(exchange.getRequest());
            // 使用SETNX原子操作,只有第一个请求能设置成功
            Boolean isAbsent = redisTemplate.opsForValue().setIfAbsent("GATEWAY_DEDUP:" + requestFingerprint, "", Duration.ofSeconds(2));
            if (Boolean.TRUE.equals(isAbsent)) {
                return chain.filter(exchange); // 放行
            } else {
                exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUERTS);
                return exchange.getResponse().setComplete(); // 拦截
            }
        }
    }
    
  • 注意事项:此方法适用于对时间窗内完全相同的请求进行去重,无法处理如“增加积分”这类参数相同但业务逻辑不允许重复的请求。
2. 接口层:幂等Token与分布式锁

这是实现业务幂等性的核心层。

  • a) 幂等Token机制:适用于需要明确防止重复提交的场景,如支付。
    1. 申请Token:客户端在执行业务操作前(如进入支付页面),先调用服务端接口获取一个全局唯一的Token。
    2. 提交请求:客户端携带此Token发起业务请求。
    3. 校验Token:服务端通过Redis的 SET key value NX(原子性命令)判断此Token是否存在。若存在(表示是第一次请求),则删除Token并执行业务;若不存在,则直接返回重复请求的结果。
  • b) 分布式锁:适用于控制对同一关键资源的并发访问,例如防止同一用户同时发起多笔付款。
    • 实现原理:以业务唯一标识(如订单ID)为Key,使用Redis的 SETNXRedisson 等框架尝试获取锁。必须为锁设置合理的过期时间,防止死锁。操作完成后释放锁,但要注意判断释放的是不是自己的锁。
    • 代码示例(Lua脚本保证原子性)
      -- 加锁Lua脚本
      local key = KEYS[1]
      local requestId = ARGV[1] -- 唯一请求ID,用于安全释放
      local expireTime = ARGV[2]
      if redis.call('setnx', key, requestId) == 1 then
          redis.call('expire', key, expireTime)
          return 1 -- 成功
      else
          return 0 -- 失败
      end
      
3. 业务层:状态机与乐观锁

这是保证业务逻辑正确性的最后防线。

  • a) 状态机:适用于订单等有明确状态流转的业务。
    • 实现原理:在更新状态时,不仅校验当前状态,更要在SQL的WHERE条件中指定前置状态。例如,将订单状态从“未支付”更新为“已支付”:
      UPDATE `order` SET status = 'PAID' WHERE order_id = '123' AND status = 'UNPAID';
      
      此操作天然幂等,因为第二次执行时,由于状态已不是UNPAID,影响行数为0。
  • b) 乐观锁:适用于更新操作,防止并发下的数据覆盖。
    • 实现原理:在数据表中增加一个version版本号字段。更新时,携带版本号作为条件。
      UPDATE account SET balance = balance - 100, version = version + 1 
      WHERE account_id = 'A' AND version = 1;
      
      如果版本号不匹配,更新会失败,需要业务逻辑进行重试或报错。
4. 数据层:最终屏障
  • 唯一索引:这是最直接、最有效防止数据重复的方法。例如,为订单表的order_no字段建立唯一索引,可以从数据库层面杜绝重复订单的创建。
  • 去重表:对于没有唯一业务ID的复杂场景,可以单独创建一张“去重表”,将需要去重的业务字段组合作为唯一索引。在处理请求前,先向该表插入一条记录,插入成功则继续后续业务,插入失败则说明是重复请求。

三、 支付场景幂等方案示例

结合以上策略,一个典型的支付接口幂等方案如下:

  1. 生成幂等Token:用户进入收银台时,前端调用后端接口生成一个全局唯一的idempotentToken,并存入Redis(过期时间稍长,如30分钟)。
  2. 支付请求:前端发起支付,在请求头或参数中携带idempotentTokenorderId
  3. Token校验(接口层):使用Lua脚本原子性地校验并删除Token。
    // 伪代码:使用RedisTemplate执行Lua脚本
    String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                       "return redis.call('del', KEYS[1]) " +
                       "else return 0 end";
    Long result = redisTemplate.execute(script, Arrays.asList("PAY_TOKEN:" + orderId), idempotentToken);
    if (result == 0) { // Token不存在或已使用
        throw new IdempotentException("请勿重复支付");
    }
    
  4. 获取分布式锁(业务层):以orderId为Key获取分布式锁,防止针对同一订单的极高并发。
  5. 状态机校验(业务层):在事务中,执行状态机更新:
    UPDATE `order` SET status = 'PAID' WHERE order_id = #{orderId} AND status = 'UNPAID';
    
    如果更新成功,才进行扣款等后续操作。
  6. 数据层保证:支付流水表通过order_id的唯一索引做最终防护。

四、 消息队列重复消费的幂等处理

对于来自Kafka等消息队列的重复消息,其解决方案与HTTP请求类似:

  • 核心:在消费者端实现幂等性。
  • 方案:为每条消息生成一个全局唯一的MessageId(或在生产端由业务方注入幂等Key)。消费者在处理消息前,先查询Redis或数据库,判断该MessageId是否已被处理过。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值