菜谱网站开发,机械行业网站建设方案,公司法人查询系统,交互网站是什么一、前言 
在微服务开发中#xff0c;接口幂等性问题是一个常见却容易被忽视的问题#xff0c;同时对于微服务架构设计来讲#xff0c;好的幂等性设计方案可以让程序更好的应对一些高并发场景下的数据一致性问题。 二、幂等性介绍 2.1 什么是幂等性 
通常我们说的幂等性接口幂等性问题是一个常见却容易被忽视的问题同时对于微服务架构设计来讲好的幂等性设计方案可以让程序更好的应对一些高并发场景下的数据一致性问题。 二、幂等性介绍 2.1 什么是幂等性 
通常我们说的幂等性大多数情况下指的是服务端的接口幂等性。所以接口幂等性指的是用户对于同一个操作发起的一次请求或者多次请求结果是一致的。 举例来说一个用户在手机APP提200块钱一不小心点击了两次理论上应该只取出200块钱而不应该出来400当然真实场景下取钱操作是一个复杂事务不可能一个接口点击就出来了。在这种场景下即使用户点了两次也应该只取出一次的钱这就是接口幂等性。 2.2 幂等性问题产生原因 
什么情况下会出现接口幂等性问题呢通常来说主要有下面几个原因 用户重复提交当客户端发起多次相同的请求时服务器未能正确处理重复请求导致产生幂等性问题。  一般指用户填写好表单信息后由于服请求响应较慢从而多次点击提交按钮。  非法调用比如像第三方通过逆向手段调试到了接口地址然后通过爬虫或接口工具多次调用。  并发操作  在高并发环境下可能出现多个请求同时到达服务器如果服务器未能正确处理并发请求可能会导致幂等性问题的产生。  事务处理不当  如果接口需要进行事务处理并且没有正确地管理事务的提交和回滚也可能导致幂等性问题的产生。  失败重试  在分布式项目中被调用方出现超时或异常时触发了调用方的重试补偿机制。  重复消息  通常是指引入MQ的项目中对于同一个消息生产者多次发送或消费者重复消费。  
2.3 为什么需要接口幂等 
接口幂等性对于系统设计和开发具有重要意义尤其是在电商、金融、交易等数据一致性要求比较严苛的场景下幂等性的保障就显得格外重要。具体来说幂等性的作用主要如下。 2.3.1 减少重复操作的影响 
在网络通信中可能由于各种原因导致请求的重复发送如果接口是幂等的即使接收到了重复的请求系统也可以保持一致的状态避免产生额外的副作用。 比如服务A调用服务B接口进行转账假设A调用B时超时了一般来说超时的原因可能是网络传输丢包也可能是处理请求的服务还没有接收到请求或者接收到请求了但是还未来得及处理或者请求处理了但是在结果返回的途中丢了。如果此时A进行重试的话师傅会发生多笔转账呢所以在这种情况下如果下游的B服务接口如果没有做好幂等性保障的话就会出现很严重的问题。 2.3.2 提高系统可靠性 
当系统中接口具有幂等性时即使出现异常情况或故障系统也可以更容易恢复到正常状态从而降低系统崩溃的风险。 举例来说在mysql表中设计了基于version的字段在每次对表的数据进行update时为了保障接口幂等性可以基于version先查然后更新在这种方式下即便应用程序意外宕机或故障也可以方便的根据version值进行回溯快速恢复之前的数据。 2.3.3 简化客户端调用逻辑 
对于客户端来说无需关心接口的幂等性只需按照业务需求发送请求降低了客户端的复杂度和错误率。 当服务A调用服务B的时候对于服务A来说就是客户端并不打算因为调用B失败而特意做其他的业务处理在这种情况下就需要B服务即被调用方做好接口的幂等性处理从而A在调用时难度和复杂性就降低了。 2.3.4 便于系统扩展和集成 
当接口具有幂等性时系统可以更容易地进行横向扩展和集成不必担心多次请求会破坏系统状态。 三、接口幂等性与防重复提交 
3.1 接口幂等性与防重复提交概念 
这是在实际应用中很多同学混淆的概念但是两者都是在开发中服务端需要解决的问题其实来说接口幂等性和防重复提交是两个不同但相关的概念。具体来说 接口幂等性是指一个接口的多次重复调用所产生的影响与一次调用的影响相同。换句话说无论某个接口被调用多少次其结果都是一致的。这样设计的接口可以更容易处理各种问题比如网络超时、断网重试等情况而不会导致数据错误或状态混乱。  防重复提交是指在用户提交数据或请求时系统需要确保同样的数据或请求不会被重复处理多次。这通常涉及到在前端或后端做一些措施比如生成唯一标识、使用Token或者设置时间间隔来避免用户多次提交同样的请求。  在实际应用中接口幂等性和防重提交一般会结合起来使用以确保系统稳定性和数据准确性。接口设计时考虑到幂等性可以简化系统的逻辑处理而防重复提交则是为了提升用户体验和数据的完整性。 3.2 接口幂等性与防重复提交异同点 
相同点 都与接口或请求的重复操作有关  都涉及到处理系统中重复请求可能带来的问题如数据不一致、资源浪费等  不同点 接口幂等性是指一个接口多次调用所产生的影响与一次调用的影响相同。接口本身具有幂等性的特点无论调用多少次结果都是一致的。  重复提交指用户或系统在短时间内多次提交同一个请求或数据。重复提交通常发生在用户界面上可能导致数据重复处理或资源浪费。  四、接口幂等性解决方案 4.1 使用核心业务字段唯一性约束 
在很多业务场景中业务表都会设置某个字段的唯一性约束从而确定数据的唯一性通常来说可以添加字段的唯一索引。比如订单表的订单号用户表用户编码等。如果相同的请求再次发送过来由于字段的唯一性约束将会触异常而被捕获。如下是一段伪代码 
S       tring orderId  puk-3309-A;Order order  new Order();order.setOrderId(orderId);try {orderDao.save(order);}catch (Exception e){//唯一性约束异常return 订单已创建;} 
完整流程如下 4.1.1 唯一约束方案优缺点 
优点 使用简单代码集成难度较低  可靠性好  缺点 基于数据库自身的特性不够优雅  在应对较大的并发时具有一定的局限性  4.2 使用乐观锁解决幂等性问题 
乐观锁的使用场景非常多是一种很好的用于解决并发冲突幂等性问题的方案在mysql中乐观锁可以避免对行数据加锁从而提升系统的并发性能。通常依赖于数据版本号或时间戳等字段进行控制适用于需要对数据变更进行版本管理的场景。 如下有一张表表中有一个version的字段 
CREATE TABLE seek_order (id int(11) NOT NULL,amount int(12) DEFAULT NULL,version int(12) DEFAULT NULL,PRIMARY KEY (id)
) ENGINEInnoDB DEFAULT CHARSETutf8; 利用乐观锁的方式怎么解决接口的幂等性问题呢请参考如下的操作流程。 具体来说完整步骤如下 查询数据  select id,amount,version from seek_order where id1;  更新数据  update seek_order set amount  amount10 ,versionversion1 where id1 and version1;  判断更新的行数  大于0说明本次更新成功如果等于0说明本次没有对数据进行变更  本次操作数据的同时还修改了数据的版本号如果此时并发请求过来再次执行相同的sql时候update 并不会真正更新数据从而update的执行影响行数为0因为上一个update完成之后数据的version已经变成2了所以version1肯定无法满足条件了  为了保证接口幂等性接口可以直接返回客户端本次处理成功因为version已经修改了所以签名的请求一定成功过一次后面都是重复请求  与之类似的还有状态机字段比如处理订单的时候通常订单表中会有一个订单处理的状态比如0代表创建1代表待支付2为已支付...相同的update请求过来时需要带上status如果本次update的影响行数为0说明之前已经有更新成功了如果影响行数为1说明本次修改成功。 4.3 分布式锁解决幂等性问题 
上面前两种解决方案中其实都是利用了数据库的分布式锁机制但是在实际开发中一般并不太推荐使用一是并发性能有局限同时在捕获的异常中进行处理起来不够优雅所以如果基于锁的特性来解决可以采用分布式锁的方案来解决解决幂等性问题。 分布式锁有很多选择像主流的基于redis的分布式锁解决方案基于zookeeper的分布式锁解决方案等 如果以redis为例进行说明在解决接口幂等性问题时以生成订单的场景说明可以参考如下流程 具体来说操作步骤如下 生成唯一订单code作为唯一业务字段  使用redis的分布式锁利用code作为key同时需要设置key的超时时间  判断是否能设置成功如果能设置成功说明是第一次请求则进入核心逻辑处理  如果设置失败说明是重复请求直接返回成功即可  注意分布式锁一定需要设置一个合理的超时时间设置过短无法合理的防止重复请求设置太长则会浪费redis的存储空间 4.4 预置令牌解决幂等性问题 
预置令牌即token方案简单来说实现步骤如下 客户端发起业务操作前先请求服务端颁发token服务端生成一个token返回给客户端  服务端存储token到redis中并设置过期时间  客户端发起操作请求比如创建订单请求携带token  服务端接收请求并查询redis中是否存在token  如果不存在则执行业务操作  否则可认为是重复请求直接返回  业务逻辑操作完成后需要删除token  4.4 本地消息事件表 
在微服务场景中经常利用MQ对微服务进行解耦在使用MQ过程中一个容易出现的问题就是消息的重复发送或消息的重复消费在消息消费端如果没有对消息做幂等性处理的话可能会引发数据不一致问题。针对这种幂等性问题的场景可以考虑采用本地消息事件表来解决。 具体操作流程如下 生成者发送消息到MQ消息具备唯一的标识这里记为messageId  消费者第一次接收到消息并消费将业务处理结果入库同时记录业务唯一标识与messageId表示处理过  如果因为某种原因发生重复消费先拿着业务ID或messageId去映射表中查询  如果已经存在说明已经处理过是重复请求  五、代码实现 
接下来演示基于token的接口幂等性方案在代码中的实现过程。 
5.1 工程搭建 
5.1.1 创建一张表 
CREATE TABLE seek_order (id int(11) NOT NULL,amount int(12) DEFAULT NULL,version int(12) DEFAULT NULL,PRIMARY KEY (id)
) ENGINEInnoDB DEFAULT CHARSETutf8; 
5.1.2 创建maven工程 
工程目录如下 5.1.3 导入maven依赖 parentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.5.5/versionrelativePath/ !-- lookup parent from repository --/parentdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependencydependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactIdversion2.2.2/version/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-starter/artifactIdversion1.1.17/version/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactId/dependency/dependencies 
5.1.4 添加配置文件 
server:port: 8088spring:application:name: client-servicedatasource:url: jdbc:mysql://IP:3306/数据库名driverClassName: com.mysql.jdbc.Driverusername: rootpassword: rootredis:host: 127.0.0.1port: 6379mybatis:mapper-locations: classpath:mapper/*.xml#目的是为了省略resultType里的代码量type-aliases-package: com.congge.entityconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 
5.2 代码实现过程 
5.2.1 获取token接口 
一般来说token需要与当前登录人信息进行关联这里简单起见使用客户端请求IP为唯一标识同时在设置redis的key时候带上过期时间。 
public String getToken(HttpServletRequest request) {String ipAddr  IpUtil.getIpAddr(request);String token  UUID.randomUUID().toString();redisTemplate.opsForValue().set(ipAddr,token, 1,TimeUnit.MINUTES);return token;
} 
5.2.2 执行业务逻辑 
按照上述的流程客户端需要先获取token然后在真正执行业务逻辑时携带token Transactionalpublic String createOrder(HttpServletRequest request) {String token  request.getHeader(token);String ipAddr  IpUtil.getIpAddr(request);String redisToken  redisTemplate.opsForValue().get(ipAddr);if(StringUtils.isEmpty(redisToken)){throw new RuntimeException(重复请求);}if(!redisToken.equals(token)){throw new RuntimeException(无效token);}SeekOrder seekOrder  new SeekOrder();int maxId  seekOrderDao.getMaxId();seekOrder.setId(maxId1);seekOrder.setAmount(11);seekOrder.setVersion(1);seekOrderDao.saveOrder(seekOrder);//执行成功删除tokenredisTemplate.delete(ipAddr);return 订单创建成功;} 
5.3 功能测试 
5.3.1 客户端获取token 
调用获取token接口http://localhost:8088/token 5.3.2 创建订单 
在获取到token之后接下来执行创建订单接口需要把上一步的token带入到请求header中 再次执行创建订单接口由于约定了请求必须携带token第一次创建完成之后删除了token所以再次创建订单时候抛异常也可以直接返回创建成功。 5.4 优化改进 
可以看到在上面创建订单的处理逻辑中对请求是否重复的判断是比较冗余的或者说放到创建订单的主流程中不是很优雅但这种处理又是必须要的于是可以考虑在某个地方统一处理这里我们定义一个过滤器在过滤器中进行处理。 5.4.1 自定义拦截器 
package com.congge.filter;import com.congge.service.IpUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;WebFilter(urlPatterns  /*,filterName  orderCheckFilter)
public class OrderCheckFilter implements Filter {Autowiredprivate RedisTemplateString,String redisTemplate;Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request  (HttpServletRequest)servletRequest;HttpServletResponse response  (HttpServletResponse)servletResponse;System.out.println(request.getRequestURI());if(!/create.equals(request.getRequestURI())){filterChain.doFilter(servletRequest,servletResponse);return;}//只拦截创建订单的请求String token  request.getHeader(token);String ipAddr  IpUtil.getIpAddr(request);String redisToken  redisTemplate.opsForValue().get(ipAddr);if(StringUtils.isEmpty(redisToken)){throw new RuntimeException(重复请求);}if(!redisToken.equals(token)){throw new RuntimeException(无效token);}return;}Overridepublic void init(FilterConfig filterConfig) throws ServletException {System.out.println(orderCheckFilter init);}Overridepublic void destroy() {System.out.println(orderCheckFilter destroy);}} 5.4.2 创建订单逻辑改造 
将原本token处理的那一段逻辑移除 Transactionalpublic String createOrder(HttpServletRequest request) {String ipAddr  IpUtil.getIpAddr(request);SeekOrder seekOrder  new SeekOrder();int maxId  seekOrderDao.getMaxId();seekOrder.setId(maxId1);seekOrder.setAmount(11);seekOrder.setVersion(1);seekOrderDao.saveOrder(seekOrder);//执行成功删除tokenredisTemplate.delete(ipAddr);return 订单创建成功;} 
5.4.3 启动类添加注解 
启动类别忘了添加下面的注解 
SpringBootApplication
MapperScan(basePackages  {com.congge.mapper})
ServletComponentScan(com.congge.filter)
public class SeekOrderApp {public static void main(String[] args) {SpringApplication.run(SeekOrderApp.class,args);}
} 
5.4.4 功能测试验证 
按照上面的步骤再次测试当第二次相同的创建订单请求过来时将会抛出异常 六、写在文末 
本文详细介绍了在微服务开发场景中幂等性问题的解决方案并结合一个实际场景给出了代码案例幂等性问题的处理在实际开发中是一个不可忽视的问题尤其是对于数据的一致性要求比较高的场景有兴趣的同学可以基于本文提到的其他方案继续深入探究本篇到此结束感谢观看。