首页 > 其他分享 >接口幂等性解决方案:基于token实现接口幂等的落地实现

接口幂等性解决方案:基于token实现接口幂等的落地实现

时间:2023-10-31 10:35:40浏览次数:33  
标签:请求 实现 request 接口 token import public String



文章目录

  • 一、什么是幂等
  • 二、基于token方案解决接口幂等问题
  • 1、token机制方案介绍
  • (1)实现思路
  • (2)问题:先执行业务再删除token
  • (3)问题:先删除token再执行业务
  • (4)方案缺点
  • 2、基于自定义业务流程实现
  • (1)获取token接口
  • (2)订单服务新增feign拦截器
  • (3)定义生成订单方法
  • (4)测试
  • 3、基于自定义注解实现
  • (1)自定义幂等性注解
  • (2)新增web拦截器
  • (3)使用自定义注解


一、什么是幂等

接口的幂等性——详细谈谈接口的幂等即解决方案

二、基于token方案解决接口幂等问题

1、token机制方案介绍

(1)实现思路

通过token机制来保证幂等是一种非常常见的解决方案,同时也适合绝大部分场景。该方案需要前后端进行一定程度的交互来完成。

接口幂等性解决方案:基于token实现接口幂等的落地实现_spring


1)服务端提供获取token接口,供客户端进行使用。服务端生成token后,如果当前为分布式架构,将token存放于redis中,如果是单体架构,可以保存在jvm缓存中。

2)当客户端获取到token后,会携带着token发起请求。

3)服务端接收到客户端请求后,首先会判断该token在redis中是否存在。如果存在,则完成进行业务处理,业务处理完成后,再删除token。如果不存在,代表当前请求是重复请求,直接向客户端返回对应标识。

(2)问题:先执行业务再删除token

在高并发下,很有可能出现第一次访问时token存在,完成具体业务操作。但在还没有删除token时,客户端又携带token发起请求,此时,因为token还存在,第二次请求也会验证通过,执行具体业务操作。

对于这个问题的解决方案的思想就是并行变串行。会造成一定性能损耗与吞吐量降低。
第一种方案:对于业务代码执行和删除token整体加线程锁。当后续线程再来访问时,则阻塞排队。

第二种方案:借助redis单线程和incr是原子性的特点。当第一次获取token时,以token作为key,对其进行自增。然后将token进行返回,当客户端携带token访问执行业务代码时,对于判断token是否存在不用删除,而是对其继续incr。如果incr后的返回值为2。则是一个合法请求允许执行,如果是其他值,则代表是非法请求,直接返回。

接口幂等性解决方案:基于token实现接口幂等的落地实现_架构_02

(3)问题:先删除token再执行业务

先删除token再执行业务,其实也会存在问题,假设具体业务代码执行超时或失败,没有向客户端返回

明确结果,那客户端就很有可能会进行重试,但此时之前的token已经被删除了,则会被认为是重复请求,不再进

行业务处理。

接口幂等性解决方案:基于token实现接口幂等的落地实现_spring_03

这种方案无需进行额外处理,一个token只能代表一次请求。一旦业务执行出现异常,则让客户端重新获取令牌,重新发起一次访问即可。推荐使用先删除token方案

(4)方案缺点

但是无论先删token还是后删token,都会有一个相同的问题。每次业务请求都回产生一个额外的请求去获取token。

但是,业务失败或超时,在生产环境下,一万个里最多也就十个左右会失败,那为了这十来个请求,让其他九千九百多个请求都产生额外请求,就有一些得不偿失了。虽然redis性能好,但是这也是一种资源的浪费。

2、基于自定义业务流程实现

接口幂等性解决方案:基于token实现接口幂等的落地实现_redis_04

(1)获取token接口

前端先通过获取token接口生成一个token,同时该token存入redis,设置过期时间。

@GetMapping("/genToken")
public String genToken(){
	String token = String.valueOf(IdUtils.nextId());
	redisTemplate.opsForValue().set(token,0,30, TimeUnit.MINUTES);
	return token;
}

(2)订单服务新增feign拦截器

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

@Component
public class FeignInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {

        //传递令牌
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes != null){
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            if (request != null){
                Enumeration<String> headerNames = request.getHeaderNames();
                while (headerNames.hasMoreElements()){
                    String hearName = headerNames.nextElement();
                    // 从请求header中获取token,并且传递
                    if ("token".equals(hearName)){
                        String headerValue = request.getHeader(hearName);
                        //传递token
                        template.header(hearName,headerValue);
                    }
                }
            }
        }
    }
}
// 定义feign拦截器
@Bean
public FeignInterceptor feignInterceptor(){
	return new FeignInterceptor();
}

(3)定义生成订单方法

/**
* 生成订单
* @param order
* @return
*/
@PostMapping("/genOrder")
public String genOrder(@RequestBody Order order, HttpServletRequest request){
	//获取令牌
	String token = request.getHeader("token");
	//校验令牌
	try {
		if (redisTemplate.delete(token)){
			//令牌删除成功,代表不是重复请求,执行具体业务
			order.setId(String.valueOf(idWorker.nextId()));
			order.setCreateTime(new Date());
			order.setUpdateTime(new Date());
			// 生成订单
			int result = orderService.addOrder(order);
			if (result == 1){
				System.out.println("success");
				return "success";
			}else {
				System.out.println("fail");
				return "fail";
			}
		}else {
			//删除令牌失败,重复请求
			System.out.println("repeat request");
			return "repeat request";
		}
	}catch (Exception e){
		throw new RuntimeException("系统异常,请重试");
	}
}

(4)测试

通过postman获取令牌,将令牌放入请求头中。开启两个postman tab页面。同时添加订单,可以发现一个执行成功,另一个重复请求。

3、基于自定义注解实现

直接把token实现嵌入到方法中会造成大量重复代码的出现。因此可以通过自定义注解将上述代码进行改造。在需要保证幂等的方法上,添加自定义注解即可。

(1)自定义幂等性注解

/**
* 幂等性注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idemptent {
}

(2)新增web拦截器

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

public class IdemptentInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
		// 如果添加了该注解
        Idemptent annotation = method.getAnnotation(Idemptent.class);
        if (annotation != null){
            //进行幂等性校验
            checkToken(request);
        }

        return true;
    }


    @Autowired
    private RedisTemplate redisTemplate;

    //幂等性校验
    private void checkToken(HttpServletRequest request) {
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)){
            throw new RuntimeException("非法参数");
        }

        boolean delResult = redisTemplate.delete(token);
        if (!delResult){
            //删除失败
            throw new RuntimeException("重复请求");
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

注册拦截器:

@Bean
public IdemptentInterceptor idemptentInterceptor() {
	return new IdemptentInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
	//幂等拦截器
	registry.addInterceptor(idemptentInterceptor());
	super.addInterceptors(registry);
}

(3)使用自定义注解

@Idemptent
@PostMapping("/genOrder")
public String genOrder(@RequestBody Order order){
	order.setId(String.valueOf(IdUtils.nextId()));
	order.setCreateTime(new Date());
	order.setUpdateTime(new Date());
	int result = orderService.addOrder(order);
	if (result == 1){
		System.out.println("success");
		return "success";
	}else {
		System.out.println("fail");
		return "fail";
	}
}


标签:请求,实现,request,接口,token,import,public,String
From: https://blog.51cto.com/u_13540373/8102155

相关文章

  • 【ROS2机器人入门到实战】坐标变换发布监听Python实现
    3.坐标变换发布监听Python实现写在前面当前平台文章汇总地址:ROS2机器人从入门到实战获取完整教程及配套资料代码,请关注公众号<鱼香ROS>获取教程配套机器人开发平台:两驱版|四驱版为方便交流,搭建了机器人技术问答社区:地址fishros.org.cn运行前面安装的jupyter,我们尝试使用代码来操......
  • 亚信科技AntDB数据库通过GB 18030-2022最高实现级别认证,荣膺首批通过该认证的产品之列
    近日,亚信科技AntDB数据库通过GB18030-2022《信息技术中文编码字符集》最高实现级别(级别3)检测认证,成为首批通过该认证的数据库产品之一。图1:AntDB通过GB18030-2022最高实现级别认证GB18030《信息技术中文编码字符集》是我国自主研制的以汉字为主、包含10种我国少数民族文字的超......
  • 界面控件DevExpress WPF Gauge组件 - 轻松实现个性化商业仪表盘
    DevExpressWPFGauge(仪表)控件包含了多种圆形仪表类型、水平和垂直线性仪表、分段和矩阵数字仪表以及状态指示器,同时还具有最终用户交互性的集成支持。P.S:DevExpressWPF拥有120+个控件和库,将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpressWPF能创建有着......
  • Lock实现线程间定制化通信
    Lock实现线程间定制化通信案例要求三个线程,AABBCCAA线程打印5次,BB线程打印10次,CC线程打印15次代码实现importjava.util.concurrent.locks.Condition;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;/***@author长名06......
  • ffmpeg实现视频的分割生成测试用的图片视频和音频
    测试代码如下:#!/bin/bash#提示用户输入参数read-p"请输入要切分的MP4文件名:"filenameread-p"请输入要生成的视频数量:"video_countread-p"请输入视频文件的大小范围(单位MB,例如10-20):"video_size_rangeread-p"请输入要生成的图片数量:"image_countread-......
  • JAVA-EE手写ThreadLocal源码实现一个线程一个连接对象------Java-Web项目
    手写ThreadLocalpackagecom.bjpowernode.ThreadLocal;importjava.util.HashMap;importjava.util.Map;publicclassMyThreadLocal<T>{privateMap<Thread,T>map=newHashMap<>();publicvoidset(To){//向threadLocal中绑定......
  • 使用【注解】加【拦截器】实现权限控制
    前面介绍了使用SpringSecurity进行权限控制,其中一个非常方便的特点就是:可以在类和方法上使用注解,从而实现对资源访问的权限控制。但是SpringSecurity具有一定的学习成本和复杂度,想要灵活驾驭并用好框架并非一件容易的事情,比如跟其它系统进行单点登录集成等等。本篇博客介绍......
  • 打印空心金字塔_Golang实现
    *******......
  • 基于Vue.js和Vanta.js的动态天空颜色效果实现
    背景最近在写一个Vue项目,想要在登录界面加一个动态背景效果,搜索之后发现了Vanta.js(https://www.vantajs.com/)这个库。Vanta可以借助three.js(WebGL)或p5.js渲染动态的3D背景效果,提供了多种预设。几种效果都挺不错的,最终我决定采用clouds效果。随即我发现这个效果是可......
  • LeetCode232.用栈实现队列
    题目描述请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):实现MyQueue类:voidpush(intx)将元素x推到队列的末尾intpop()从队列的开头移除并返回元素intpeek()返回队列开头的元素booleanempty()如果队列为空,返回tr......