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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 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;
    }
}

创作不易记得关注

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
存储 缓存 NoSQL
深入理解Django与Redis的集成实践
深入理解Django与Redis的集成实践
58 0
|
1月前
|
Java Maven Docker
gitlab-ci 集成 k3s 部署spring boot 应用
gitlab-ci 集成 k3s 部署spring boot 应用
|
16天前
|
XML Java 数据库连接
SpringBoot集成Flowable:打造强大的工作流管理系统
在企业级应用开发中,工作流管理是一个核心组件,它能够帮助我们定义、执行和管理业务流程。Flowable是一个开源的工作流和业务流程管理(BPM)平台,它提供了强大的工作流引擎和建模工具。结合SpringBoot,我们可以快速构建一个高效、灵活的工作流管理系统。本文将探讨如何将Flowable集成到SpringBoot应用中,并展示其强大的功能。
61 1
|
25天前
|
JSON Java API
springboot集成ElasticSearch使用completion实现补全功能
springboot集成ElasticSearch使用completion实现补全功能
26 1
|
16天前
|
XML 存储 Java
SpringBoot集成Flowable:构建强大的工作流引擎
在企业级应用开发中,工作流管理是核心功能之一。Flowable是一个开源的工作流引擎,它提供了BPMN 2.0规范的实现,并且与SpringBoot框架完美集成。本文将探讨如何使用SpringBoot和Flowable构建一个强大的工作流引擎,并分享一些实践技巧。
43 0
|
1月前
|
前端开发 Java 程序员
springboot 学习十五:Spring Boot 优雅的集成Swagger2、Knife4j
这篇文章是关于如何在Spring Boot项目中集成Swagger2和Knife4j来生成和美化API接口文档的详细教程。
107 1
|
1月前
|
存储 前端开发 Java
Spring Boot 集成 MinIO 与 KKFile 实现文件预览功能
本文详细介绍如何在Spring Boot项目中集成MinIO对象存储系统与KKFileView文件预览工具,实现文件上传及在线预览功能。首先搭建MinIO服务器,并在Spring Boot中配置MinIO SDK进行文件管理;接着通过KKFileView提供文件预览服务,最终实现文档管理系统的高效文件处理能力。
294 11
|
2月前
|
XML Java 关系型数据库
springboot 集成 mybatis-plus 代码生成器
本文介绍了如何在Spring Boot项目中集成MyBatis-Plus代码生成器,包括导入相关依赖坐标、配置快速代码生成器以及自定义代码生成器模板的步骤和代码示例,旨在提高开发效率,快速生成Entity、Mapper、Mapper XML、Service、Controller等代码。
springboot 集成 mybatis-plus 代码生成器
|
2月前
|
Java Spring
springboot 集成 swagger 2.x 和 3.0 以及 Failed to start bean ‘documentationPluginsBootstrapper‘问题的解决
本文介绍了如何在Spring Boot项目中集成Swagger 2.x和3.0版本,并提供了解决Swagger在Spring Boot中启动失败问题“Failed to start bean ‘documentationPluginsBootstrapper’; nested exception is java.lang.NullPointerEx”的方法,包括配置yml文件和Spring Boot版本的降级。
springboot 集成 swagger 2.x 和 3.0 以及 Failed to start bean ‘documentationPluginsBootstrapper‘问题的解决
|
1月前
|
Java Spring
springboot 学习十一:Spring Boot 优雅的集成 Lombok
这篇文章是关于如何在Spring Boot项目中集成Lombok,以简化JavaBean的编写,避免冗余代码,并提供了相关的配置步骤和常用注解的介绍。
101 0
下一篇
无影云桌面