网站制作 合肥,建网站多少钱建个网站需要怎么做,wordpress 个人简历,三亚网站开发✅作者简介#xff1a;大家好#xff0c;我是Leo哥#xff0c;热爱Java后端开发者#xff0c;一个想要与大家共同进步的男人#x1f609;#x1f609; #x1f34e;个人主页#xff1a;Leo哥的博客 #x1f49e;当前专栏#xff1a; Java ✨特色专栏#xff1a; MyS… ✅作者简介大家好我是Leo哥热爱Java后端开发者一个想要与大家共同进步的男人 个人主页Leo哥的博客 当前专栏 Java ✨特色专栏 MySQL学习 本文内容什么是接口的幂等性如何保证接口的幂等性 个人知识库 知识库欢迎大家访问
1.前言☕
大家好我是Leo哥有半个月没更新了最近都忙着工作跟其他事情博客水都没时间水了。这不年前几天没啥任务了昨天看了下关于幂等性的一些问题今天早上抽空写了篇博客分享给大家。好了话不多说让我们开始吧。
2.什么是幂等性
幂等是一个数学与计算机学概念在数学中某一元运算为幂等时其作用在任一元素两次后会和其作用一次的结果相同。在计算机中编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数或幂等方法是指可以使用相同参数重复执行并能获得相同结果的函数。这些函数不会影响系统状态也不用担心重复执行会对系统造成改变。
3.什么是接口幂等性
所谓接口幂等性就是一次和多次请求某一个资源对于资源本身应该具有同样的结果。
接口的 幂等性Idempotence 是指一个操作在执行一次和多次执行时其结果是一样的。换句话说无论这个操作被执行多少次它对系统状态的影响都是相同的。幂等性是分布式系统中的一个重要概念它有助于确保系统的健壮性和一致性。
在网络通信和 API 设计中幂等性尤为重要因为它可以防止由于网络重传、客户端或服务器端的重复请求等问题导致的数据不一致。例如一个幂等的 HTTP 请求如 GET 或 PUT 请求在多次发送时服务器应该能够正确处理确保不会对资源造成意外的影响。
4.应用场景
不知道以下场景朋友们是否遇到过
前端重复提交表单 在填写一些表格时候用户填写完成提交很多时候会因网络波动没有及时对用户做出提交成功响应致使用户认为没有成功提交然后一直点提交按钮这时就会发生重复提交表单请求。用户恶意进行刷单 例如在实现用户投票这种功能时如果用户针对一个用户进行重复提交投票这样会导致接口接收到用户重复提交的投票信息这样会使投票结果与事实严重不符。接口超时重复提交 很多时候 HTTP 客户端工具都默认开启超时重试的机制尤其是第三方调用接口时候为了防止网络波动超时等造成的请求失败都会添加重试机制导致一个请求提交多次。消息进行重复消费 当使用 MQ 消息中间件时候如果发生消息中间件出现错误未及时提交消费信息导致发生重复消费。
没错这些都是幂等性问题。
接口幂等性是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的不会因为多次点击而产生了副作用。
这类问题多发于接口的
insert操作这种情况下多次请求可能会产生重复数据。update操作如果只是单纯的更新数据比如update user set status1 where id1是没有问题的。如果还有计算比如update user set statusstatus1 where id1这种情况下多次请求可能会导致数据错误。
5.解决方案
5.1 inset之前先select
通常情况下在保存数据的接口中我们为了防止产生重复数据一般会在insert前先根据name字段select一下数据。如果该数据已存在则执行update操作如果不存在才执行 insert操作。 5.2 数据库唯一索引
大多数情况下我们为了防止数据重复提交我们都会在表中添加唯一索引这个一个非常简单而且有奇效的方案。
alter table order add UNIQUE KEY t_code (code);加了唯一索引之后第一次请求数据可以插入成功。但后面的相同请求插入数据时会报Duplicate entry 002 for key order.t_code异常表示唯一索引有冲突。
具体流程图如下 5.3 Token机制
针对客户端连续点击或者调用方的超时重试等情况例如提交订单此种操作就可以用 Token 的机制实现防止重复提交。简单的说就是调用方在调用接口的时候先向后端请求一个全局 IDToken请求的时候携带这个全局 ID 一起请求**Token 最好将其放到 Headers 中**后端需要对这个 Token 作为 Key用户信息作为 Value 到 Redis 中进行键值内容校验如果 Key 存在且 Value 匹配就执行删除命令然后正常执行后面的业务逻辑。如果不存在对应的 Key 或 Value 不匹配就返回重复执行的错误信息这样来保证幂等操作。
该方案跟之前的所有方案都有点不一样需要两次请求才能完成一次业务操作。
第一次请求获取token第二次请求带着这个token完成业务操作。
具体流程图如下 5.4 悲观锁机制
比如某银行有个转账场景用户A手里有200块钱想转出100元正常情况下用户A转账之后只剩下了100元SQL如下
update user amount amount-100 where id888;但是实际情况下并非如此如果有个相同的请求进来用户A的账户就会一直扣减直到变成负数。这种情况用户A直接哭死在业务场景中也是不允许出现的。
通常情况下通过如下sql锁住单行数据
select * from user id888 for update;具体步骤
多个请求同时根据id查询用户信息。判断余额是否不足100如果余额不足则直接返回余额不足。如果余额充足则通过for update再次查询用户信息并且尝试获取锁。只有第一个请求能获取到行锁其余没有获取锁的请求则等待下一次获取锁的机会。第一个请求获取到锁之后判断余额是否不足100如果余额足够则进行update操作。如果余额不足说明是重复请求则直接返回成功。
悲观锁需要在同一个事务操作过程中锁住一行数据如果事务耗时比较长会造成大量的请求等待影响接口性能。
此外每次请求接口很难保证都有相同的返回值所以不适合幂等性设计场景但是在防重场景中是可以的使用的。
5.5 乐观锁机制
因为悲观锁是比较消耗的性能的操作那么我们为了提高接口性能完全可以使用乐观锁。需要再表中添加一个version字段。
比如
alter table user add version int2每当我们更新数据的时候需要对我们的版本号1
update user set amount amount100,versionversion1 where id 888 and version 1 更新数据的同时version1然后判断本次update操作的影响行数如果大于0则说明本次更新成功如果等于0则说明本次更新没有让数据变更。
等到下一个请求过来的时候依然回去执行这行SQL此时发现根本不可能满足version 1 这个条件因为version值已经修改了那么前面必定已经成功过一次后面都是重复的请求。
流程图如下 具体步骤
先根据id查询用户信息包含version字段根据id和version字段值作为where条件的参数更新用户信息同时version1判断操作影响行数如果影响1行则说明是一次请求可以做其他数据操作。如果影响0行说明是重复请求则直接返回成功。
5.6 建防重表
有时候我们的表中需要一些重复数据只有一些特殊场景才不需要重复数据此时我们上面的唯一索引方案可能就不太行了。
这时候直接在表中加唯一索引显然是不太合适的。
针对这种情况我们可以通过建防重表来解决问题。
简单来说就是我们再单独建立一张表只需要含有id和唯一索引当然唯一索引可以是多个字段比如name、code等组合起来的唯一标识
流程图如下
具体步骤
用户通过浏览器发起请求服务端收集数据。将该数据插入mysql防重表。判断是否执行成功如果成功则做mysql其他的数据操作可能还有其他的业务逻辑。如果执行失败捕获唯一索引冲突异常直接返回成功。
6.代码实践
以上我们都是给出了一些大概的解决方案跟思路接下来Leo哥大家以Token机制为例用代码实现如果解决接口的幂等性。
首先我们来回顾一下Token机制的整个流程。 下面开始直接上代码。
首先准备一个springboot工程项目只需要添加两个依赖即可。 然后开始编写Redis工具类跟一个简单的Token工具类。
RedisService
package org.javatop.idempotent.token;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;/*** author : Leo* version 1.0* date 2024-02-01 21:03* description :*/
Component
public class RedisService {Autowiredprivate RedisTemplate redisTemplate;public boolean setEx(String key, Object value, Long expireTime) {boolean result false;try {ValueOperations ops redisTemplate.opsForValue();ops.set(key, value);redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);result true;} catch (Exception e) {e.printStackTrace();}return result;}/*** 判断key是否存在* param key key* return*/public boolean exists(String key) {return Boolean.TRUE.equals(redisTemplate.hasKey(key));}/*** 删除key* param key key* return*/public boolean remove(String key) {if (exists(key)) {return Boolean.TRUE.equals(redisTemplate.delete(key));}return false;}
}TokenService
主要是生成一个全局唯一不重复的Token以及前端请求过来被拦截后需要检验token的方法。
package org.javatop.idempotent.token;import io.micrometer.common.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.javatop.idempotent.exception.IdempotentException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.util.UUID;/*** author : Leo* version 1.0* date 2024-02-01 21:01* description :*/
Component
public class TokenService {AutowiredRedisService redisService;public String createToken() {String uuid UUID.randomUUID().toString();redisService.setEx(uuid, uuid, 10000L);return uuid;}public boolean checkToken(HttpServletRequest request) throws IdempotentException {String token request.getHeader(token);if (StringUtils.isEmpty(token)) {token request.getParameter(token);if (StringUtils.isEmpty(token)) {throw new IdempotentException(token 不存在);}}if (!redisService.exists(token)) {throw new IdempotentException(重复的操作);}boolean remove redisService.remove(token);if (!remove) {throw new IdempotentException(重复的操作);}return true;}
}自定义幂等注解
我们自定义一个幂等注解来对我们想要幂等性一致的接口进行标识。
/*** author : Leo* version 1.0* date 2024-02-01 21:17* description : 幂等注解*/
Target(ElementType.METHOD)
Retention(RetentionPolicy.RUNTIME)
public interface AutoIdempotent {
}添加拦截器
在拦截器中我们解析出所有的请求标注有幂等注解的请求我们去检验他的token然后来决定下一步操作。
package org.javatop.idempotent.interceptor;import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.javatop.idempotent.annotation.AutoIdempotent;
import org.javatop.idempotent.exception.IdempotentException;
import org.javatop.idempotent.token.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;/*** author : Leo* version 1.0* date 2024-02-01 21:14* description :*/
Component
public class IdempotentInterceptor implements HandlerInterceptor {AutowiredTokenService tokenService;Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (!(handler instanceof HandlerMethod)) {return true;}HandlerMethod handlerMethod (HandlerMethod) handler;AutoIdempotent idempotent handlerMethod.getMethod().getAnnotation(AutoIdempotent.class);if (idempotent ! null) {try {return tokenService.checkToken(request);} catch (IdempotentException e) {throw e;}}return true;}
}测试
最后编写接口进行测试。 首先生成一个Token,然后把这个token放到hello接口的请求头上面。 可以看到第一次可以正常访问接口 但当你第二次访问该接口的时候已经提示你操作重复了。因为在我们第一次访问接口之后就把Redis中的token删除了。 7.总结
以上便是本文的全部内容本人才疏学浅文章有什么错误的地方欢迎大佬们批评指正我是Leo一个在互联网行业的小白立志成为更好的自己。
如果你想了解更多关于Leo可以关注公众号-程序员Leo后面文章会首先同步至公众号。