当前位置: 首页
编程语言
SpringBoot中接口幂等性的5种实现方法详解及代码示例

SpringBoot中接口幂等性的5种实现方法详解及代码示例

热心网友 时间:2026-07-21
转载

摘要介绍了接口幂等性的概念与重要性,分析了网络波动、客户端重试等引发重复请求的场景,并指出支付、订单创建等接口对幂等性的高要求。重点阐述了四种实现方案:基于数据库唯一索引、Token令牌、分布式锁及Redis原子操作,分别说明其原理、优劣势与适用场景。

1. 幂等性的概念与重要性

1.1 什么是幂等性

幂等性(Idempotence)这个概念听起来有些学术化,但通俗来讲就是:无论你对同一个操作执行多少次,最终产生的结果都是一致的。在API设计领域,这一特性就像一道安全防线,确保重复请求不会引发意外的副作用——比如同一个订单被重复扣款,或者同一个用户被重复创建。

1.2 为什么需要幂等性

在实际的生产环境中,触发重复请求的场景非常普遍,归纳起来主要有以下几种:

  • 网络波动:网络不稳定会导致请求重复发送,服务端可能会收到两次相同的指令。
  • 客户端重试:客户端在超时后,通常会自动重试请求,此时后端必须具备判断能力。
  • 用户误操作:用户可能因为手抖而连续点击提交按钮,瞬间产生多个请求。
  • 分布式系统:在分布式环境下,消息中间件可能重复投递消息,导致消费方收到重复数据。

1.3 幂等性的应用场景

几乎每一个正经的接口都需要考虑幂等性,以下场景尤其敏感:

  • 支付接口
  • 订单创建接口
  • 数据更新接口
  • 资源创建接口

2. 实现方案

方案一:基于数据库唯一索引

实现原理

这个思路最为朴素:在数据库表里添加一个唯一索引,让数据库本身帮我们拦截重复的插入操作。如果两个请求的订单号相同,第二次插入就会触发唯一索引冲突,捕获异常后直接返回已存在的数据即可。

核心源码

数据库表设计

CREATE TABLE `order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(32) NOT NULL COMMENT '订单号',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `amount` decimal(10,2) NOT NULL COMMENT '金额',
  `status` int(11) NOT NULL COMMENT '状态',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`) COMMENT '订单号唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

Service层实现

@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    
    @Transactional
    public Order createOrder(OrderCreateRequest request) {
        // 生成唯一订单号
        String orderNo = generateOrderNo();
        
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setUserId(request.getUserId());
        order.setAmount(request.getAmount());
        order.setStatus(0);
        order.setCreateTime(new Date());
        
        try {
            orderMapper.insert(order);
            return order;
        } catch (DuplicateKeyException e) {
            // 唯一索引冲突,说明订单已存在
            return orderMapper.selectByOrderNo(orderNo);
        }
    }
    
    private String generateOrderNo() {
        // 生成唯一订单号的逻辑
        return "ORD" + System.currentTimeMillis() + RandomUtil.randomString(6);
    }
}

优劣势分析

优点

  • 实现简单,依赖数据库自身的约束
  • 可靠性高,数据库保证唯一性

缺点

  • 仅适用于插入操作
  • 并发性能受数据库限制
  • 错误处理需要捕获异常

适用场景

  • 订单创建
  • 用户注册
  • 资源分配

方案二:基于Token令牌

实现原理

这个方案比较经典,流程非常清晰:

  1. 客户端先请求获取一个令牌
  2. 服务端生成唯一令牌并存储(通常放在Redis里)
  3. 客户端携带令牌发起实际请求
  4. 服务端验证令牌是否存在,存在则执行操作,然后删除令牌
  5. 重复请求因为令牌已被删除,直接拒绝

核心源码

Token生成与验证

@Component
public class IdempotentTokenService {
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * 生成幂等性令牌
     */
    public String generateToken() {
        String token = UUID.randomUUID().toString();
        // 存储令牌,设置过期时间为5分钟
        redisTemplate.opsForValue().set("idempotent_token:" + token, token, 5, TimeUnit.MINUTES);
        return token;
    }
    
    /**
     * 验证令牌
     */
    public boolean validateToken(String token) {
        if (StringUtils.isEmpty(token)) {
            return false;
        }
        String key = "idempotent_token:" + token;
        // 原子操作,获取并删除令牌
        return redisTemplate.delete(key);
    }
}

Controller层实现

@RestController
@RequestMapping("/api/order")
public class OrderController {
    @Autowired
    private IdempotentTokenService idempotentTokenService;
    @Autowired
    private OrderService orderService;
    
    /**
     * 获取幂等性令牌
     */
    @GetMapping("/token")
    public ResponseEntity getToken() {
        String token = idempotentTokenService.generateToken();
        return ResponseEntity.ok(token);
    }
    
    /**
     * 创建订单
     */
    @PostMapping
    public ResponseEntity createOrder(@RequestHeader("X-Idempotent-Token") String token, 
                                           @RequestBody OrderCreateRequest request) {
        // 验证令牌
        if (!idempotentTokenService.validateToken(token)) {
            return ResponseEntity.badRequest().build();
        }
        
        Order order = orderService.createOrder(request);
        return ResponseEntity.ok(order);
    }
}

优劣势分析

优点

  • 适用范围广,可用于各种操作
  • 实现灵活,可自定义过期时间
  • 性能较好,基于Redis操作

缺点

  • 需要额外的Redis存储
  • 增加了请求次数(获取令牌)
  • 客户端需要额外处理令牌

适用场景

  • 支付接口
  • 表单提交
  • 数据更新

方案三:基于分布式锁

实现原理

分布式锁的思路是:同一时间只允许一个请求执行关键操作,其他请求要么等待,要么直接返回。通过锁的互斥性,天然避免了重复处理。

核心源码

分布式锁实现

@Component
public class DistributedLockService {
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * 获取分布式锁
     */
    public boolean tryLock(String key, long expireTime) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "locked", expireTime, TimeUnit.MILLISECONDS);
        return Boolean.TRUE.equals(result);
    }
    
    /**
     * 释放分布式锁
     */
    public void releaseLock(String key) {
        redisTemplate.delete(key);
    }
}

Service层实现

@Service
public class PaymentService {
    @Autowired
    private DistributedLockService distributedLockService;
    @Autowired
    private PaymentMapper paymentMapper;
    
    public Payment processPayment(PaymentRequest request) {
        // 生成锁键
        String lockKey = "payment_lock:" + request.getOrderNo();
        
        try {
            // 尝试获取锁,设置过期时间为30秒
            if (!distributedLockService.tryLock(lockKey, 30000)) {
                throw new BusinessException("支付处理中,请稍后再试");
            }
            
            // 检查是否已支付
            Payment existingPayment = paymentMapper.selectByOrderNo(request.getOrderNo());
            if (existingPayment != null && existingPayment.getStatus() == 1) {
                return existingPayment;
            }
            
            // 处理支付逻辑
            Payment payment = new Payment();
            payment.setOrderNo(request.getOrderNo());
            payment.setAmount(request.getAmount());
            payment.setStatus(1);
            payment.setCreateTime(new Date());
            
            paymentMapper.insert(payment);
            return payment;
        } finally {
            // 释放锁
            distributedLockService.releaseLock(lockKey);
        }
    }
}

优劣势分析

优点

  • 适用范围广,可用于各种操作
  • 支持并发场景
  • 实现灵活,可自定义锁的粒度

缺点

  • 依赖分布式锁实现
  • 可能存在锁竞争问题
  • 实现复杂度较高

适用场景

  • 支付处理
  • 库存扣减
  • 数据同步

方案四:基于Redis实现

实现原理

利用Redis的原子操作(比如SETNX)来做一个“去重标记”。如果标记已经存在,说明请求已经被处理过了,直接拒绝。这个方案跟Token方案很像,但更轻量,不需要提前获取令牌。

核心源码

Redis幂等性服务

@Component
public class RedisIdempotentService {
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * 检查并设置幂等性键
     */
    public boolean checkAndSet(String key, long expireTime) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "processed", expireTime, TimeUnit.MILLISECONDS);
        return Boolean.TRUE.equals(result);
    }
    
    /**
     * 生成幂等性键
     */
    public String generateKey(String prefix, String... params) {
        StringBuilder sb = new StringBuilder(prefix);
        for (String param : params) {
            sb.append(":").append(param);
        }
        return sb.toString();
    }
}

Service层实现

@Service
public class OrderService {
    @Autowired
    private RedisIdempotentService redisIdempotentService;
    @Autowired
    private OrderMapper orderMapper;
    
    public Order createOrder(OrderCreateRequest request) {
        // 生成幂等性键
        String key = redisIdempotentService.generateKey("order_create", 
                                                     request.getUserId(), 
                                                     request.getProductId());
        
        // 检查是否已处理
        if (!redisIdempotentService.checkAndSet(key, 30000)) {
            throw new BusinessException("订单已处理,请不要重复提交");
        }
        
        // 创建订单逻辑
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setAmount(request.getAmount());
        order.setStatus(0);
        order.setCreateTime(new Date());
        
        orderMapper.insert(order);
        return order;
    }
}

优劣势分析

优点

  • 实现简单,基于Redis原子操作
  • 性能优异,Redis操作速度快
  • 适用范围广

缺点

  • 依赖Redis服务
  • 需要合理设置过期时间
  • 键的设计需要考虑唯一性

适用场景

  • 高频接口
  • 表单提交
  • 数据更新

方案五:基于乐观锁

实现原理

乐观锁通常依赖版本号(version字段)或时间戳。每次更新时,检查版本号是否与之前读取的一致,一致则更新并将版本号加1,不一致则说明数据已被其他请求修改,更新失败。这样避免了锁竞争,但需要业务方处理重试。

核心源码

数据库表设计

CREATE TABLE `product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL COMMENT '产品名称',
  `stock` int(11) NOT NULL COMMENT '库存',
  `version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品表';

Service层实现

@Service
public class ProductService {
    @Autowired
    private ProductMapper productMapper;
    
    @Transactional
    public boolean deductStock(Long productId, int quantity) {
        // 获取产品信息
        Product product = productMapper.selectById(productId);
        if (product == null) {
            throw new BusinessException("产品不存在");
        }
        
        // 检查库存
        if (product.getStock() < quantity) {
            throw new BusinessException("库存不足");
        }
        
        // 更新库存,使用乐观锁
        int result = productMapper.updateStock(productId, quantity, product.getVersion());
        if (result == 0) {
            // 更新失败,说明版本号已变化
            throw new BusinessException("库存更新失败,请重试");
        }
        
        return true;
    }
}

Mapper层实现

public interface ProductMapper {
    @Update("UPDATE product SET stock = stock - #{quantity}, version = version + 1, update_time = NOW() WHERE id = #{productId} AND version = #{version}")
    int updateStock(@Param("productId") Long productId, @Param("quantity") int quantity, @Param("version") int version);
}

优劣势分析

优点

  • 实现简单,基于数据库版本号
  • 无锁设计,并发性能好
  • 适用于更新操作

缺点

  • 仅适用于更新操作
  • 可能需要重试机制
  • 实现复杂度较高

适用场景

  • 库存扣减
  • 数据更新
  • 状态变更

3. 方案比较

方案实现复杂度性能可靠性适用场景依赖
基于数据库唯一索引插入操作数据库
基于Token令牌各种操作Redis
基于分布式锁并发操作Redis/Zookeeper
基于Redis实现各种操作Redis
基于乐观锁更新操作数据库

4. 最佳实践

4.1 选择合适的方案

场景不同,倾向的方案也不同,这里给出几个常用的选型建议:

  • 插入操作:优先使用基于数据库唯一索引的方案
  • 更新操作:优先使用基于乐观锁的方案
  • 高频接口:优先使用基于Redis的方案
  • 并发操作:优先使用基于分布式锁的方案
  • 通用场景:优先使用基于Token令牌的方案

4.2 性能优化

  1. 合理设置过期时间:根据业务场景设置适当的过期时间,太短可能导致重复请求漏过,太长浪费存储
  2. 批量处理:对于批量操作,考虑批量验证幂等性,减少多次IO
  3. 异步处理:对于耗时操作,考虑异步处理幂等性验证,避免阻塞主流程
  4. 缓存优化:使用Redis集群提高性能和可靠性,避免单点故障

4.3 安全性考虑

  1. 防止Token泄露:使用HTTPS传输Token,避免中间人截获
  2. 防止暴力破解:限制Token验证失败次数,防止恶意枚举
  3. 防止重放攻击:使用时间戳或随机数,让每次请求具有唯一性
  4. 数据加密:对敏感数据进行加密存储,避免数据泄露风险

4.4 代码示例:综合方案

在实际项目中,我们往往不会只用一种方案,而是组合使用。比如下面这个支付接口,就同时使用了Token令牌+分布式锁,形成一个双重保险:

@RestController
@RequestMapping("/api/payment")
public class PaymentController {
    @Autowired
    private IdempotentTokenService idempotentTokenService;
    @Autowired
    private PaymentService paymentService;
    
    @GetMapping("/token")
    public ResponseEntity getToken() {
        String token = idempotentTokenService.generateToken();
        return ResponseEntity.ok(token);
    }
    
    @PostMapping
    public ResponseEntity processPayment(@RequestHeader("X-Idempotent-Token") String token, 
                                               @RequestBody PaymentRequest request) {
        // 1. 验证Token
        if (!idempotentTokenService.validateToken(token)) {
            return ResponseEntity.badRequest().body(null);
        }
        
        // 2. 处理支付
        Payment payment = paymentService.processPayment(request);
        return ResponseEntity.ok(payment);
    }
}

@Service
public class PaymentService {
    @Autowired
    private DistributedLockService distributedLockService;
    @Autowired
    private PaymentMapper paymentMapper;
    
    public Payment processPayment(PaymentRequest request) {
        String lockKey = "payment_lock:" + request.getOrderNo();
        
        try {
            // 3. 获取分布式锁
            if (!distributedLockService.tryLock(lockKey, 30000)) {
                throw new BusinessException("支付处理中,请稍后再试");
            }
            
            // 4. 检查是否已支付
            Payment existingPayment = paymentMapper.selectByOrderNo(request.getOrderNo());
            if (existingPayment != null && existingPayment.getStatus() == 1) {
                return existingPayment;
            }
            
            // 5. 处理支付逻辑
            Payment payment = new Payment();
            payment.setOrderNo(request.getOrderNo());
            payment.setAmount(request.getAmount());
            payment.setStatus(1);
            payment.setCreateTime(new Date());
            
            paymentMapper.insert(payment);
            return payment;
        } finally {
            // 6. 释放锁
            distributedLockService.releaseLock(lockKey);
        }
    }
}

5. 总结

幂等性在API设计中不是锦上添花,而是必备要素。它直接关系到系统的可靠性和数据一致性,尤其在分布式、高并发的场景下,一旦缺失,后果往往是数据混乱、用户投诉、甚至资金损失。

在SpringBoot应用中,我们可以通过多种方式实现接口幂等性——从简单的数据库唯一索引,到灵活的Token令牌,再到高并发的Redis方案和分布式锁,以及无锁的乐观锁。每种方案都有各自的优缺点和适用场景,没有银弹。

  1. 基于数据库唯一索引:简单可靠,适用于插入操作
  2. 基于Token令牌:灵活通用,适用于各种操作
  3. 基于分布式锁:支持并发,适用于复杂场景
  4. 基于Redis实现:性能优异,适用于高频接口
  5. 基于乐观锁:无锁设计,适用于更新操作

在实际应用中,我们应该根据具体的业务场景选择合适的实现方案,也可以结合多种方案来提高系统的可靠性和性能。比如在上面的综合示例中,Token负责防重放,分布式锁负责防并发,两者配合,几乎能把所有重复请求都挡在门外。

通过合理的幂等性设计,我们可以有效防止重复请求导致的问题,让系统更稳定,用户体验也更丝滑。

来源:https://www.jb51.net/program/367781p6q.htm

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
FileZilla断点续传设置与操作指南

FileZilla断点续传设置与操作指南

FileZilla支持断点续传,需客户端与服务器均开启REST命令。设置中确保启用断点续传及继续传输选项。中断后自动或手动从断点恢复。注意服务器支持、传输模式匹配及文件完整性校验。

时间:2026-07-25 22:29
Debian系统C++编译器位置查找方法

Debian系统C++编译器位置查找方法

在Debian系统中,通过apt安装的C++编译器g++默认位于 usr bin g++,可使用which或whereis命令验证路径。g++属于build-essential软件包,若未安装则需执行sudoaptinstallbuild-essential。该包还包含gcc、make等编译工具链,g++是GNUC++编译器,实际是符号链接指向具体版本,验证

时间:2026-07-25 22:29
Debian系统安装C++环境的方法

Debian系统安装C++环境的方法

在Debian系统安装C++开发环境:先sudoaptupdate更新包列表,再sudoaptinstallbuild-essential安装编译工具链,或单独安装g++。用g++--version验证。可选安装VSCode、GDB、CMake等工具并配置默认编译器版本。

时间:2026-07-25 22:29
Debian系统C++开发环境配置指南

Debian系统C++开发环境配置指南

在Debian系统中,先执行aptupdate更新软件包列表,再安装build-essential元包即可获得GCC、G++、Make和GDB。通过运行g++--version命令验证编译器安装成功。可选安装VisualStudioCode、CLion等编辑器及CMake构建工具,并编写一个简单的HelloWorld程序,使用g++编译运行以验证环境配置正确

时间:2026-07-25 22:29
通过cpustat工具查看CPU状态的具体方法与详细步骤

通过cpustat工具查看CPU状态的具体方法与详细步骤

cpustat是sysstat包中的CPU监控工具,可按固定间隔输出带时间戳的CPU使用率统计。安装后运行cpustat即可实时显示各核心信息,常用指标包括%usr、%sys、%iowait、%steal和%idle,用于定位用户态、内核态或I O瓶颈。高级选项-c可显示单核统计,-m可同时查看内存使用,适合脚本采集和性能分析。

时间:2026-07-25 22:18
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 热门数据榜