IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Reids解决海量重复提交问题 -> 正文阅读

[Java知识库]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.先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。

在这里插入图片描述

3.搭建 Redis 服务 API

对应的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<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;

    }
}

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 创建和检验

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<String, Object> customExceptionHandler(CustomException ex) {
        Map<String, 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与对象的转换,我们添加以下依赖

     <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <!-- JSONObject对象依赖的jar包 -->
        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>1.9.3</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>net.sf.ezmorph</groupId>
            <artifactId>ezmorph</artifactId>
            <version>1.0.6</version>
        </dependency>
        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.2.3</version>
            <classifier>jdk15</classifier><!-- 指定jdk版本 -->
        </dependency>

拦截器的配置如下:

  • 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<String, Object> getToken() {
        Map<String, 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
在这里插入图片描述

我们在header中添加token访问业务逻辑接口进行第一次测试
在这里插入图片描述

我们同样的header,同样的请求再来一次
在这里插入图片描述

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

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-03-17 21:56:42  更:2022-03-17 22:01:51 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 9:26:29-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码