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

接口幂等性问题和常见解决方案

接口幂等性问题和常见解决方案

  • 1.什么是接口幂等性问题
    • 1.1 会产生接口幂等性的问题
    • 1.2 解决思路
  • 2.接口幂等性的解决方案
    • 2.1 唯一索引解决方案
    • 2.2 乐观锁解决方案
    • 2.3 分布式锁解决方案
    • 2.4 Token解决方案(最优方案)
  • 3 Token解决方案落地
    • 3.1 token获取、token校验
    • 3.2 自定义注解, 标识哪些接口需要幂等性校验
    • 3.3 目标接口上添加注解
    • 3.4 拦截器

1.什么是接口幂等性问题

幂等性: 用户同一操作发起的一次多次请求的结果是一致的

在增删改查4个操作中, 查询不会修改数据, 删除进行一次或者多次的产生的结果一致, 所以只需要关注修改新增操作, 修改和新增在重复提交的场景下会产生接口幂等性问题

1.1 会产生接口幂等性的问题

  • 定时任务重复执行
  • 使用了失效或超时的重试机制, 发起的重试
  • 第三方平台的接口, 因为异常导致多次异步回调
  • 中间件、应用服务根据自身特性, 也有可能进行重试
  • 使用浏览器后退按钮重复之前的操作, 导致重复提交表单
  • 网络波动等异常, 未收到反馈后发起重复请求, 页面重复刷新
  • 用户在使用的时候无意多次点击(重复操作),或者没有响应而导致多次下单或者交易。

1.2 解决思路

解决思路分为两个方向:

  • 客户端防止重复调用
  • 服务端防止重复调用

2.接口幂等性的解决方案

2.1 唯一索引解决方案

根据业务需求, 对数据表中字段设置唯一索引, 可以是单一索引, 也可以是联合索引, 防止新增时出现脏数据

例如: 新增用户数据, 具体流程:

  1. 给表中的手机号设置唯一索引
  2. 第一次请求, 插入成功
  3. 后续请求, 抛出唯一索引冲突异常(DuplicateKeyException), 插入失败

优缺点: 操作简单, 只要对字段建立唯一索引即可, 但是只适用于新增操作, 而且效率不高, 基于数据库机制去防止重复新增, 相当于把压力都给到了数据库, 在高并发情况下会出现性能问题

2.2 乐观锁解决方案

根据业务需求, 给数据表添加一个版本字段(version), 执行更新操作时, 比较版本号. 如果版本号相同, 则可以更新成功, 并在更新时增加版本号, 如果版本号不同, 则更新失败

例如: 更新账户余额, 具体流程:

  1. 给表中添加版本号字段(version), 默认为0
  2. 第一次请求, 开启事务, 将id为1的用户的账户余额+10
start transaction;
update account set money = money + 10, version = version + 1 where id = 1 and version = 1;
  1. 第二次请求, 开启事务, 将id为1的用户的账户余额更新-20
start transaction;
update account set money = money - 20, version = version + 1 where id = 1 and version = 1;
  1. 第一次请求, 提交事务, 更新成功
  2. 第二次请求, 提交事务, 更新失败, 因为version = 1这个条件已经不符合了
    在这里插入图片描述

缺点:

  • 只适用于更新操作
  • 无法完全保证幂等性, 例如第一个请求已经完成并提交事务, 那么第二个请求即使是相同的请求, 仍然会修改数据

2.3 分布式锁解决方案

这里演示使用Redis + 自定义注解 + AOP解决

  1. 浏览器请求接口时, 携带一个唯一标识(前端生成, 可以是UUID或者类似的唯一标识符), 短时间内重复点击, 唯一标识相同
  2. 将唯一标识缓存到Redis中, 并设置超时时间, 例如500毫秒
  3. 第一次请求, 设置成功(setNx方法), 继续操作数据
  4. 第二次请求, 设置失败, 代表已经有线程在执行同一个请求了, 直接返回, 不进行重复操作

代码实现:

  • 自定义注解(实现更灵活的接口幂等性校验)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {/*** 过期时长(毫秒)*/long expire();
}
  • 针对添加了Idempotent注解的接口, 进行AOP
@Aspect
@Component
@Slf4j
public class IdempotentAspect{@Resourceprivate RedisTemplate<String,String> redisTemplate;@Pointcut("@annotation(com.itheima.annotation.Idempotent)")public void execute(){}@Around("execute()")public Object around(ProceedingJoinPoint joinPoint) {HttpServletRequest request =((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();// 获取本次请求唯一标识String token = request.getHeader("token");// 获取注解对象Idempotent annotation = method.getAnnotation(Idempotent.class);// 缓存设置(setNx方法), key为唯一标识, value为随机值, 过期时间为注解的设置, 单位是毫秒Boolean b = redisTemplate.opsForValue().setIfAbsent(redisKey, "1", nnotation.expireMillis(), TimeUnit.MILLISECONDS);if (b != null && b) {// 放行, 执行业务方法Object obj = joinPoint.proceed();// 删除缓存redisTemplate.opsForValue().delete(redisKey);return obj;}else {// 友好提示 throw new RuntimeException("您操作的太快,请稍后再试");;}}
}

缺点:

  • 浏览器快速点击, 产生了两次请求, 第一次请求先到服务器, 因为某些原因, 第二次请求达到服务器时, 第一次请求已经执行完毕并释放了锁, 此时第二次请求仍然可以加锁成功, 并执行业务逻辑, 这种情况下幂等性失效

客户端连续发起多次请求,这多次请求同时到达服务端,此时开始争抢锁,谁抢到锁谁就执行,其他没有抢到锁的请求都统统不执行。这种情况能保证幂等性。

2.4 Token解决方案(最优方案)

解决幂等性的思路: 为每一次操作(即使发生多次请求)提供一个唯一Token, 我们确保Token的一次性唯一性, 唯一性很好理解, 每次产生的Token都是UUID(类似技术), 一次性可以想象为一个待消费的二维码, 扫描一次后即失效(一次性凭证)

  1. 服务端要记录这个一次性凭证, 所以Token需要在服务端生成, 在服务端提供一个返回Token的接口, 每次请求都会将Token写入Redis缓存(记录凭证, 后期验证), 并响应给浏览器(凭证发放), 这个Token相当于一次性凭证, 例如消费券的二维码

  2. 后端提供一个返回Token的接口, 后端会将Token写入缓存, 并响应给前端(这个token等于是一个一次性的钥匙, 例如二维码)

  3. 浏览器携带Token发起请求

  4. 服务端在拦截器(或者AOP)中校验Token的有效性, 实质就是判断Redis中是否存在这个Token

  5. 有这个token, 就开门放行, 并删除Redis中token(一次性凭证已使用), 然后执行相应的业务逻辑

  6. 如果没有这个token, 可能是因为token已经过期了(有过期时间)、伪造的token、token已经销毁了(delete)这些情况都属于访问失败, 服务器会拒绝请求

PS: 一般来说,服务端会在验证通过后立即删除Token,以确保后续的请求都被认为是无效的(更接近原子性)

简单来说: 为每一次操作生成一个待使用的一次性凭证, 第一次请求使用凭证, 开门放行, 后续请求再携带凭证, 但凭证已经失效了, 无法放行

3 Token解决方案落地

3.1 token获取、token校验

@RestController
@RequestMapping("token")
public class TokenController {@Autowiredprivate TokenService TokenService;@GetMapping("get")public String getToken() {return TokenService.getToken();}}
  • getToken: 执行获取Token的业务
  • check: 对请求中携带的Token进行校验
// 业务接口
public interface TokenService {String getToken();void check(HttpServletRequest request);
}
  • 获取Token
    • 使用签证标识+UUID作为Token, 并设置过期时间5分钟(5分钟没有请求操作, 则Token过期)
    • 将Token存入Redis, 这个操作是记录凭证, 用于后期验证
  • 校验Token
    • 浏览器在请求头中携带Token, 服务端获取到Token后, 判断Redis中是否存在Redis
    • 如果有, 则开门放行, 并删除Token(凭证已使用)
    • 如果没有, 则拒绝请求
// 业务实现
@Service
public class TokenServiceImpl implements TokenService {@Autowiredprivate StringRedisTemplate redisTemplate;@Overridepublic String getToken() {// 使用UUID作为TokenString token = UUID.randomUUID().toString().replace("-", "");// 给Token加个前缀, 意思是进行幂等性校验的Tokentoken = "API_IDEMPOTENT_TOKEN:" + token;// 将Token缓存到redis中, key是token, value是随机值, 过期时间为300(5分钟), 过期单位为秒// 如果5分钟之内, 客户端不携带token进行一次请求, 则token过期, 访问目标接口需要重新获取TokenredisTemplate.opsForValue().set(token, "0", 5 * 60, TimeUnit.SECONDS);return token;}@Overridepublic void check(HttpServletRequest request) {// 从请求头里拿到ToeknString token = request.getHeader("idempotentToken");if (StringUtil.isBlank(token)) {// 请求头中不存在, 就从请求参数中拿token = request.getParameter("idempotentToken");if (StringUtil.isBlank(token)) {throw new RuntimeException("参数不合法");}}// 判断redis中是否存在tokenif (!redisTemplate.hasKey(token)) {// 不存在, 其实分为好几种情况. 1-过期了、2-伪造、3-已经被消费了, 我们同一回复throw new RuntimeException("请勿重复操作");}// 校验通过, redis中存在token, 一次性消费成功, 任务放行. 凭证过期redisTemplate.delete(token);}}

3.2 自定义注解, 标识哪些接口需要幂等性校验

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {}

3.3 目标接口上添加注解

@RestController
@RequestMapping("api")
public class ApiController {@ApiIdempotent@GetMapping("test")public void test() {System.out.println("执行业务, 模拟一个比较耗时的操作");}
}

3.4 拦截器

拦截器中怎么使用@Autowired!!!
SpringBoot常使用的拦截器案例, 是无法在拦截器中直接使用@Autowired的

在这里插入图片描述

原因: 拦截器并非是Spring容器中的Bean, Spring无法对其进行自动装配,
问题扩展: 如果给拦截器上添加@Component注解, 依旧无法使用@Autowired, 因为被@Component注解的类确实被Spring容器管理了, 但你注册到SpringMVC容器中的是new ApiIdempotentInterceptor(), 它们都不是同一个

解决方案:

  1. 在拦截器上添加@Component注解, 让IOC容器管理这个拦截器
  2. 在MVC的配置中通过IOC容器获取拦截器(使用@Autowired注入), 然后再注入到MVC容器中
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate ApiIdempotentInterceptor apiIdempotentInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(apiIdempotentInterceptor).addPathPatterns("/**"); // 可以指定拦截的路径,/** 表示拦截所有路径}}

之后就可以在拦截器中, 直接使用@Autowired

@Component
public class ApiIdempotentInterceptor implements HandlerInterceptor {// 注入Spring容器@Autowiredprivate TokenService tokenService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {try {// 转换为可处理的method对象HandlerMethod handlerMethod = (HandlerMethod) handler;// 校验方法上是否添加了幂等性校验注解Method method = handlerMethod.getMethod();ApiIdempotent annotation = method.getAnnotation(ApiIdempotent.class);if (annotation != null) {// 方法上添加了自定义注解tokenService.check(request);}} catch (Exception e) {// 为统一异常处理对象设置code和msg, 并将其转换为JSON字符串ErrorResponse errorResponse = new ErrorResponse(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());String json = JSON.toJSONString(errorResponse);// response返回统一异常处理对象response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);// 设置响应类型为JSONresponse.setContentType("application/json");// 设置响应编码为UTF-8response.setCharacterEncoding("UTF-8");// 将JSON字符串写入响应输出流response.getWriter().write(json);// 不放行return false;}// 在请求处理前执行的逻辑return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {// 在请求处理后,视图渲染前执行的逻辑}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 在请求处理完成后执行的逻辑,可以用于资源清理等操作}
}

相关文章:

接口幂等性问题和常见解决方案

接口幂等性问题和常见解决方案 1.什么是接口幂等性问题1.1 会产生接口幂等性的问题1.2 解决思路 2.接口幂等性的解决方案2.1 唯一索引解决方案2.2 乐观锁解决方案2.3 分布式锁解决方案2.4 Token解决方案(最优方案) 3 Token解决方案落地3.1 token获取、token校验3.2 自定义注解,…...

网站首页添加JS弹屏公告窗口教程

很多小白站长会遇到想给自己的网站添加一个弹屏公告&#xff0c;用于做活动说明、演示站提示等作用与目的。 下面直接上代码&#xff1a;&#xff08;直接复制到网页头部、底部php、HTML文件中&#xff09; <script src"https://www.mohuda.com/site/js/sweetalert.m…...

【Rockchip 安10.1 默认给第三方apk默认开启所有权限】

Rockchip 安10.1 默认给第三方apk默认开启所有权限 问题描述解决方法 郑重声明:本人原创博文&#xff0c;都是实战&#xff0c;均经过实际项目验证出货的 转载请标明出处:攻城狮2015 Platform: Rockchip 3229 OS:Android 10.1 Kernel: 4.19 问题描述 有些第三方或者主界面&…...

python-redis缓存装饰器

目录 redis_decorator安装查看源代码使用 redis_decorators安装查看源代码\_\_init\_\_.pycacheable.py 各种可缓存的类型cache_element.py 缓存的元素caching.py 缓存主要逻辑 使用 总结全部代码参考 redis_decorator 安装 pip install redis_decorator查看源代码 from io …...

每个私域运营者都必须掌握的 5 大关键流量运营核心打法!

很多人觉得私域运营比较简单&#xff0c;只是运营的事情&#xff0c;但事实并非如此&#xff0c;私域运营体系非常大&#xff0c;包含了公私域联动、品牌运营、品类战略&#xff0c;它是一个自上而下&#xff0c;由内到外的系统化工程。 很多人天天在想着如何引流拓客&#xf…...

蓝桥杯--平均

在编程竞赛&#xff0c;尤其是参与蓝桥杯的过程中&#xff0c;遇到各种问题需求是家常便饭。最近&#xff0c;我遇到了一个非常有趣且颇具挑战性的算法问题。问题描述如下&#xff1a;对于一个长度为n的数组&#xff08;n是10的倍数&#xff09;&#xff0c;数组中的每个元素均…...

未来已来:科技驱动的教育变革

我们的基础教育数百年来一成不变。学生们齐聚在一个物理空间&#xff0c;听老师现场授课。每节课时长和节奏几乎一致&#xff0c;严格按照课表进行。老师就像“讲台上的圣人”。这种模式千篇一律&#xff0c;并不适用于所有人。学生遇到不懂的问题&#xff0c;只能自己摸索或者…...

【蓝桥杯每日一题】填充颜色超详细解释!!!

为了让蓝桥杯不变成蓝桥悲&#xff0c;我决定在舒适的周日再来一道题。 例&#xff1a; 输入&#xff1a; 6 0 0 0 0 0 0 0 0 1 1 1 1 0 1 1 0 0 1 1 1 0 0 0 1 1 0 0 0 0 1 1 1 1 1 1 1 输出&#xff1a; 0 0 0 0 0 0 0 0 1 1 1 1 0 1 1 2 2 1 1 1 2 2 2 1 1 2 2 2 2 1 1…...

VSCODE的常用插件

1、中文设置 &#xff08;1&#xff09;搜索 chinese Chinese (Simplified) Language Pack for Visual Studio Code C/C Extension Pack &#xff08;2&#xff09;配置 通过使用“Configure Display Language”命令显式设置 VS Code 显示语言&#xff0c;可以替代默认 UI…...

Oracle常用DBA相关语句

Oracle常用DBA相关语句 1 表空间1.1 创建表空间1.2 删除表空间1.3 收缩表空间1.4 新增表空间文件1.5 查看表空间使用情况1.6 查看表所占用的空间大小 2 表分区2.1 查询表分区的创建情况2.2 查询表时指定分区 3 用户3.1 创建用户3.2 给用户赋权限3.3 删除用户 4 导入导出4.1 导入…...

JavaScript 入门指南(一)简介及基础语法

JavaScript 简介 JavaScript&#xff0c;简称 js&#xff0c;是一种基于对象&#xff08;object-based&#xff09;和事件驱动&#xff08;Event Driven&#xff09;的简单的并具有安全性能的脚本语言。 JavaScript 特性 JavaScript 是解释性语言&#xff0c;而不是编译性语言…...

UbuntuServer22.04配置静态IP地址

查看网络配置文件 使用命令, 查看网络配置文件 ls -l /etc/netplan/输出如下(文件名可能不同&#xff0c; 以实际查询为准) -rw------- 1 root root 191 Mar 17 03:30 00-installer-config.yaml编辑文件即可修改网络配置 sudo vim /etc/netplan/00-installer-config.yaml配…...

vue3 打印局部网页、网页下载为图片、下载为pdf-自动分页,几行代码搞定

经常有一些需求&#xff0c;要将网页保存为一张图片&#xff0c;感觉异常困难&#xff0c;这里发现一个简单的办法。 这么简单&#xff0c;直接一句哇塞&#xff0c;老板&#xff1a;马上完成任务。 先安装几个依赖 npm i howuse html2canvas jspdf 下载图片代码 <button …...

力扣hot100:34. 在排序数组中查找元素的第一个和最后一个位置(二分查找的理解)

我们知道使用二分查找能找到值所在的位置。假如我们在找到值后仍然不断的更新指针会发生什么&#xff1f;我们可以利用这一点来找到最左边的以及最右边的值。 如果当nums[mid]target时&#xff0c;使得 rightmid-1&#xff0c;那么最终会使得target在right的右边。 如果当nums[…...

几何相互作用GNN预测3D-PLA

预测PLA是药物发现中的核心问题。最近的进展显示了将ML应用于PLA预测的巨大潜力。然而,它们大多忽略了复合物的3D结构和蛋白质与配体之间的物理相互作用,而这对于理解结合机制至关重要。作者提出了一种结合3D结构和物理相互作用的几何相互作用图神经网络GIGN,用于预测蛋白质…...

2024最新版使用PyCharm搭建Anaconda

2024最新版使用PyCharm搭建Anaconda 因为pycharm自带的包不全&#xff0c;或者下载的时候比较慢&#xff0c;所以我们直接用anaconda的包&#xff0c;毕竟我们以后还会学到很多的包&#xff0c;不多说&#xff0c;直接开干&#xff01; 一、下载Pycharm、Anacoda pycharm中文网…...

前台于后台项目

一&#xff1a;技术栈 前台&#xff1a;vue3element plus 后台&#xff1a;reactant desgin 二&#xff1a;项目中的问题&#xff1a; 多人协同开发导致样式冲突 ui框架中组件的使用 ui框架中组件样式的修改 精度缺失问题 框架的使用 三&#xff1a;解决方案&#xff1a; …...

Magical Combat VFX

这个包包含30个可供游戏使用的VFX,有各种口味,为您的游戏增添趣味! 所有VFX都经过了很好的优化,可以在所有平台上使用。 这个包特别有一堆闪电魔法,有两种主要的变体,一种是深色的,另一种是浅色的。但它也提供了一系列其他视觉效果,如神圣咒语、音乐主题等等! 我们提供…...

hadoop伪分布式环境搭建详解

&#xff08;操作系统是centos7&#xff09; 1.更改主机名&#xff0c;设置与ip 的映射关系 hostname //查看主机名 vim /etc/hostname //将里面的主机名更改为master vim /etc/hosts //将127.0.0.1后面的主机名更改为master&#xff0c;在后面加入一行IP地址与主机名之间的…...

day12-SpringBootWeb 登录认证

一、登录功能 Slf4j RestController public class LoginController {Autowiredprivate EmpService empService;PostMapping("/login")public Result login(RequestBody Emp emp){log.info("员工登录: {}", emp);Emp e empService.login(emp);//登录失败, …...

论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)

HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...

【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力

引言&#xff1a; 在人工智能快速发展的浪潮中&#xff0c;快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型&#xff08;LLM&#xff09;。该模型代表着该领域的重大突破&#xff0c;通过独特方式融合思考与非思考…...

蓝桥杯3498 01串的熵

问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798&#xff0c; 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...

使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度

文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...

【无标题】路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论

路径问题的革命性重构&#xff1a;基于二维拓扑收缩色动力学模型的零点隧穿理论 一、传统路径模型的根本缺陷 在经典正方形路径问题中&#xff08;图1&#xff09;&#xff1a; mermaid graph LR A((A)) --- B((B)) B --- C((C)) C --- D((D)) D --- A A -.- C[无直接路径] B -…...

基于IDIG-GAN的小样本电机轴承故障诊断

目录 🔍 核心问题 一、IDIG-GAN模型原理 1. 整体架构 2. 核心创新点 (1) ​梯度归一化(Gradient Normalization)​​ (2) ​判别器梯度间隙正则化(Discriminator Gradient Gap Regularization)​​ (3) ​自注意力机制(Self-Attention)​​ 3. 完整损失函数 二…...

【51单片机】4. 模块化编程与LCD1602Debug

1. 什么是模块化编程 传统编程会将所有函数放在main.c中&#xff0c;如果使用的模块多&#xff0c;一个文件内会有很多代码&#xff0c;不利于组织和管理 模块化编程则是将各个模块的代码放在不同的.c文件里&#xff0c;在.h文件里提供外部可调用函数声明&#xff0c;其他.c文…...

PostgreSQL 与 SQL 基础:为 Fast API 打下数据基础

在构建任何动态、数据驱动的Web API时&#xff0c;一个稳定高效的数据存储方案是不可或缺的。对于使用Python FastAPI的开发者来说&#xff0c;深入理解关系型数据库的工作原理、掌握SQL这门与数据库“对话”的语言&#xff0c;以及学会如何在Python中操作数据库&#xff0c;是…...

Java毕业设计:办公自动化系统的设计与实现

JAVA办公自动化系统 一、系统概述 本办公自动化系统基于Java EE平台开发&#xff0c;实现了企业日常办公的数字化管理。系统包含文档管理、流程审批、会议管理、日程安排、通讯录等核心功能模块&#xff0c;采用B/S架构设计&#xff0c;支持多用户协同工作。系统使用Spring B…...

C++参数传递 a与a的区别

在 C 中&#xff0c;&a&#xff08;引用&#xff09;和 a&#xff08;值传递&#xff09; 的关键区别在于 参数如何传递给函数&#xff0c;以及由此引发的 性能、语义和安全问题。 最核心的在于你想不想传入的参数被改变&#xff0c;如果想&#xff0c;就用参数传递&#…...