前言:
一个对外暴露的接口有可能会面临瞬间大量的重复请求提交,如何过滤掉重复的请求并保证不重复处理,那就需要实现幂等性
什么是幂等性:
任意多次执行所产生的影响均与一次执行的影响相同。也就是说对数据库的影响只能是一次性的,不能进行重复处理。
如何保证幂等性的解决方案:
- 数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据
- token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token (本文使用就是这种)
- 悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)
- 先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。
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,如果没有则抛出异常
至此完成。