Spring Boot 接口幂等性完整实现指南

AI Agent 入门(一):什么是 AI Agent?——从概念到第一个 API 调用 阅读详情

适用对象:Java 后端开发工程师、架构师
目标:彻底理解并实现生产级接口幂等性,杜绝重复请求导致的数据错误
核心理念:“同一个请求,多次执行,结果一致”


✅ 一、什么是接口幂等性?(Idempotency)

🔍 定义

接口幂等性(Idempotency)是指:客户端对同一接口发起多次相同请求,服务端的处理结果与仅执行一次完全一致,不会因重复调用产生副作用(如重复扣款、重复下单、重复发短信)。

📌 通俗理解

  • ✅ 幂等操作:像“按一次开关灯”——无论按多少次,灯的状态只有“开”或“关”两种,不会越按越亮。
  • ❌ 非幂等操作:像“按一次加1按钮”——按10次,结果是+10,不是+1。

🧩 举例说明

场景是否幂等原因
GET /api/users/1001✅ 是查询操作,不修改数据,多次调用结果相同
POST /api/orders❌ 否每次调用都会创建新订单,重复调用 = 重复下单
PUT /api/users/1001✅ 是(若设计正确)修改用户信息,即使多次提交,最终状态一致
DELETE /api/users/1001✅ 是(软删除)删除一次后,再次删除仍为“已删除”状态
POST /api/payments❌ 否重复调用 = 重复支付,严重事故

⚠️ 重要结论:
幂等性不是“默认具备”的能力,而是必须主动设计和实现的特性!


✅ 二、哪些接口需要实现幂等性?

✅ 必须实现幂等性的接口(写操作)

接口类型示例为什么需要幂等?
创建类(POST)/api/orders、/api/payments、/api/messages网络抖动、客户端重试、浏览器刷新导致重复创建
更新类(PUT)/api/users/{id}、/api/products/{id}客户端误点多次,导致数据被多次覆盖或状态混乱
删除类(DELETE)/api/orders/{id}(物理删除)重复删除不应报错或产生副作用(推荐软删除)
转账/扣款类/api/wallet/withdraw、/api/transfer重复调用 = 重复扣款,资金损失,法律风险
回调类(异步通知)支付宝/微信支付回调支付平台会多次重试回调,若未幂等,会重复发货/发券

💡 行业标准:
所有非查询类接口(非 GET)都应默认视为“非幂等”,必须主动设计幂等机制!


✅ 三、哪些接口天然具有幂等性?

✅ 天然幂等的操作(无需额外设计)

接口类型说明为什么天然幂等
GET查询操作,如 /api/users/1001不修改任何状态,只读取数据,多次调用结果一致
HEAD获取响应头,不返回体同 GET,仅获取元信息
OPTIONS获取接口支持的方法仅返回元数据,无副作用
安全的 PUT用完整资源替换(全量更新)如 PUT /api/users/1001 传入完整 JSON,多次调用最终状态一致
安全的 DELETE软删除(逻辑删除)如 UPDATE users SET deleted=true WHERE id=1001,重复执行仍为 deleted=true

❌ 注意:不是所有 PUT 都幂等!

# ❌ 非幂等 PUT(部分更新)
PUT /api/users/1001
{ "age": 25 }  # 每次只传 age,其他字段被清空 → 多次调用数据丢失!

# ✅ 幂等 PUT(全量更新)
PUT /api/users/1001
{
  "id": 1001,
  "name": "张三",
  "email": "zhangsan@example.com",
  "age": 25,
  "phone": "13800138000"
}  # 无论调用多少次,最终都是这个完整状态

✅ 建议:
生产环境中,优先使用全量更新(PUT),或使用 PATCH + 语义明确的更新指令(如 {"op": "add", "path": "/age", "value": 25})来保证幂等。


✅ 四、实现接口幂等性的主流方案对比

方案实现方式优点缺点适用场景推荐度
1. 数据库唯一索引在表中加 idempotency_key 字段,设为 UNIQUE数据强一致,无需中间件性能差,高并发下锁竞争严重,扩展性差小流量系统、简单场景⭐⭐
2. 状态字段判断业务表加 status 字段,如 processed=true,先查后改实现简单并发下存在“检查-更新”竞态条件(Race Condition),不安全不推荐用于金融、支付⭐
3. Token 机制(一次性)服务端生成 token,客户端携带,使用后失效可防重放服务端需存储 token,内存/DB 压力大,不支持分布式登录、验证码等场景⭐⭐
4. Redis 缓存 + 客户端 Key客户端生成 UUID 作为幂等键,服务端用 Redis 缓存结果✅ 高性能、分布式、可追溯、支持重试依赖 Redis,需配置 TTL✅ 推荐用于所有写接口⭐⭐⭐⭐⭐
5. MQ 消费幂等消息队列消费端做幂等校验(如 Kafka 消费)解耦、异步需在消费者层实现,复杂度高消息驱动系统⭐⭐⭐⭐

🚫 不推荐方案:前端防重复点击、按钮禁用、setTimeout 防抖 —— 这些完全无法拦截网络层重试、API 被恶意调用!


✅ 五、推荐方案:Idempotency Key + Redis 缓存(业界标准)

✅ 为什么推荐?

  • ✅ 客户端可控:由前端/客户端生成 UUID,服务端不生成,避免单点
  • ✅ 高性能:Redis 内存读写,QPS > 10万+
  • ✅ 分布式支持:Redis 集群可横向扩展
  • ✅ 可追溯:日志中可查 key,便于审计
  • ✅ 支持重试:HTTP 重试自动幂等,用户体验好
  • ✅ 无锁竞争:Redis 是单线程,原子操作

✅ 业界实践:

  • 支付宝:支付请求必须携带 out_biz_no(幂等键)
  • 微信支付:out_trade_no 为幂等键
  • AWS S3:x-amz-idempotency-token
  • Google Cloud:X-Idempotency-Key

✅ 六、完整实现方案(含详细中文注释示例)

本方案包含:

  • 客户端生成幂等键
  • 服务端拦截器自动校验
  • Redis 缓存响应结果
  • AOP 切面统一处理
  • 数据库唯一索引兜底(双重保障)

📁 1. 客户端:生成并携带幂等键(前端/调用方)

前端示例(JavaScript)

// 生成 UUID(客户端)
function generateIdempotencyKey() {
  return 'uuid-' + Math.random().toString(36).substr(2, 9);
}

// 发起支付请求(携带幂等键)
async function pay(amount) {
  const idempotencyKey = generateIdempotencyKey();
  localStorage.setItem('lastIdempotencyKey', idempotencyKey); // 保存用于重试

  const response = await fetch('/api/v1/payments', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey, // ✅ 关键:通过 Header 传递幂等键
    },
    body: JSON.stringify({
      userId: 1001,
      amount: amount,
      orderId: 'ORD-20240520-001'
    })
  });

  return response.json();
}

// 用户点击“支付”按钮,若失败自动重试(带相同 key)
document.getElementById('payBtn').addEventListener('click', async () => {
  try {
    const result = await pay(99.99);
    console.log('支付成功', result);
  } catch (err) {
    // 网络错误,自动重试(使用相同幂等键)
    setTimeout(() => {
      console.log('网络异常,自动重试...');
      pay(99.99); // 重试,但 key 相同 → 服务端自动幂等
    }, 2000);
  }
});

✅ 客户端最佳实践:

  • 使用 UUID v4(随机唯一)
  • 重试时必须复用原始 key,不能重新生成
  • 将 key 存入 localStorage 或 sessionStorage,防止页面刷新丢失

📁 2. 服务端:定义幂等性注解(@Idempotent)

package com.example.demo.annotation;

import java.lang.annotation.*;

/**
 * 幂等性控制注解
 * 标记在 Controller 方法上,自动启用幂等校验
 * 所有写操作(POST/PUT/DELETE)都应使用此注解
 *
 * @author Java后端开发工程师
 */
@Target(ElementType.METHOD)      // 只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,供 AOP 使用
@Documented
public @interface Idempotent {

    /**
     * 幂等键在请求中的字段名
     * 默认从 HTTP Header 中获取:Idempotency-Key
     * 也可配置为 "X-Idempotency-Key"、"Idempotency-Key" 等
     */
    String keyName() default "Idempotency-Key";

    /**
     * 幂等键来源:Header 或 Body
     * 推荐使用 HEADER,因为 Body 可能被读取一次后失效(InputStream 只能读一次)
     */
    IdempotencySource source() default IdempotencySource.HEADER;

    /**
     * 幂等键缓存的过期时间(单位:秒)
     * 支付类建议 2 小时(7200s),订单类建议 24 小时(86400s)
     * 避免无限期缓存,防止内存爆炸
     */
    long ttlSeconds() default 7200; // 默认 2 小时
}

/**
 * 幂等键来源枚举
 * 说明:Header 更安全、更标准,Body 需特殊处理(见后文)
 */
enum IdempotencySource {
    HEADER, // 从 HTTP Header 获取(推荐)
    BODY    // 从 JSON 请求体获取(需封装 RequestWrapper,复杂,仅特殊场景用)
}

📁 3. 服务端:编写幂等性 AOP 切面(核心)

✅ 使用 Spring AOP 拦截所有标记了 @Idempotent 的方法,实现自动幂等控制

package com.example.demo.aspect;

import com.example.demo.annotation.Idempotent;
import com.example.demo.annotation.IdempotencySource;
import com.example.demo.response.ApiResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * 幂等性切面(AOP)
 * 核心逻辑:拦截带 @Idempotent 注解的方法,自动校验幂等键
 * 如果 Redis 中已存在该 key 的响应,则直接返回缓存结果,不再执行业务
 * 如果不存在,则执行业务,成功后将结果缓存到 Redis
 *
 * @author Java后端开发工程师
 */
@Aspect
@Component
public class IdempotencyAspect {

    private static final Logger log = LoggerFactory.getLogger(IdempotencyAspect.class);

    @Autowired
    private StringRedisTemplate redisTemplate; // Redis 操作模板

    @Autowired
    private ObjectMapper objectMapper; // 用于将响应对象序列化为 JSON 字符串

    /**
     * 切点:所有被 @Idempotent 注解标注的方法
     * 表达式说明:
     *   @annotation(com.example.demo.annotation.Idempotent):匹配带有该注解的方法
     */
    @Around("@annotation(idempotent)")
    public Object checkIdempotency(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {

        // 1. 获取当前 HTTP 请求对象(Spring 的 RequestContextHolder)
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 2. 从注解中获取配置参数
        String keyName = idempotent.keyName(); // 默认是 "Idempotency-Key"
        IdempotencySource source = idempotent.source();
        long ttlSeconds = idempotent.ttlSeconds(); // 缓存过期时间

        // 3. 根据来源(HEADER/BODY)获取幂等键
        String idempotencyKey = null;

        if (source == IdempotencySource.HEADER) {
            // ✅ 推荐方式:从 HTTP Header 中获取
            idempotencyKey = request.getHeader(keyName);
        } else if (source == IdempotencySource.BODY) {
            // ⚠️ 不推荐:需封装 RequestWrapper 读取 Body,此处简化处理
            log.warn("【幂等性】Body 方式暂未实现,建议使用 HEADER。当前请求方法:{}", joinPoint.getSignature().getName());
            idempotencyKey = request.getHeader(keyName); // 降级使用 Header
        }

        // 4. 如果未提供幂等键,放行(但建议强制要求)
        if (idempotencyKey == null || idempotencyKey.trim().isEmpty()) {
            log.warn("⚠️ 请求未携带幂等键({}),接口 {} 将直接执行,可能不幂等",
                    keyName, joinPoint.getSignature().toShortString());
            return joinPoint.proceed(); // 继续执行业务
        }

        // 5. 构造 Redis 缓存键:格式:idempotency:{key}
        String cacheKey = "idempotency:" + idempotencyKey;

        // 6. 尝试从 Redis 中读取缓存的响应结果
        String cachedResponseJson = redisTemplate.opsForValue().get(cacheKey);

        if (cachedResponseJson != null) {
            // ✅ 幂等命中:该请求已处理过,直接返回缓存结果
            log.info("✅ 幂等键 [{}] 已存在,直接返回缓存结果,避免重复处理", idempotencyKey);

            // 将 JSON 字符串反序列化为 ApiResponse 对象并返回
            // 注意:这里假设返回的是 ApiResponse<T> 类型
            try {
                ApiResponse<?> response = objectMapper.readValue(cachedResponseJson, ApiResponse.class);
                return response;
            } catch (Exception e) {
                log.error("❌ 反序列化缓存响应失败,key={}", cacheKey, e);
                // 缓存损坏,继续执行业务(兜底)
                return joinPoint.proceed();
            }
        }

        // 7. 未命中缓存:执行原始业务逻辑
        log.info("🚀 幂等键 [{}] 未缓存,开始执行业务逻辑...", idempotencyKey);
        Object result = joinPoint.proceed(); // 执行 Controller 方法

        // 8. 业务执行成功后,将响应结果缓存到 Redis(TTL)
        // 注意:仅当业务成功时才缓存,失败不缓存(下次还可重试)
        if (result instanceof ApiResponse) {
            try {
                // 将 ApiResponse 对象转为 JSON 字符串
                String resultJson = objectMapper.writeValueAsString(result);

                // 缓存到 Redis,TTL 为注解指定时间(如 7200 秒)
                redisTemplate.opsForValue().set(cacheKey, resultJson, ttlSeconds, TimeUnit.SECONDS);

                log.info("💾 幂等键 [{}] 处理成功,响应已缓存,TTL={}s", idempotencyKey, ttlSeconds);

            } catch (Exception e) {
                log.error("❌ 缓存幂等响应失败,key={}", cacheKey, e);
                // 缓存失败不影响业务,但失去幂等保障 → 生产环境建议报警
            }
        }

        // 9. 返回业务处理结果
        return result;
    }
}

✅ 关键设计说明:

  • 缓存的是整个响应对象(如 ApiResponse<Payment>),而非只缓存“成功”标志
  • 缓存内容包含状态码、消息、数据,客户端可直接使用
  • 失败不缓存:避免错误结果被重复返回(如“余额不足”)
  • TTL 设置合理:2小时足够覆盖重试窗口,避免内存泄漏

📁 4. 服务端:统一响应结构(ApiResponse)

所有接口返回统一格式,便于前端解析和缓存

package com.example.demo.response;

import lombok.Data;

import java.time.LocalDateTime;

/**
 * 统一响应封装类(所有接口返回都使用此结构)
 * 优点:前端无需判断不同结构,统一处理 success / code / message / data
 *
 * @author Java后端开发工程师
 */
@Data
public class ApiResponse<T> {

    /**
     * 响应码:200 表示成功,其他为错误码(建议使用业务码,如 4001 用户未登录)
     * 200: 成功
     * 400: 请求参数错误
     * 401: 未授权
     * 404: 资源不存在
     * 500: 服务器内部错误
     * 自定义业务码:如 1001 - 用户已存在
     */
    private Integer code;

    /**
     * 响应信息:对用户友好的提示语
     */
    private String message;

    /**
     * 响应数据:泛型,可为任意类型(List、Object、null)
     */
    private T data;

    /**
     * 请求时间戳,便于链路追踪和日志分析
     */
    private LocalDateTime timestamp;

    /**
     * 构造成功响应(带数据)
     */
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.code = 200;
        response.message = "操作成功";
        response.data = data;
        response.timestamp = LocalDateTime.now();
        return response;
    }

    /**
     * 构造成功响应(无数据)
     */
    public static <T> ApiResponse<T> success() {
        return success(null);
    }

    /**
     * 构造错误响应(带错误码和信息)
     */
    public static <T> ApiResponse<T> error(Integer code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.code = code;
        response.message = message;
        response.timestamp = LocalDateTime.now();
        return response;
    }

    /**
     * 构造系统异常响应(500)
     */
    public static <T> ApiResponse<T> systemError(String message) {
        return error(500, "系统繁忙,请稍后再试:" + message);
    }
}

📁 5. 服务端:业务层(PaymentService)—— 无需关心幂等

业务逻辑只关注“做什么”,幂等性由切面统一处理

package com.example.demo.service;

import com.example.demo.dto.PaymentCreateRequest;
import com.example.demo.entity.Payment;
import com.example.demo.repository.PaymentRepository;
import com.example.demo.response.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

/**
 * 支付服务层
 * 此处无需任何幂等逻辑!
 * 幂等性由 @Idempotent 注解 + AOP 拦截器自动处理
 *
 * @author Java后端开发工程师
 */
@Service
public class PaymentService {

    @Autowired
    private PaymentRepository paymentRepository;

    /**
     * 创建支付订单(核心业务逻辑)
     * 注意:这里不处理幂等!
     * 即使被调用 10 次,也会创建 10 条记录(但 AOP 会阻止!)
     */
    public ApiResponse<Object> createPayment(PaymentCreateRequest request) {
        // 1. 参数校验(业务层面)
        if (request.getAmount() <= 0) {
            return ApiResponse.error(400, "支付金额必须大于0");
        }

        // 2. 创建支付记录
        Payment payment = new Payment();
        payment.setUserId(request.getUserId());
        payment.setAmount(request.getAmount());
        payment.setOrderId(request.getOrderId());
        payment.setStatus("SUCCESS");
        payment.setCreatedAt(LocalDateTime.now());

        // 3. 保存到数据库(此时可能因唯一索引冲突而失败,作为兜底)
        payment = paymentRepository.save(payment);

        // 4. 返回成功响应(会被 AOP 缓存)
        return ApiResponse.success(payment);
    }
}

📁 6. 数据实体类(Payment)

package com.example.demo.entity;

import lombok.Data;

import javax.persistence.*;
import java.time.LocalDateTime;

/**
 * 支付实体类
 * ✅ 关键:增加 idempotency_key 字段,作为数据库兜底
 * 即使 Redis 挂了,数据库唯一索引也能防止重复插入
 *
 * @author Java后端开发工程师
 */
@Entity
@Table(name = "payments")
@Data
public class Payment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long userId;          // 用户ID
    private Double amount;        // 支付金额
    private String orderId;       // 业务订单号
    private String status;        // 状态:SUCCESS / FAILED
    private String idempotencyKey; // ✅ 幂等键(数据库唯一索引兜底)

    private LocalDateTime createdAt;

    // 无参构造函数(JPA 要求)
    public Payment() {}

    // 有参构造函数(方便使用)
    public Payment(Long userId, Double amount, String orderId, String idempotencyKey) {
        this.userId = userId;
        this.amount = amount;
        this.orderId = orderId;
        this.idempotencyKey = idempotencyKey;
        this.status = "PENDING";
    }
}

📁 7. 数据访问层(Repository)—— 数据库唯一索引兜底

package com.example.demo.repository;

import com.example.demo.entity.Payment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * 支付仓库
 * ✅ 在数据库层面设置唯一索引,防止 Redis 失效时的重复插入
 *
 * @author Java后端开发工程师
 */
@Repository
public interface PaymentRepository extends JpaRepository<Payment, Long> {

    // ✅ 数据库兜底:根据幂等键查询是否存在
    Payment findByIdempotencyKey(String idempotencyKey);

    // ✅ 可选:按订单号查询(业务常用)
    Payment findByOrderId(String orderId);
}

✅ 数据库建表语句(MySQL):

CREATE TABLE payments (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  amount DECIMAL(10,2) NOT NULL,
  order_id VARCHAR(64) NOT NULL,
  status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
  idempotency_key VARCHAR(64) UNIQUE NOT NULL, -- ✅ 关键:唯一索引
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

📁 8. 控制器层(Controller)—— 使用注解启用幂等

package com.example.demo.controller;

import com.example.demo.annotation.Idempotent;
import com.example.demo.dto.PaymentCreateRequest;
import com.example.demo.response.ApiResponse;
import com.example.demo.service.PaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/payments")
public class PaymentController {

    @Autowired
    private PaymentService paymentService;

    /**
     * 创建支付订单(幂等接口)
     * 客户端必须在 Header 中携带:Idempotency-Key: uuid-xxxx
     *
     * @Idempotent 注解说明:
     *   keyName = "Idempotency-Key":从 Header 获取
     *   ttlSeconds = 7200:缓存2小时
     *
     * 无论客户端重试多少次,只要 key 相同,服务端只处理一次!
     *
     * ✅ 示例请求:
     * POST /api/v1/payments
     * Header: Idempotency-Key: 8a7e3f1b-9d2c-4e1f-a0b5-6d4c7f8e9a0b
     * Body: { "userId": 1001, "amount": 99.99, "orderId": "ORD-20240520-001" }
     */
    @PostMapping
    @Idempotent(keyName = "Idempotency-Key", ttlSeconds = 7200) // ✅ 启用幂等
    public ApiResponse<Object> createPayment(@RequestBody PaymentCreateRequest request) {
        return paymentService.createPayment(request);
    }
}

📁 9. 配置文件(application.yml)—— Redis 配置

spring:
  redis:
    host: localhost
    port: 6379
    password: # 如果有密码
    timeout: 5000ms
    lettuce:
      pool:
        max-active: 20   # 最大连接数
        max-wait: -1ms   # 等待连接最大时间
        max-idle: 10     # 最大空闲连接
        min-idle: 5      # 最小空闲连接

# 开启 AOP(默认开启)
spring.aop.auto=true

✅ 七、幂等性流程图(客户端 → 服务端)

graph TD
    A[客户端发起请求] --> B{是否携带 Idempotency-Key?}
    B -- 否 --> C[执行业务逻辑,可能重复]
    B -- 是 --> D[查询 Redis: idempotency:{key}]
    D -- 存在 --> E[返回缓存结果,结束]
    D -- 不存在 --> F[执行业务逻辑]
    F --> G{业务是否成功?}
    G -- 是 --> H[将响应结果写入 Redis,TTL=2h]
    G -- 否 --> I[返回错误,不缓存]
    H --> J[返回响应]
    I --> J

✅ 八、测试用例(MockMvc)—— 验证幂等性

package com.example.demo.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
public class PaymentControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void testIdempotentRequest() throws Exception {
        String requestBody = """
            {
              "userId": 1001,
              "amount": 99.99,
              "orderId": "ORD-2024-001"
            }
            """;

        String key = "test-key-001";

        // 第一次请求:创建支付
        mockMvc.perform(post("/api/v1/payments")
                .header("Idempotency-Key", key)
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isOk());

        // 第二次请求:使用相同 key,应返回缓存结果,不创建新记录
        mockMvc.perform(post("/api/v1/payments")
                .header("Idempotency-Key", key)
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isOk())
                .andExpect(result -> {
                    // 可验证响应体与第一次相同
                    // 如:两次响应的 data.id 应该一致(实际是同一个支付记录)
                });
    }
}

✅ 测试结果:

  • 数据库中只插入 1 条记录
  • Redis 中缓存 1 个响应
  • 两次请求都返回 200,且内容一致

✅ 九、生产环境最佳实践总结

项目建议
✅ 幂等键生成客户端生成 UUID v4,保存在本地(localStorage)
✅ 幂等键传输使用 HTTP Header:Idempotency-Key(标准)
✅ 缓存存储Redis,Key 格式:idempotency:{key},TTL 2~24 小时
✅ 数据库兜底在核心表加 UNIQUE(idempotency_key) 字段
✅ 异常处理缓存失败不阻塞业务,但记录日志并告警
✅ 监控监控 Redis 中 idempotency:* key 数量,异常增长报警
✅ 日志记录每个请求的 idempotency_key,便于排查
✅ 前端网络错误时自动重试,不重新生成 key
✅ 文档API 文档中注明“此接口幂等,需携带 Idempotency-Key”

✅ 十、总结:一句话记住幂等性实现

“客户端生成唯一幂等键,服务端用 Redis 缓存响应,数据库加唯一索引兜底”


✅ 附录:常见问题解答(FAQ)

Q1:幂等键能重复使用吗?

✅ 可以,但仅限于相同业务场景。如订单支付成功后,若用户退款,应使用新的幂等键,避免混淆。

Q2:如果 Redis 挂了怎么办?

✅ 数据库唯一索引兜底,保证不重复插入。但响应缓存失效,可能重新执行一次(损失性能,不损失数据)。

Q3:PUT 和 PATCH 哪个更适合幂等?

✅ PUT(全量更新) 更适合幂等。PATCH(部分更新)易因字段遗漏导致状态不一致。

Q4:能否用数据库事务代替幂等?

❌ 不能。事务只能保证原子性,不能防止重复请求。幂等是业务层概念,事务是数据层概念。


🎯 结语

幂等性不是“可有可无”的优化,而是金融级系统、支付系统、订单系统的“生命线”。
一个支付接口重复扣款,可能引发客户投诉、法律诉讼、品牌危机。
用好 Idempotency Key + Redis + 数据库唯一索引,你将构建出真正可靠的后端系统。

复现:Mamba-UNet:降水临近预报的创新解决方案 Mamba-UNet通过其创新的双分支Mamba融合模块、多尺度时空注意力模块和动态分位数加权损失函数,在降水临近预报领域取得了显著的性能提升。它不仅在多种降水场景下表现出色,尤其在预测强降水事件方面展现出了强大的能力。这一创新模型为降水临近预报提供了一种新的解决方案,有望在未来的研究和应用中发挥更大的作用,为应对气候变化带来的挑战提供有力支持。随着技术的不断发展和优化,我们期待Mamba-UNet在更广泛的气象预报任务中发挥更大的价值,为各行各业提供更精准、更可靠的降水预报服务。 阅读详情

相关推荐

嵌入式系统启动流程解析:ROM Code、Bootloader与启动代码的协同工作

本文深入解析嵌入式系统从通电到运行的完整启动流程,详细阐述了ROM Code、Bootloader与启动代码三者如何像接力赛般协同工作。ROM Code作为芯片固化的初始引导程序,负责硬件自检并加载Bootloader;Bootloader作为系统总指挥,初始化关键硬件并加载操作系统内核;启动代码则为C语言运行环境铺平道路。理解这三者的分工与协作,是进行系统移植和解决启动问题的关键。

dd012的博客 553

什么是接口的幂等性,如何实现接口幂等性?一文搞定

同一个接口,多次发出同一个请求,必须保证操作只执行一次。

Java鱼仔的博客 2万+

Maltab GUI课程设计——网络爬虫程序(基金连涨连跌提醒)

Maltab GUI课程设计——网络爬虫程序(基金连涨连跌提醒)

果果爱腰果的博客 848

Spring Boot 实现接口幂等性的 4 种方案!还有谁不会?

点击关注公众号,Java干货及时送达作者:超级小豆丁链接:mydlq.club/article/94一、什么是幂等性幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元...

Java技术栈,分享最主流的Java技术 1万+

MT6768平台Sensor Bring Up全流程实战:从驱动添加到问题排查(附避坑指南)

本文详细介绍了联发科MT6768平台Sensor Bring Up的全流程实战指南,从驱动添加、硬件配置到问题排查,提供了全面的避坑建议。内容涵盖环境准备、驱动文件集成、硬件接口配置、编译系统适配以及常见问题解决方案,帮助开发者高效完成传感器集成与调试工作。

weixin_30374009的博客 416

【原创,解决CLodop安装后无法调用的问题】已经安装过CLodop,但是网页上仍然提示“clodop云打印服务(localhost本地)未安装启动,点击这里执行安装....”

在某些国内web网站中,需要安装一个名为“CLodop”的插件;但在实际使用过程中,即使安装了CLodop插件,网页试图调用它时还是会提示“clodop云打印服务(localhost本地)未安装启动,点击这里执行安装...”,且无法完成相关打印任务 本文属原创博客,分析了解决CLodop无法被调用的问题,并附上了原理和解决方法,本质上是和跨域请求以及CORS类安全组策略相关

link_in_csdn的博客 2万+

【Spring Boot】实现接口幂等性的 4 种方案

在HTTP/1.1中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。这里的副作用是不会对结果产生破坏或者产生不可预料的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。幂等性是开发当中很常见也很重要的一个需求,尤其是支付、订单等与金钱挂钩的服务,保证接口幂等性尤其重要。

weixin_74412978的博客 2089

RT-Thread实战:手把手教你用STM32F04ZGT6通过SPI驱动BMI088(附完整工程)

本文详细介绍了如何使用RT-Thread操作系统在STM32F04ZGT6上通过SPI接口驱动BMI088高性能6轴惯性传感器(包含陀螺仪和加速度计)。从硬件连接到软件配置,再到数据采集与处理,提供了完整的开发指南和优化技巧,帮助开发者快速实现精确的姿态测量功能。

kappa的博客 236

说说 Spring Boot 实现接口幂等性有哪几种方案?

简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求(Token 最好将其放到 Headers 中),后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。Put 方法一般用于修改资源。所以在使用时候需要考虑是否引入幂等性的必要性,根据实际业务场景具体分析,除了业务上的特殊要求外,一般情况下不需要引入的接口幂等性。

qq_39940634的博客 2438

OpenAI Codex 科研实战:从文献整理到论文初稿的自动化流水线

在科研工作中,大量时间消耗在文献整理、数据清洗、结果反复排版等重复性任务上,而非真正的思考与创新。AI 编程代理作为一种能理解自然语言指令并直接操作文件、执行脚本的命令行工具,正逐渐改变科研工作流的组织方式。它基于云端大模型推理,本地无需 GPU,通过解析用户意图,自动完成代码生成、文件批量处理与任务拆解,将传统需要手动操作的工程过程转化为可验证、可追溯的指令执行。这类工具在学术场景中的价值尤为突出:科研人员可以将文献总结、数据预处理、统计分析与可视化出图交给代理完成,从而把精力聚焦在研究问题本身。Open

weixin_34318326的博客 458

【Unity3D框架】GameFramework框架完全教程《全网最全》-专栏文章目录

最近准备学习框架,了解到GameFramework框架是国产Unity3D框架,准备学习一下,但是奈何全网关于这个框架的教程实在太少。为了让自己学习方便一些,也为了让后来者乘凉。将框架以自己的理解整理目录,并且补全教程。觉得教程有帮助别忘了一键三连哦。

优快云博客专家,Unity3D高级软件工程师,《Unity 3D从入门到实战》作者。 2万+

spring boot实现接口幂等性操作

spring boot 结合redis实现接口幂等前言:1.新建redis工具类2. 自定义注解AutoIdempotent3. token创建和检验3.1 token服务接口3.2 token的服务实现类4. 配置拦截器4.1 web配置类4.2 拦截处理器5.测试6.总结 前言: 在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,

意田天的博客 1万+

基于Simulink的雷达跟踪系统仿真(附源码)

此示例包含对雷达系统的基本特征进行建模的子系统。该模型是用于目标位置和速度检测的雷达系统的典型模型。该示例包括雷达脉冲发生器、射频发射器子系统、移动目标的 Simulink 表示、射频接收器和接收模块(Rx 模块)。

珞瑜的博客 3512

Spring Boot 实现接口幂等性的 4 种方案

在HTTP/1.1中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。这里的副作用是不会对结果产生破坏或者产生不可预料的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。幂等性是开发当中很常见也很重要的一个需求,尤其是支付、订单等与金钱挂钩的服务,保证接口幂等性尤其重要。对于下单等存在唯一主键的,可以使用“唯一主键方案”的方式实现。...

LoveSummer 1万+

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

接口幂等性校验

接口幂等性校验   一、概念 幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。 在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。 这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和set...

huchao_lingo的博客 1万+

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Spring Boot实现接口幂等性的四种解决方案

幂等性是开发中常见的一个需求,尤其在支付、订单等与金钱挂钩的服务,保证接口的幂等性尤其重要。①对于下单等存在唯一主键的,可以使用“唯一主键方案”的方式实现②对于更新订单状态等相类似的更新场景操作时,可以使用“乐观锁方案”实现更简单③对于上下游这种,下游请求上游,上游服务可以使用“下游传递唯一序列号方案”更合适④类似前端重复提交,重复下单,没有唯一id号的场景,可以通过Token与Redis配合的“防重Token方案”实现更加便捷。

小白先生的博客 1308

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

spring boot 实现幂等性

spring boot 实现幂等性 背景 在分布式服务中,业务在高并发的情况下会消费者出现多次请求的情况.这个时候如果执行插入的业务操作,则数据库中出现多条数据.造成脏数据的产生.而且也是对资源的浪费. 此时我们需要做的就是阻止多页业务的处理操作. 执行方案 实现接口的幂等性,让请求只成功一次.这里需要保存一个唯一标示,在下一个请求执行时获取标示如果重复提交则阻止执行. 代码实现 创建一个自定义异...

壮哥哥 5402

Spring Boot 实现接口幂等性的 4 种方案!

幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。[在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。在 HTTP/1.1 中,对幂等性进行了定义。

2301_79727522的博客 842

Spring-Boot-+-Redis-实现接口幂等性,看这篇就太好了!

幂等性的概念是,任意多次执行所产生的影响都与一次执行产生的影响相同,按照这个含义,最终的解释是对数据库的影响只能是一次性的,不能重复处理。手段如下数据库建立唯一索引token机制悲观锁或者是乐观锁先查询后判断小小主要带你们介绍Redis实现自动幂等性。其原理如下图所示。自定义一个注解,定义此注解的目的是把它添加到需要实现幂等的方法上,只要某个方法注解了其,都会自动实现幂等操作。其代码如下。

2401_83329718的博客 776

Spring Boot 接口幂等性实现的 4 种方案!

来源:mydlq.club/article/94/一、什么是幂等性二、什么是接口幂等性三、为什么需要实现幂等性四、引入幂等性后对系统的影响五、Restful API 接口的幂等性六、如何实...

ITMuch的专栏 181

【SpringBoot】Spring Boot 实现接口的幂等性

调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求(Token 最好将其放到 Headers 中),后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。针对客户端连续点击或者调用方的超时重试等情况,例如:提交订单,此种操作就可以用 Token 的机制实现防止重复提交。

sco5282的博客 926
上一篇: Spring Boot 接口开发设计原则与最佳实践指南
下一篇: 1、Java 后端开发者设计模式系统学习指南
龙茶清欢
博客等级 码龄7年 3720粉丝 375原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值