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;
    }
}

创作不易记得关注

相关文章
|
2月前
|
NoSQL Java 网络安全
SpringBoot启动时连接Redis报错:ERR This instance has cluster support disabled - 如何解决?
通过以上步骤一般可以解决由于配置不匹配造成的连接错误。在调试问题时,一定要确保服务端和客户端的Redis配置保持同步一致。这能够确保SpringBoot应用顺利连接到正确配置的Redis服务,无论是单机模式还是集群模式。
257 5
|
3月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
245 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
6月前
|
机器学习/深度学习 数据采集 人机交互
springboot+redis互联网医院智能导诊系统源码,基于医疗大模型、知识图谱、人机交互方式实现
智能导诊系统基于医疗大模型、知识图谱与人机交互技术,解决患者“知症不知病”“挂错号”等问题。通过多模态交互(语音、文字、图片等)收集病情信息,结合医学知识图谱和深度推理,实现精准的科室推荐和分级诊疗引导。系统支持基于规则模板和数据模型两种开发原理:前者依赖人工设定症状-科室规则,后者通过机器学习或深度学习分析问诊数据。其特点包括快速病情收集、智能病症关联推理、最佳就医推荐、分级导流以及与院内平台联动,提升患者就诊效率和服务体验。技术架构采用 SpringBoot+Redis+MyBatis Plus+MySQL+RocketMQ,确保高效稳定运行。
467 0
|
9月前
|
存储 人工智能 NoSQL
SpringBoot整合Redis、ApacheSolr和SpringSession
本文介绍了如何使用SpringBoot整合Redis、ApacheSolr和SpringSession。SpringBoot以其便捷的配置方式受到开发者青睐,通过引入对应的starter依赖,可轻松实现功能整合。对于Redis,可通过配置RedisSentinel实现高可用;SpringSession则提供集群Session管理,支持多种存储方式如Redis;整合ApacheSolr时,借助Zookeeper搭建SolrCloud提高可用性。文中详细说明了各组件的配置步骤与代码示例,方便开发者快速上手。
176 11
|
2月前
|
JavaScript Java 关系型数据库
基于springboot的项目管理系统
本文探讨项目管理系统在现代企业中的应用与实现,分析其研究背景、意义及现状,阐述基于SSM、Java、MySQL和Vue等技术构建系统的关键方法,展现其在提升管理效率、协同水平与风险管控方面的价值。
|
2月前
|
搜索推荐 JavaScript Java
基于springboot的儿童家长教育能力提升学习系统
本系统聚焦儿童家长教育能力提升,针对家庭教育中理念混乱、时间不足、个性化服务缺失等问题,构建科学、系统、个性化的在线学习平台。融合Spring Boot、Vue等先进技术,整合优质教育资源,提供高效便捷的学习路径,助力家长掌握科学育儿方法,促进儿童全面健康发展,推动家庭和谐与社会进步。
|
2月前
|
JavaScript Java 关系型数据库
基于springboot的古树名木保护管理系统
本研究针对古树保护面临的严峻挑战,构建基于Java、Vue、MySQL与Spring Boot技术的信息化管理系统,实现古树资源的动态监测、数据管理与科学保护,推动生态、文化与经济可持续发展。
|
2月前
|
监控 安全 JavaScript
2025基于springboot的校车预定全流程管理系统
针对传统校车管理效率低、信息不透明等问题,本研究设计并实现了一套校车预定全流程管理系统。系统采用Spring Boot、Java、Vue和MySQL等技术,实现校车信息管理、在线预定、实时监控等功能,提升学校管理效率,保障学生出行安全,推动教育信息化发展。
|
2月前
|
人工智能 Java 关系型数据库
基于springboot的画品交流系统
本项目构建基于Java+Vue+SpringBoot+MySQL的画品交流系统,旨在解决传统艺术交易信息不透明、流通受限等问题,融合区块链与AI技术,实现画品展示、交易、鉴赏与社交一体化,推动艺术数字化转型与文化传播。