SpringBoot集成Redis解决表单重复提交接口幂等(亲测可用)

简介: SpringBoot集成Redis解决表单重复提交接口幂等(亲测可用)

1.接口幂等介绍

接口幂等性是指同一个接口,多次发出同一个请求,必须保证操作只执行一次。即用户对于同一个接口发起的一次请求或者多次请求的结果是一致的,不会因为多次请求而产生不同的结果。

在应用中,如果一个接口没有设计成幂等的,那么每次请求可能会产生不同的结果,这可能会导致数据的不一致性。因此,在设计接口时,需要考虑接口的幂等性。

2.防止重复提交的几种方式

  1. 使用Redis记录请求信息:在用户提交请求时,将请求的唯一标识(如Token、接口名、请求参数等)存储到Redis中。如果Redis中已经存在相同的请求信息,说明该请求已经被处理过,可以直接返回结果或者给出相应的提示。
  2. 数据库表增加唯一索引约束:在数据库表中增加唯一索引约束,确保同一个请求的数据不会被重复插入。当有重复请求到达时,数据库会因为唯一索引约束而拒绝插入操作,从而防止重复提交。
  3. 请求唯一ID:为每个请求生成一个唯一的ID,并在处理请求时检查该ID是否已经存在。如果存在,说明该请求已经被处理过,直接返回结果或者给出相应的提示。
  4. 使用分布式锁:通过分布式锁机制,确保同一个请求在多个节点上不会被重复处理。当一个节点处理完请求后,会释放锁,其他节点在获取锁之前无法处理该请求,从而防止重复提交。

3.我采用的是 Redis

1.表单提交前获取Token

2.提交表单的时候参数或在请求头中带着Token就可以

4.集成Redis

SpringBoot配置文件
server:
  port: 9000
  tomcat:
    basedir: /.
 
spring:
  profiles:
    active: dev
  redis:
    host: 127.0.0.1
    port: 6379
    password:
pom.xml
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>provided</scope>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
 
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>

5.Redis封装

package com.lp.redis;
 
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
 
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
 
/**
 * @author liu pei
 * @date 2023年12月18日 下午6:44
 * @Description: 封装redis
 */
@Service
public class RedisService {
    @Resource
    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;
 
    }
 
}

6.自定义注解

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

package com.lp.ann;
 
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 FromAnnId {
}

7.表单唯一ID,创建和唯一验证

package com.lp.service;
 
import cn.hutool.core.util.StrUtil;
import com.lp.redis.RedisService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
 
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
 
/**
 * @author liu pei
 * @date 2023年12月18日 下午6:52
 * @Description:
 */
@Service
public class FromTokenService {
    @Resource
    private RedisService redisService;
 
    public static String FROM_KEY = "redis:from:token:";
 
    public static String FROM_HEADER_KEY = "FROM-TOKEN-ID";
 
    /**
     * 创建token
     *
     * @return
     */
    public String createToken() {
        String str = UUID.randomUUID().toString();
        try {
            String token = StringUtils.join(FROM_KEY,str);
            //设置默认过期时间:30分钟
            redisService.setEx(token, str,30000L);
            boolean notEmpty = StrUtil.isNotEmpty(str);
            if (notEmpty) {
                return str;
            }
        }catch (Exception ex){
            ex.printStackTrace();
        }
        return null;
    }
 
 
    /**
     * 检验token
     *
     * @param request
     * @return
     */
    public boolean checkToken(HttpServletRequest request) throws Exception {
 
        String formId = request.getHeader(FROM_HEADER_KEY);
        String token = StringUtils.join(FROM_KEY,formId);
        if (StrUtil.isBlank(formId)) {// header中不存在token
            formId = request.getParameter(FROM_HEADER_KEY);
            if (StrUtil.isBlank(formId)) {// parameter中也不存在token
                throw new RuntimeException("重复提交参数异常。");
            }
        }
 
        if (!redisService.exists(token)) {
            throw new RuntimeException("提交已过期。");
        }
 
        boolean remove = redisService.remove(token);
        if (!remove) {
            throw new RuntimeException("表单提交异常。");
        }
        return true;
    }
}

8.拦截器配置也可以用AOP(我用的是拦截器)

package com.lp.interceptor;
 
import cn.hutool.json.JSONUtil;
import com.lp.ann.FromAnnId;
import com.lp.service.FromTokenService;
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.annotation.Resource;
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;
 
/**
 * @author liu pei
 * @date 2023年12月18日 下午7:12
 * @Description:
 */
@Component
public class AuthInterceptor implements HandlerInterceptor {
    @Resource
    private FromTokenService 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();
        //被ApiIdempotment标记的扫描
        FromAnnId fromAnnId = method.getAnnotation(FromAnnId.class);
        if (fromAnnId != null) {
            try {
                return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
            }catch (Exception ex){
                HashMap<String, Object> hashMap = new HashMap<>();
                hashMap.put("msg",ex.getMessage());
                hashMap.put("code",500);
                writeReturnJson(response, JSONUtil.toJsonStr(hashMap));
                throw ex;
            }
        }
        //必须返回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();
        }
    }
}
package com.lp.config;
 
import com.lp.interceptor.AuthInterceptor;
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;
 
/**
 * @author liu pei
 * @date 2023年12月18日 下午7:09
 * @Description:
 */
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
    @Resource
    private AuthInterceptor authInterceptor;
 
    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor);
        super.addInterceptors(registry);
    }
}

9.测试

1.使用创建Token接口创建一个token
2.使用FROM-TOKEN-ID在save()方法中填写请求头参数。
例如:FROM-TOKEN-ID:token
package com.lp.controller;
 
import com.lp.service.FromTokenService;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import javax.annotation.Resource;
import java.util.Map;
 
/**
 * @author liu pei
 * @date 2023年12月18日 下午7:18
 * @Description:
 */
@RestController
@RequestMapping("/redis")
public class TestController {
    @Resource
    private FromTokenService tokenService;
 
    @RequestMapping("/token")
    public Object fromToken(){
        return tokenService.createToken();
    }
    @RequestMapping("/save")
    @FromAnnId 
    public Object save(@RequestBody Map<String,Object> user){
        System.out.println(user);
        return user;
    }
}

创作不易记得关注

相关文章
|
安全 Java Apache
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 身份和权限认证
本文介绍了 Apache Shiro 的身份认证与权限认证机制。在身份认证部分,分析了 Shiro 的认证流程,包括应用程序调用 `Subject.login(token)` 方法、SecurityManager 接管认证以及通过 Realm 进行具体的安全验证。权限认证部分阐述了权限(permission)、角色(role)和用户(user)三者的关系,其中用户可拥有多个角色,角色则对应不同的权限组合,例如普通用户仅能查看或添加信息,而管理员可执行所有操作。
758 0
|
安全 Java 数据安全/隐私保护
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 三大核心组件
本课程介绍如何在Spring Boot中集成Shiro框架,主要讲解Shiro的认证与授权功能。Shiro是一个简单易用的Java安全框架,用于认证、授权、加密和会话管理等。其核心组件包括Subject(认证主体)、SecurityManager(安全管理员)和Realm(域)。Subject负责身份认证,包含Principals(身份)和Credentials(凭证);SecurityManager是架构核心,协调内部组件运作;Realm则是连接Shiro与应用数据的桥梁,用于访问用户账户及权限信息。通过学习,您将掌握Shiro的基本原理及其在项目中的应用。
596 0
|
前端开发
SpringBoot2.3.1集成Knife4j接口文档
SpringBoot2.3.1集成Knife4j接口文档
1363 44
|
JSON 分布式计算 大数据
springboot项目集成大数据第三方dolphinscheduler调度器
springboot项目集成大数据第三方dolphinscheduler调度器
906 3
|
缓存 JSON 前端开发
第07课:Spring Boot集成Thymeleaf模板引擎
第07课:Spring Boot集成Thymeleaf模板引擎
1105 0
第07课:Spring Boot集成Thymeleaf模板引擎
|
Java 关系型数据库 MySQL
springboot项目集成dolphinscheduler调度器 实现datax数据同步任务
springboot项目集成dolphinscheduler调度器 实现datax数据同步任务
1195 2
|
分布式计算 Java 大数据
springboot项目集成dolphinscheduler调度器 可拖拽spark任务管理
springboot项目集成dolphinscheduler调度器 可拖拽spark任务管理
692 2
|
缓存 Java 数据库
SpringBoot集成Ehcache缓存使用指南
以上是SpringBoot集成Ehcache缓存的基本操作指南,帮助你在实际项目中轻松实现缓存功能。当然,Ehcache还有诸多高级特性,通过学习和实践,你可以更好地发挥它的威力。
1084 20
|
缓存 安全 Java
Shiro简介及SpringBoot集成Shiro(狂神说视频简易版)
Shiro简介及SpringBoot集成Shiro(狂神说视频简易版)
1050 7

热门文章

最新文章