腾讯二面:如何保证接口幂等性?高并发下的接口幂等性如何实现?

什么是接口幂等性

接口幂等性这一概念源于数学,原意是指一个操作如果连续执行多次所产生的结果与仅执行一次的效果相同,那么我们就称这个操作是幂等的。在互联网领域,特别是在Web服务、API设计和分布式系统中,接口幂等性具有非常重要的意义。

具体到HTTP接口或者服务间的API调用,接口幂等性就可以理解为当客户端对同一接口发起多次相同的请求时,服务端系统也应该确保只执行一次相应的操作,并且不论接收到了多少次请求,系统的状态变更始终是一致的,不会因为重复的请求而导致数据的错误。

比如我们常常遇到的订单创建,支付等业务。

  • 如果一个“创建订单”接口实现了幂等性,当收到两次同样的创建请求时,系统应该要么拒绝第二个请求(因为它已经是重复请求),要么确保只有一个订单被创建,而不是两个完全一样的订单。
  • 对于一个“支付”接口,幂等性要求即便用户由于网络原因反复点击支付按钮,服务端也只会扣除用户账户一次金额,避免重复扣费。

导致接口幂等性问题的原因

要向杜绝幂等性,那么我们就要之道导致接口幂等性问题的原因有哪些。接口幂等性问题通常由以下多种原因引起:

  1. 网络波动不稳定
    网络通信中的丢包、延迟等情况可能导致客户端未收到服务端的响应或服务端未收到客户端的请求,此时客户端可能会重试发送请求,导致接口被重复调用。

  2. 用户操作
    用户快速重复点击导致,例如用户在等待响应时,由于不确定是否操作成功,可能会多次点击提交按钮,进而发送多次相同的请求。再比如页用户频繁刷新页面,尤其是在某些提交操作尚未完成时,刷新页面可能会重新发送请求。还有用户可能在浏览器上点击回退然后再重复之间的提交操作,这都可能会导致重新发送请求。

  3. 重试机制
    在高可用性设计中,客户端常常设置有重试机制,当请求失败或超时时会自动重新发起请求。而在分布式系统中,服务间调用也可能有重试策略,以应对临时故障。比如Nginx重试,RPC重试,或者调用方业务层中进行重试。

  4. 定时任务或异步处理
    在定时任务中如果定时任务调度或逻辑设计不当,可能会导致同一任务被执行多次。或者在消息队列中,消息可能会因为异常等原因被重复消费。

  5. 并发控制
    缺乏有效的并发控制手段,导致在并发环境下,针对同一资源的操作被多次执行。

总的来说,导致接口幂等性问题可以粗略的归类于两种情况:前端调用以及服务端调用,那么我们可以针对这两种情况看一下如何去保证接口幂等。

如何保证接口幂等?

前端调用
页面控制

页面调用接口时可以通过禁用(如按钮置灰或显示加载状态)防止用户在请求未完成前重复点击,从而减少不必要的重复请求和可能的数据冲突。虽然在前端进行按钮置灰等操作可以辅助提高系统的幂等性表现,但是这个方式只是从用户体验和用户行为控制的角度来避免重复提交的一种方法,并没有从系统设计层面完全解决接口本身的幂等性问题。

使用RPG模式

PRG(POST/Redirect/GET)模式是一种前端交互策略,旨在解决用户刷新页面时可能导致表单数据重复提交的问题。它巧妙地利用了HTTP协议的特性,具体的交互流程如下:

  1. 用户在网页表单中填写数据,并通过POST请求将其发送至服务器进行处理,例如创建新资源或更新现有数据。
  2. 服务器接收到POST请求后,对提交的数据进行有效处理和持久化存储,并在操作成功后不直接返回处理结果,而是通过HTTP响应码302或303实现重定向,指示客户端发起一个新的GET请求去访问一个特定的URL。
  3. 客户端遵照服务器的重定向指示,自动发送GET请求访问新的URL,此时返回的页面将展示之前POST操作处理完毕的结果。
  4. 当用户在此后刷新页面时,浏览器只会按照常规方式重新发起GET请求,而非重新提交POST数据,因此有效地避免了重复提交引发的潜在问题
Token机制

Token机制是一种广泛应用互联网领域的认证与授权方法,特别是Web服务系统。token可以理解为一种安全凭证,它是由服务端生成并颁发给客户端的一段经过加密处理的字符串或数据结构,用来代表用户的某种状态或权限。

通过Token机制,我们可以解决接口幂等性问题。在接口中,我们允许重复提交,但是要保证重复提交不产生副作用,比如点击n次只产生一条记录,客户端每次请求都需要携带一个唯一的Token,而服务器则验证这个Token的有效性。如果服务器收到了一个已经使用过的Token就会认为这是一个重复请求并拒绝处理,从而确保接口的幂等性具体流握如下Token机制是一种常用的方法,用于确保接口的幂等性和防止重复请求。具体流程如下:

  1. 生成Token
    当用户开始执行一个需要确保幂等性的操作(如支付、下单、更新用户信息等)时,服务端会生成一个唯一的、有时效性的token。这个token可以是一个随机字符串或者带有时间戳和其他相关信息的哈希值,确保其唯一性。

  2. 存储Token
    生成的token会被存储在服务端的一个临时存储介质中,如Redis、Memcached或数据库,同时设置一个合理的过期时间(例如15分钟)。

  3. 传递Token
    将生成的token返回给客户端,客户端在进行后续的API调用时,需将此token作为请求参数或放在请求头中一并发送给服务端。

  4. 验证Token
    服务端在接收到带有token的请求时,首先检查token是否存在并且有效(未过期且未被使用过)。如果token有效且未被使用,则执行相应的业务逻辑,并在执行完成后立即从存储介质中移除或标记为已使用。若token已失效或已被使用,则拒绝此次请求,返回相应的错误提示,确保同一个操作不会被执行两次。

  5. 限制并发
    在并发场景下,通过原子操作(如Redis的SETNX命令)确保在验证token有效的同时,将其删除或更新状态,避免多个请求同时通过验证。

image.png

服务端控制

在服务端接口处理逻辑时,可以通过通过一些特定的标识符或请求参数来校验请求的幂等性,以确保同样的请求不会被重复处理。

唯一标识符

客户端每次发起请求会携带一个全局唯一的标识符。服务器接收到请求后就会对这个标识符进行检查,若服务器发现该标识符已经在系统中存在,表明这是一个重复请求,此时服务器可以选择忽略该请求,或者向客户端返回已处理过相同请求的结果信息。若服务器未找到该标识符存在于系统内,则认定该请求为新请求,服务器将继续对其进行正常处理,并将此唯一标识符保存至系统中,以便于后续对接收的请求进行有效性校验,防止同一请求的重复处理。比如我们在要求上游ERP系统对接订单平台时就会要求上游传递一个账号下全局唯一的一个参考单号,这个参考单号一个很重要的作用就是保证接口幂等性。

请求参数

某些请求参数确实可以用来辅助校验请求的幂等性。例如,时间戳可以作为一种可能的请求参数,在处理请求时,服务器可以通过比较时间戳与服务器当前时间来判断请求的有效性。若时间戳与当前时间之间的差异超出预设的合理范围(如几秒钟到几分钟不等,具体阈值视业务场景而定),服务器可以推测该请求可能是由于网络延迟或者其他原因导致的重复提交。

单纯依靠时间戳来判断幂等性和重复请求并不完全准确,因为不同的客户端时间可能并不精确同步,而且时间戳本身无法保证全局唯一性。但是它可以作为一种有效的辅助手段来减少重复处理的可能性

状态机设计

对于状态转移类的操作类型的业务,可采用状态机设计,每次请求只允许合法的状态变迁,非法状态变迁(如已经完成的订单不允许再次支付)将被拒绝。

乐观锁

在更新数据时,可以通过版本号或时间戳等机制判断数据是否已被修改,防止因并发请求导致的多次更新问题。具体做法:

  1. 在数据库表中增加一个版本号字段(version)或者时间戳字段(timestamp)。
  2. 客户端第一次请求时获取数据的版本号或时间戳。
  3. 客户端发起更新操作时,将上次读取的版本号或时间戳一起发送回服务器。
  4. 服务器在执行更新操作前,首先检查当前数据库中的版本号或时间戳是否与客户端提交的一致。
    • 如果一致,说明在这期间数据没有被其他事务修改过,于是更新数据并递增版本号或更新时间戳。
    • 如果不一致,说明数据已经被修改过,此时服务器拒绝本次更新请求,返回错误提示,客户端可以根据错误信息决定是否重新获取最新数据再尝试更新。

通过这种方式,即使客户端因为网络原因或其他因素导致同一请求被多次发送,乐观锁机制能确保只有在数据未被其他事务修改的前提下,才会执行更新操作,从而达到接口幂等的效果。

实现幂等性方案示例

从上述的几种解决幂等性问题的方案来看,使用token机制可以保证在不同请求动作下的幂等性。所以我们以此作为方案作为示例方案。

准备工作

我们使用Redis保存Token令牌,引入SpringBootRedisULID相关的依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>2.7.0</version>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.7.0</version>
</dependency><dependency><groupId>com.github.f4b6a3</groupId><artifactId>ulid-creator</artifactId><version>5.2.0</version>
</dependency>

Redis相关的配置

spring.redis.database=0  
spring.redis.host=127.0.0.1  
spring.redis.port=6379  
spring.redis.password=  
spring.redis.pool.max-active=8  
spring.redis.pool.max-wait=-1  
spring.redis.pool.max-idle=8  
spring.redis.pool.min-idle=0  
spring.redis.timeout=60  server.port=8080  
server.servlet.context-path=/coderacademy
生成Token令牌

使用ULID生成随机字符串,然后将其保存在Redis当中。这里以idempotent_token+账户+请求操作类型+token作为key。

private StringRedisTemplate stringRedisTemplate;/*** 存入 Redis 的 Token 键的前缀*/
private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:%s:$s:%s";/*** 生成token令牌** @param accountSecret 账户令牌* @param operatorType 接口请求类型,可以是接口url或者其他可以区分接口服务类型的值* @return token令牌*/
@Override
public String generateToken(String accountSecret, String operatorType) {// 创建或获取ULID生成器实例long timestampInMillis = LocalDateTime.now().atZone(ZoneOffset.systemDefault()).toInstant().toEpochMilli();Ulid ulid = UlidCreator.getUlid(timestampInMillis);String token = ulid.toString();// 设置存入 Redis 的 KeyString key = String.format(IDEMPOTENT_TOKEN_PREFIX, accountSecret, operatorType, token);// 存储 Token 到 Redis,且设置过期时间为5分钟stringRedisTemplate.opsForValue().set(key, accountSecret, 5, TimeUnit.MINUTES);// 返回 Tokenreturn token;
}
校验Token令牌

这里我们使用Redis执行Lua命令去查找以及删除key,Lua 表达式能保证命令执行的原子性。

/*** 验证 Token 正确性** @param token token 字符串* @param operatorType 接口请求类型,可以是接口url或者其他可以区分接口服务类型的值* @return 验证结果*/
private boolean validToken(String token, String accountSecret, String operatorType) {// 设置 Lua 脚本,其中 KEYS[1] 是 key,KEYS[2] 是 valueString script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);// 根据 Key 前缀拼接 KeyString key = String.format(IDEMPOTENT_TOKEN_PREFIX, accountSecret, operatorType, token);// 执行 Lua 脚本Long result = stringRedisTemplate.execute(redisScript, Arrays.asList(key, operatorType));// 根据返回结果判断是否成功成功匹配并删除 Redis 键值对,若果结果不为空和0,则验证通过if (result != null && result != 0L) {System.out.println(String.format("验证 token=%s,key=%s,value=%s 成功", token, key, operatorType));return true;}System.err.println(String.format("验证 token=%s,key=%s,value=%s 失败", token, key, operatorType));return false;
}
业务代码以及接口

我们在实现模拟创建订单的服务,在创建订单之前,首先校验token令牌。

/*** 创建订单接口** @param requestVO     创建订单参数* @param accountSecret 账户令牌* @param token         token令牌* @return 生成的订单号*/
@Override
public String createOrder(OrderCreateRequestVO requestVO, String accountSecret, String token) {// 根据 Token 和与用户相关的信息到 Redis 验证是否存在对应的信息boolean result = validToken(token, accountSecret, "createOrder");if (!result){// 这里需要自定义异常,统一处理异常,再统一响应返回throw new RuntimeException("重复的请求");}// 根据验证结果响应不同信息return "Success";
}

校验如果不存在token,则说明请求时重复请求,直接抛出异常,由统一异常管理,直接返回客户端请求失败的错误信息。关于SpringBoot中统一异常处理,统一结果响应,请查看:

我们在定义获取Token令牌的接口,以及创建订单的接口

@RestController
@RequestMapping("order")
public class OrderController {private IOrderService orderService;/*** 获取token接口* @param secret 账户令牌* @return*/@GetMapping("getToken")public String getToken(@RequestHeader("secret") String secret){return orderService.generateToken(secret, "createOrder");}/*** 创建订单接口* @param requestVO 参数* @param token token令牌* @param secret 账户令牌* @return 响应信息*/@PostMapping("create")public OrderCreateResponseVO createOrder(@RequestBody OrderCreateRequestVO requestVO,@RequestHeader("token") String token,@RequestHeader("secret") String secret){OrderCreateResponseVO responseVO = new OrderCreateResponseVO();String result = orderService.createOrder(requestVO, secret, token);responseVO.setSuccess(Boolean.TRUE);responseVO.setMsg(result);return responseVO;}@Autowiredpublic void setOrderService(IOrderService orderService) {this.orderService = orderService;}
}

我们使用Apifox模拟3个请求并发操作

image.png

执行结果如下:
image.png

控制台打印日志如下

image.png

可以看见只有1个请求成功了,并且控制台中打印只有一个token校验成功。

总结

幂等性是开发当中很常见也很重要的一个需求,尤其是订单,支付以及与金钱挂钩的服务,保证接口幂等性尤其重要。在实际开发中,我们需要针对不同的业务场景我们需要灵活的选择幂等性的实现方式:

  • 如果是web服务,客户端可以采取在页面上使用按钮置灰禁用,使用PRG模式,或者搭配后端的Token令牌进行解决。
  • 在服务端,我们可以采取唯一标识符,乐观锁,Token令牌,状态机等校验方式。

最后强调一下,实现幂等性需要先理解自身业务需求,根据业务逻辑来实现这样才合理,处理好其中的每一个结点细节,完善整体的业务流程设计,才能更好的保证系统的正常运行。

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/762336.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

嵌入式软件面试-linux-中高级问题

Linux系统启动过程&#xff1a; BIOS自检并加载引导程序。引导程序&#xff08;如GRUB&#xff09;加载Linux内核到内存。内核初始化硬件&#xff0c;加载驱动&#xff0c;建立内存管理。加载init进程&#xff08;PID为1&#xff09;&#xff0c;通常是systemd或SysVinit。init…

安卓使用MQTT实现阿里云物联网云台订阅和发布主题(3)

一、订阅主题代码讲解 private final String mqtt_sub_topic "/sys/k0wih08FdYq/LHAPP/thing/service/property/set";//订阅话题//mqtt客户端订阅主题//QoS0时&#xff0c;报文最多发送一次&#xff0c;有可能丢失//QoS1时&#xff0c;报文至少发送一次&#xff0c…

23 OpenCV 直方图比较

文章目录 直方图比较的目的相关性计算 (CV_COMP_CORREL)卡方计算 (CV_COMP_CHISQR)十字计算(CV_COMP_INTERSECT)巴氏距离计算 (CV_COMP_BHATTACHARYYA )compareHist 直方图比较算子示例 直方图比较的目的 直方图比较的目的是衡量两幅图像之间的相似度或差异度。通过计算图像的颜…

linux下用docker部署es和kibana(带ik分词器)(二)

在上一篇文章中讲到&#xff0c;我们利用docker安装了es和kibana&#xff0c;下面我们讲解一下在安装es时把ik分词器这个插件集成进去&#xff0c;首先我们编写一个dockerfile文件&#xff0c;自定义一个es镜像&#xff0c;当然这个镜像肯定集成了ik分词器&#xff0c;具体步骤…

【LVGL-开关部件】

LVGL-开关部件 ■ LVGL-开关部件■ 开关部件&#xff1a;指示器打开的颜色■ 开关部件&#xff1a;不可修改■ 开关部件&#xff1a;获取开关状态■ 开关部件&#xff1a;示例一&#xff1a;制冷,制暖,开关 ■ LVGL-开关部件 ■ 开关部件&#xff1a;指示器打开的颜色 ■ 开关部…

探讨Java代码混淆加固工具

摘要 本篇博客将介绍几种常用的Java代码混淆工具&#xff0c;如ProGuard、Allatori Java Obfuscator、VirboxProtector、ipaguard和DashO。我们将深入探讨它们的特点、功能以及在保护Java应用程序安全方面的作用。此外&#xff0c;还将强调在使用Java代码混淆工具时需要注意的…

RK3568笔记二十:PP-YOLOE部署测试

若该文为原创文章&#xff0c;转载请注明原文出处。 注&#xff1a;转换测试使用的是Autodl服务器&#xff0c;CUDA11.1版本&#xff0c;py3.8。 一、PP-YOLOE环境安装 创建环境 # 使用 conda 创建一个名为 PaddleYOLO 的环境&#xff0c;并指定 python 版本conda create -n…

(vue)新闻列表与图片对应显示,体现选中、移入状态

(vue)新闻列表与图片对应显示&#xff0c;体现选中、移入状态 项目背景&#xff1a;郑州院XX项目首页-新闻展示模块&#xff0c;鼠标移入显示对应图片&#xff0c;且体现选中和移入状态 首次加载&#xff1a; 切换列表后&#xff1a; html: <el-row :gutter"20"…

阶乘求和(第十四届蓝桥杯JavaB组省赛真题)

/ 10^9考虑前九位&#xff0c;% 10^9保留后9位 解题思路: 求获取结果的后九位数字&#xff0c;需要对10^9取余&#xff0c;因为202320232023这个数字的阶乘太大&#xff0c;必须要减少计算量&#xff0c;因为当一个整数乘以10^9后对其取余&#xff0c;那么结果都为0。 所以我…

【毕设级项目】基于ESP8266的家庭灯光与火情智能监测系统——文末源码及PPT

目录 系统介绍 硬件配置 硬件连接图 系统分析与总体设计 系统硬件设计 ESP8266 WIFI开发板 人体红外传感器模块 光敏电阻传感器模块 火焰传感器模块 可燃气体传感器模块 温湿度传感器模块 OLED显示屏模块 系统软件设计 温湿度检测模块 报警模块 OLED显示模块 …

FebHost:注册了新加坡.SG域名,还需要申请SSL证书吗?

在互联网飞速发展的今天&#xff0c;网站安全性已经成为评价一个网站是否专业、可靠的重要标准。对于注册了新加坡.SG域名的企业或个人而言&#xff0c;申请SSL证书不仅是保障网站数据安全的关键&#xff0c;更是提升网站在搜索引擎中排名的有效手段。 首先&#xff0c;SSL证书…

osgEarth学习笔记2-第一个Osg QT程序

原文链接 上个帖子介绍了osgEarth开发环境的安装。本帖介绍我的第一个Osg QT程序。 下载 https://github.com/openscenegraph/osgQt 解压&#xff0c;建立build目录。 使用Cmake-GUI Configure 根据需要选择win32或者x64&#xff0c;这里我使用win32. 可以看到include和lib路…

RK3568驱动指南|第二篇 字符设备基础-第13章 杂项设备驱动实验

瑞芯微RK3568芯片是一款定位中高端的通用型SOC&#xff0c;采用22nm制程工艺&#xff0c;搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码&#xff0c;支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU&#xff0c;可用于轻量级人工…

【go从入门到精通】select条件控制

作者简介&#xff1a; 高科&#xff0c;先后在 IBM PlatformComputing从事网格计算&#xff0c;淘米网&#xff0c;网易从事游戏服务器开发&#xff0c;拥有丰富的C&#xff0c;go等语言开发经验&#xff0c;mysql&#xff0c;mongo&#xff0c;redis等数据库&#xff0c;设计模…

MATLAB环境下基于振动信号的轴承状态监测和故障诊断

故障预测与健康管理PHM分为故障预测和健康管理与维修两部分&#xff0c;PHM首先借助传感器采集关键零部件的运行状态数据&#xff0c;如振动信号、温度图像、电流电压信号、声音信号及油液分析等&#xff0c;提取设备的运行监测指标&#xff0c;进而实现对设备关键零部件运行状…

复旦MBA GIP全球课程:数字化新趋势带来的机遇和挑战

复旦MBA站在商业教育变革前沿&#xff0c;联合世界顶级商学院开展长、中、短期的第二学位、海外交换及GIP全球课程&#xff0c;使同学们与世界紧密接轨&#xff0c;打破国界、专业和过往的边界&#xff0c;探索更广阔的未来可能。至今已有2000多名复旦MBA学生通过近60个GIP全球…

【vue】vue中的路由vue-router,vue-cli脚手架详细使用教程

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

Ubuntu Desktop - lock screen (锁屏)

Ubuntu Desktop - lock screen [锁屏] 1. System Settings -> Security & Privacy (安全和隐私)2. System Settings -> Keyboard -> Shortcuts -> System3. LockReferences 1. System Settings -> Security & Privacy (安全和隐私) 使用 Putty 远程登录…

FcaNet:频率通道注意力,进阶版SE

paper&#xff1a;https://arxiv.org/abs/2012.11879 github&#xff1a;GitHub - cfzd/FcaNet: FcaNet: Frequency Channel Attention Networks 目录 1. 动机 2. 方法 2.1. 回顾通道注意力和离散余弦变换&#xff08;DCT&#xff09; 通道注意力&#xff1a; 离散余弦变换…

如何使用Android平板公网访问本地Linux code-server

文章目录 1.ubuntu本地安装code-server2. 安装cpolar内网穿透3. 创建隧道映射本地端口4. 安卓平板测试访问5.固定域名公网地址6.结语 1.ubuntu本地安装code-server 准备一台虚拟机,Ubuntu或者centos都可以&#xff0c;这里以VMwhere ubuntu系统为例 下载code server服务,浏览器…