redis|Reids解决海量重复提交问题

在目前高并发的生产环境下,一个对外暴露的接口往往会面临很多次请求,此时我们需要对这些请求进行过滤,来保证幂等性的问题。
1.什么叫幂等性

任意多次执行所产生的影响均与一次执行的影响相同
所以按照这个幂等性的定义,我们通俗来讲就是对数据库的影响只能是一次性的,不能重复处理。
2.如何保证幂等性
  • 1.数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据
  • 2.token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。
  • 3.悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)
    对于悲观锁,乐观锁不了解的可以看看《浅谈悲观锁、乐观锁》,对于select … for update 行锁还是表锁不了解的,可以阅读一下《select…for update会锁表还是锁行》
  • 4.先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。
redis|Reids解决海量重复提交问题
文章图片

3.搭建 Redis 服务 API
  • 点击此处查看windows安装redis
  • 点击此处查看SpringBoot集成redis
对应的redis工具类代码如下:
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 org.springframework.stereotype.Service; import javax.annotation.Resource; import java.io.Serializable; import java.util.concurrent.TimeUnit; @Service public class RedisUtils { @Autowired private RedisTemplate redisTemplate; /** * 写入缓存 * @param key * @param value * @return */ public boolean set(final String key, Object value) { boolean result = false; try { ValueOperations, 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, 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, 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; } }

4.自定义注解 AutoIdempotent 自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。
后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface AutoIdempotent { boolean required() default true; }

  • 使用元注解ElementType.METHOD表示它只能放在方法上
  • etentionPolicy.RUNTIME表示它在运行时。
5.token 创建和检验 【redis|Reids解决海量重复提交问题】token服务接口:我们新建一个接口,创建token服务,里面主要是两个方法:
  • 一个用来创建token
  • 一个用来验证token
创建token主要产生的是一个字符串,检验token的话主要是传达request对象,为什么要传request对象呢?
主要作用就是获取header里面的token,然后检验,通过抛出的Exception来获取具体的报错信息返回给前端。
5.1 TokenService
import javax.servlet.http.HttpServletRequest; public interface TokenService { /** * 创建token * @return */ public String createToken(); /** * 检验token * @param request * @return */ public boolean checkToken(HttpServletRequest request) throws Exception; }

  • token的服务实现类:token引用了redis服务,创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,最后返回这个token值。
  • checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。
接下来我们接着看看tokenService的实现类,在实现实现类之前,我们先做一些必要的工作:
5.2 添加lombok 《idea集成lomBook》
5.3 创建自定义异常
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class CustomException extends RuntimeException { private Integer code; private String msg; }

但是我们返回给前端的信息不能是系统抛出的异常,我们需要拦截异常并且对用户进行友好提示 ,所以我们还需要拦截自定义异常
5.4 自定义异常拦截
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ResponseBody; import java.util.HashMap; import java.util.Map; @ControllerAdvice public class ExceptionHandler { //指定出现什么异常值执行这个方法 @org.springframework.web.bind.annotation.ExceptionHandler(CustomException.class) @ResponseBody//为了返回数据 public Map, Object> customExceptionHandler(CustomException ex) { Map, Object> map = new HashMap<>(); map.put("code", ex.getCode()); map.put("msg", ex.getMsg()); return map; } }

5.5 tokenService实现类
import com.example.repeat_submission.exception.CustomException; import com.example.repeat_submission.service.TokenService; import com.example.repeat_submission.utils.RedisUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.util.UUID; @Service public class TokenServiceImpl implements TokenService { @Autowired RedisUtils redisUtils; private final String TOKEN_PREFIX = "ninesun"; private final String TOKEN_NAME = "X-Token"; @Override public String createToken() { String str = UUID.randomUUID().toString(); StringBuilder token = new StringBuilder(); try { token.append(TOKEN_PREFIX).append(str); redisUtils.setEx(token.toString(), token.toString(), 10000L); if (!StringUtils.isEmpty(token.toString())) { return token.toString(); } } catch (Exception ex) { ex.printStackTrace(); } return null; }@Override public boolean checkToken(HttpServletRequest request) throws Exception {String token = request.getHeader(TOKEN_NAME); if (StringUtils.isEmpty(token)) {// header中不存在token token = request.getParameter(TOKEN_NAME); if (StringUtils.isEmpty(token)) {// parameter中也不存在token throw new CustomException(20001, "缺少参数token"); } }if (!redisUtils.exists(token)) { throw new CustomException(20001, "不能重复提交-------token不正确、空"); }boolean remove = redisUtils.remove(token); if (!remove) { throw new CustomException(20001, "Token刷新失败"); } return true; } }

6.配置拦截器 为了方便处理json与对象的转换,我们添加以下依赖
com.alibaba fastjson 1.2.47 commons-beanutils commons-beanutils 1.9.3 commons-collections commons-collections 3.2.1 commons-lang commons-lang 2.6 commons-logging commons-logging 1.1.1 net.sf.ezmorph ezmorph 1.0.6 net.sf.json-lib json-lib 2.2.3 jdk15

拦截器的配置如下:
  • WebConfiguration
import com.example.repeat_submission.interceptor.AutoIdempotentInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import javax.annotation.Resource; @Configuration public class WebConfiguration extends WebMvcConfigurerAdapter {@Resource private AutoIdempotentInterceptor autoIdempotentInterceptor; /** * 添加拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(autoIdempotentInterceptor); super.addInterceptors(registry); } }

  • AutoIdempotentInterceptor
import com.example.repeat_submission.annotation.AutoIdempotent; import com.example.repeat_submission.service.TokenService; import net.sf.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; 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.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; /** * 拦截器 */ @Component public class AutoIdempotentInterceptor implements HandlerInterceptor {@Autowired private TokenService 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)) {//如果没有注解,直接返回true return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); //被ApiIdempotment标记的扫描 if (method.isAnnotationPresent(AutoIdempotent.class)) { AutoIdempotent autoIdempotentAnnotation = method.getAnnotation(AutoIdempotent.class); //通过反射获取注解 if (autoIdempotentAnnotation.required()) { return tokenService.checkToken(request); // 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示}} //必须返回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 {}/** * 返回的json值 * @param response * @param json * @throws Exception */ private void writeReturnJson(HttpServletResponse response, String json) throws Exception { PrintWriter writer = null; response.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=utf-8"); try { writer = response.getWriter(); writer.print(json); } catch (IOException e) { } finally { if (writer != null) writer.close(); } }}

7.添加接口进行测试
import com.example.repeat_submission.annotation.AutoIdempotent; import com.example.repeat_submission.service.TokenService; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.HashMap; import java.util.Map; @RestController public class TestController {@Resource private TokenService tokenService; @PostMapping("/get/token") public Map, Object> getToken() { Map, Object> map = new HashMap<>(); String token = tokenService.createToken(); if (!StringUtils.isEmpty(token)) { map.put("code", 200); map.put("data", token); map.put("msg", "token创建成功"); } else { map.put("code", 500); map.put("msg", "token创建失败"); } return map; }@AutoIdempotent @PostMapping("/test/Idempotence") public String testIdempotence() { return "可以进行正常的业务"; } }

我们先拿到token
redis|Reids解决海量重复提交问题
文章图片

我们在header中添加token访问业务逻辑接口进行第一次测试
redis|Reids解决海量重复提交问题
文章图片

我们同样的header,同样的请求再来一次
redis|Reids解决海量重复提交问题
文章图片

可以看到请求已经被拦截了,原理呢就是需要我们每次请求结束之后,新的请求需要先获取一个令牌,有了这个令牌就可以进行正常的业务请求,一般适用于新增数据库操作或是高并发下获取大量数据的操作

    推荐阅读