当前位置: 首页 > java >正文

接口保证幂等性你学废了吗?

接口幂等性定义:无论一次或多次调用某个接口,对资源产生的副作用都是一致的。

简单来说:用户由于各种原因(网络超时、前端重复点击、消息重试等)对同一个接口发了多次请求,系统只能处理一次,不能因为多次请求导致数据错误(如扣款两次、生成两个订单)。

方案一:Token 机制(适用于新增、提交类操作)

这是最经典的防重提交方案,尤其适合前端表单提交、支付下单等场景。

核心思想:客户端先获取一个服务器颁发的唯一令牌(Token),提交请求时必须带上这个Token。服务器处理请求后,使该Token失效。

流程:

  1. 获取Token:客户端在发起业务请求前,先调用一个接口获取一个全局唯一的Token(通常存于Redis,并设置较短的有效期)。
  2. 提交请求:客户端带着业务参数和这个Token发起业务请求。
  3. 校验Token:
    服务器端(通常通过AOP或Filter实现)检查Redis中是否存在该Token。
    存在:执行业务逻辑,然后删除Redis中的Token。
    不存在:说明该请求已被处理过,直接返回重复提交的错误信息。

代码示例(AOP实现):

// 1. 自定义一个幂等性注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {int expireTime() default 60; // Token有效期,秒
}// 2. 编写Token创建接口
@RestController
public class TokenController {@Autowiredprivate StringRedisTemplate redisTemplate;@GetMapping("/token")public String getToken() {String token = UUID.randomUUID().toString();redisTemplate.opsForValue().set(token, "1", Duration.ofSeconds(60)); // 存入Redis,60秒过期return token;}
}// 3. 编写AOP切面处理幂等性校验
@Aspect
@Component
public class IdempotentAspect {@Autowiredprivate StringRedisTemplate redisTemplate;@Around("@annotation(idempotent)")public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String token = request.getHeader("X-Idempotent-Token"); // 从Header中获取Tokenif (StringUtils.isEmpty(token)) {throw new RuntimeException("Token不存在");}// 核心逻辑:删除Token(原子操作)。如果删除成功,返回1,说明是第一次请求。Boolean isDeleted = redisTemplate.delete(token);if (Boolean.TRUE.equals(isDeleted)) {// 是第一次请求,放行return joinPoint.proceed();} else {// 删除失败,可能是Token已过期或被使用// 尝试获取Token,如果还能获取到值,说明是重复请求(但还没过期)。如果获取不到,说明已过期。String tokenValue = redisTemplate.opsForValue().get(token);if (tokenValue != null) {throw new IdempotentException("请勿重复操作");} else {throw new IdempotentException("操作已过期,请刷新页面后重试");}}}
}// 4. 在需要幂等性的接口上使用注解
@RestController
public class OrderController {@PostMapping("/createOrder")@Idempotent // 加上注解public String createOrder(@RequestBody Order order) {// 业务逻辑...return "订单创建成功";}
}

优点:实现简单,通用性强。
缺点:需要额外一次获取Token的请求。

方案二:基于数据库唯一索引(适用于插入操作)

核心思想:利用数据库唯一索引的排他性,防止重复数据插入。

流程:

  1. 在数据表中为一个或多个字段建立唯一索引。这个“唯一键”可以是:
    业务主键(如订单ID)
    组合字段(如:用户ID + 业务类型 + 关联ID)
  2. 插入数据时,如果唯一键重复,数据库会抛出 DuplicateKeyException。
  3. 代码中捕获这个异常,返回“请勿重复操作”的提示。

示例:防止用户重复创建订单。

  • 在 order 表为 user_id 和 order_source_id(本次请求的唯一源ID)建立联合唯一索引。
  • 每次创建订单时,如果同一个用户使用同一个源ID请求,第二次插入就会失败。

优点:实现最简单,无需额外编码,依靠数据库本身能力。
缺点:只适用于新增场景,不适用于更新、删除操作。

注意:
这个唯一id并不是数据库表的自增id,而是在发起请求前,生成一个全局唯一的业务ID,例如:order_id = 雪花算法生成的长整形ID。
将这个 order_id 随请求一起传到后端。
在数据库的 order 表上,为 order_id 字段建立一个唯一索引。
插入时,如果两个请求带着同一个 order_id 到来,第二个请求就会因为唯一索引冲突而插入失败。

方案三:悲观锁/乐观锁(适用于更新、扣减操作)

  1. 悲观锁(Pessimistic Lock)
    思想:“先取锁,再操作”。认为并发冲突一定会发生,因此在操作数据时直接将其锁住。
    实现:使用 SELECT … FOR UPDATE。
    场景:适用于写操作非常频繁,冲突概率极高的场景。要谨慎使用,容易导致性能瓶颈和死锁。
  2. 乐观锁(Optimistic Lock) - 更推荐
    思想:“先操作,再验证”。认为冲突不常发生,通过版本号(Version)或状态机来保证。
    实现:
    在表中增加一个 version 字段。
    更新数据时,将 version 作为条件:UPDATE table SET value = new_value, version = version + 1 WHERE id = #{id} AND version = #{old_version}。
  3. 检查 executeUpdate() 返回的影响行数:
    如果为 1:更新成功,是第一次请求。
    如果为 0:说明version已被其他请求修改过,是重复请求或冲突请求,操作失败。

示例(余额扣款):

UPDATE account SET balance = balance - 100,version = version + 1
WHERE user_id = 123 
AND version = 5; -- 旧的版本号
--通过在一次事务内,先执行 SELECT 语句查询出来最新的版本号再进行更新操作。

优点(乐观锁):性能高,避免数据库锁竞争。
缺点:需要修改表结构,失败后需要重试或告知客户端。

方案四:状态机约束(适用于有状态流转的业务)

核心思想: 很多业务数据都有明确的状态流转(如:订单状态:0待支付->1已支付->2已发货)。只有在特定状态下,操作才是允许的。
流程:
执行更新操作时,不仅以ID为条件,还必须加上当前状态作为条件。
如果状态不符合预期,则更新失败,说明请求无效或已处理过。

示例(支付回调接口):

-- 只有状态为“待支付”的订单才能被更新为“已支付”
UPDATE orders SET status = '已支付' WHERE order_id = '123' AND status = '待支付';

优点:业务逻辑本身自带的幂等性,无需额外组件。
缺点:仅适用于有状态变化的业务。

总结与选择

方案适用场景优点缺点
Token 机制通用,尤其前端提交、创建操作通用性强,可靠性高需额外接口,多一次交互
唯一索引数据插入类操作实现最简单,绝对可靠仅限插入操作
乐观锁数据更新、扣减库存类操作性能好,避免锁竞争需修改表结构,增加version字段
状态机有明确状态流转的业务(订单、流程)天然幂等,符合业务逻辑局限性较强
悲观锁极高并发写场景(少用)保证强一致性性能差,易死锁

在金融项目中如何选择?
薪酬计算触发接口:可用 Token机制 或基于业务ID的唯一索引(如 calculation_id),防止重复触发计算。
奖金发放、余额扣减接口:乐观锁是最佳选择,保证金额不会重复扣减。
订单、流程状态变更:状态机约束是必须的,例如“只有未支付的订单才能支付”。
最佳实践: 通常会将多种方案组合使用。例如,先用 Token 机制 防重,在业务逻辑内再用 乐观锁 或 状态机 做最终保障,形成双保险。

http://www.xdnf.cn/news/19857.html

相关文章:

  • Kafka Topic(主题)详解
  • 【CMake】message函数
  • Flutter + Web:深度解析双向通信的混合应用开发实践
  • 深入理解 jemalloc:从内存分配机制到技术选型
  • Docker--架构篇
  • C++CSP-J/S必背模板
  • 机器学习从入门到精通 - Transformer颠覆者:BERT与预训练模型实战解析
  • PLSQL导入excel数据的三种方法
  • PL-YOLOv8:基于YOLOv8的无人机实时电力线检测与植被风险预警框架,实现精准巡检与预警
  • 区块链版权存证的法律效力与司法实践
  • 52Hz——STM32单片机学习记录——FSMC
  • maven scope=provided || optional=true会打包到jar文件中吗?
  • 车辆安全供电系统开发原则和实践
  • VR节约用水模拟体验系统:沉浸式体验如何改变我们的用水习惯
  • Debezium报错处理系列之第130篇:OutOfMemoryError: Java heap space
  • Spring boot3.x整合mybatis-plus踩坑记录
  • Cesium 实战 - 自定义纹理材质 - 箭头流动线(图片纹理)
  • 企业资源计划(ERP)在制造业的定制化架构
  • 【QT随笔】巧用事件过滤器(installEventFilter 和 eventFilter 的组合)之 QComboBox 应用
  • 手把手教你开发第一个 Chrome 扩展程序:网页字数统计插件
  • 从竞态到原子:pread/pwrite 如何重塑高效文件 I/O?
  • 如何使文件夹内的软件或者文件不受windows 安全中心的监视
  • Java8特性
  • 【HarmonyOS 6】仿AI唤起屏幕边缘流光特效
  • leetcode-每日一题-人员站位的方案数-C语言
  • Spring 循环依赖问题
  • 《LINUX系统编程》笔记p8
  • 大模型RAG项目实战:RAG技术原理及核心架构
  • SpringBoot 事务管理避坑指南
  • 机器学习:从技术原理到实践应用的深度解析