SprngBoot+redis+Token 解决接口幂等性问题

前言:
一个对外暴露的接口有可能会面临瞬间大量的重复请求提交,如何过滤掉重复的请求并保证不重复处理,那就需要实现幂等性

什么是幂等性:
任意多次执行所产生的影响均与一次执行的影响相同。也就是说对数据库的影响只能是一次性的,不能进行重复处理。

如何保证幂等性的解决方案:

  1. 数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据
  2. token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token (本文使用就是这种)
  3. 悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)
  4. 先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。

redis实现自动幂等的原理图:

一:创建redis的基础类

package com.bootdo.common.poweretc;

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.io.Serializable;
import java.util.concurrent.TimeUnit;

/**
 * @author Cc
 * @data 2020/4/21 12:18
 */
@Component
public class RedisService {
	@Autowired
	private RedisTemplate redisTemplate;

	/**
	 * 写入缓存
	 *
	 * @param key
	 * @param value
	 * @return
	 */
	public boolean set(final String key, Object value) {
		boolean result = false;
		try {
			ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
			operations.set(key, value);
			result = true;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}

	/**
	 * 写入缓存设置时效时间
	 *
	 * @param key
	 * @param value
	 * @return
	 */
	public boolean setEx(final String key, Object value, Long expireTime) {
		boolean result = false;
		try {
			ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
			operations.set(key, value);
			redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
			result = true;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}

	/**
	 * 判断缓存中是否有对应的value
	 *
	 * @param key
	 * @return
	 */
	public boolean exists(final String key) {
		return redisTemplate.hasKey(key);
	}

	/**
	 * 读取缓存
	 *
	 * @param key
	 * @return
	 */
	public Object get(final String key) {
		Object result = null;
		ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
		result = operations.get(key);
		return result;
	}

	/**
	 * 删除对应的value
	 *
	 * @param key
	 */
	public boolean remove(final String key) {
		if (exists(key)) {
			Boolean delete = redisTemplate.delete(key);
			return delete;
		}
		return false;
	}
}

二:创建生产 Tocken的接口及实现类

package com.bootdo.common.poweretc;

import javax.servlet.http.HttpServletRequest;

/**
 * 生成tomken 接口
 * @author Cc
 * @data 2020/4/21 11:57
 */
public interface ITokenDao {

	/**
	 * 创建token
	 * @return
	 */
	String createToken();
	/**
	 * 检验token
	 * @param request
	 * @return
	 */
	boolean checkToken(HttpServletRequest request) throws Exception;
}

创建Tocken的实现类

package com.bootdo.common.poweretc;


import com.bootdo.common.redis.shiro.RedisManager;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

/**
 * @author Cc
 * @data 2020/4/21 11:58
 */
@Service
public class TockenImpl implements ITokenDao {

	RedisManager redisManager = new RedisManager();

	@Autowired
	private RedisService redisService;
	/**
	 * @Description 创建token
	 * @param:
	 * @return java.lang.String
	 * @author Cc
	 * @date 2020/4/21 12:01
	 */
	@Override
	public String createToken() {

		String str = UUID.randomUUID().toString();
		StringBuilder token = new StringBuilder();
		try {
			token.append("LOGEN_PRES_").append(str);
			redisService.setEx(token.toString(), token.toString(), 10000L);
			if (StringUtils.isNotEmpty(token.toString())) {
				return token.toString();
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return null;
	}

	/**
	 * @Description 校验token
	 * @param:
	 * @param request
	 * @return boolean
	 * @author Cc
	 * @date 2020/4/21 12:01
	 */
	@Override
	public boolean checkToken(HttpServletRequest request) throws Exception {
		String token = request.getHeader(Constant.TOKEN_NAME);
		if (StringUtils.isEmpty(token)) {// header中不存在token
			token = request.getParameter(Constant.TOKEN_NAME);
			if (StringUtils.isNotEmpty(token)) {// parameter中也不存在token
				throw new RuntimeException("请求违法");
			}
		}
		if (!redisService.exists(token)) {
			throw new RuntimeException("请求违法");
		}
		boolean remove = redisService.remove(token);
		if (!remove) {
			throw new RuntimeException("redis 删除Key失败");
		}
		return true;
	}
}

三:自定义一个注解

定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等

/**
 * 它添加在需要实现幂等的方法上
 * @author Cc
 * @data 2020/4/21 12:19
 */
@Target(ElementType.METHOD)               //表示它只能放在方法上
@Retention(RetentionPolicy.RUNTIME)    //etentionPolicy.RUNTIME表示它在运行时
public @interface AutoICheckToken {

}

4.接下来创建拦截器

/**
 * @author Cc
 * @data 2020/4/21 12:21
 */
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {

	@Autowired
	private AutoIdempotentInterceptor autoIdempotentInterceptor;
	/**
	 * 添加拦截器
	 * @param registry
	 */
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(autoIdempotentInterceptor);
		super.addInterceptors(registry);
	}

}
/**
 * @author 拦截器
 * @data 2020/4/21 12:22
 */
@Component
public class AutoCheckTokenInterceptor implements HandlerInterceptor {

	@Autowired
	private ITokenDao tokenService;
	/**
	 * 预处理
	 *
	 * @param request
	 * @param response
	 * @param handler
	 * @return
	 * @throws Exception
	 */
	@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();
		//被AutoICheckToken标记的扫描
		AutoICheckToken methodAnnotation = method.getAnnotation(AutoICheckToken.class);
		if(methodAnnotation != null) {
			try{
				return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
			}catch(Exception ex){
				throw new RuntimeException("");
			}
		}
		//必须返回true,否则会被拦截一切请求
		return true;
	}
	@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{
	}
}

6.写Controller 进行校验是否能完成接口幂等

/**
 * @author Cc
 * @data 2020/4/21 14:44
 */
@RestController
public class CheckTokenController {

	@Autowired
	private ITokenDao tokenService;
	
	/**
	 * @Description 创建一个Tocken 并放到redis中
	 * @param: 
	 * @return java.lang.String
	 * @author Cc
	 * @date 2020/4/21 15:56 
	 */ 
	@GetMapping("/getToken")
	public String getToken() {
		String token = tokenService.createToken();
		if (StringUtils.isNotEmpty(token)) {
			return token;
		}
		return "获取Token失败";
	}

	/**
	 * @Description 访问该接口,如果Head 中不带 token测试是否会进入该方法
	 * @param: 
	 * @return java.lang.String
	 * @author Cc
	 * @date 2020/4/21 15:56 
	 */ 
	@AutoICheckToken
	@GetMapping("/testToken")
	public String testIdempotence() {
		return "成功";
	}
}

7.测试结果

验证逻辑:

1. 首先访问getToken()接口获取 Token
2. 访问 testToken()方法,并设置Head的值为Token,因为该接口有自定义注解【@AutoICheckToken】,所以会被自定义拦截器拦截
3. 自定义拦截器判断是否有Token,如果有则去redis中判断是否存在,如果存在则请求成功并在redis中删除Token,如果没有则抛出异常

获取Token成功在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

至此完成。

出现这个错误的原因是在导入seaborn包时,无法从typing模块中导入名为'Protocol'的对象。 解决这个问题的方法有以下几种: 1. 检查你的Python版本是否符合seaborn包的要求,如果不符合,尝试更新Python版本。 2. 检查你的环境中是否安装了typing_extensions包,如果没有安装,可以使用以下命令安装:pip install typing_extensions。 3. 如果你使用的是Python 3.8版本以下的版本,你可以尝试使用typing_extensions包来代替typing模块来解决该问题。 4. 检查你的代码是否正确导入了seaborn包,并且没有其他导入错误。 5. 如果以上方法都无法解决问题,可以尝试在你的代码中使用其他的可替代包或者更新seaborn包的版本来解决该问题。 总结: 出现ImportError: cannot import name 'Protocol' from 'typing'错误的原因可能是由于Python版本不兼容、缺少typing_extensions包或者导入错误等原因造成的。可以根据具体情况尝试上述方法来解决该问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [ImportError: cannot import name ‘Literal‘ from ‘typing‘ (D:\Anaconda\envs\tensorflow\lib\typing....](https://blog.youkuaiyun.com/yuhaix/article/details/124528628)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值