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的集成实践
54 0
|
21天前
|
Java 开发者 Spring
精通SpringBoot:16个扩展接口精讲
【10月更文挑战第16天】 SpringBoot以其简化的配置和强大的扩展性,成为了Java开发者的首选框架之一。SpringBoot提供了一系列的扩展接口,使得开发者能够灵活地定制和扩展应用的行为。掌握这些扩展接口,能够帮助我们写出更加优雅和高效的代码。本文将详细介绍16个SpringBoot的扩展接口,并探讨它们在实际开发中的应用。
37 1
|
27天前
|
存储 安全 Java
|
27天前
|
存储 算法 安全
SpringBoot 接口加密解密实现
【10月更文挑战第18天】
|
25天前
|
监控 Java 开发者
掌握SpringBoot扩展接口:提升代码优雅度的16个技巧
【10月更文挑战第20天】 SpringBoot以其简化配置和快速开发而受到开发者的青睐。除了基本的CRUD操作外,SpringBoot还提供了丰富的扩展接口,让我们能够更灵活地定制和扩展应用。以下是16个常用的SpringBoot扩展接口,掌握它们将帮助你写出更加优雅的代码。
46 0
|
2月前
|
SQL JSON Java
springboot 如何编写增删改查后端接口,小白极速入门,附完整代码
本文为Spring Boot增删改查接口的小白入门教程,介绍了项目的构建、配置YML文件、代码编写(包括实体类、Mapper接口、Mapper.xml、Service和Controller)以及使用Postman进行接口测试的方法。同时提供了SQL代码和完整代码的下载链接。
springboot 如何编写增删改查后端接口,小白极速入门,附完整代码
|
1月前
|
存储 NoSQL Java
Spring Boot项目中使用Redis实现接口幂等性的方案
通过上述方法,可以有效地在Spring Boot项目中利用Redis实现接口幂等性,既保证了接口操作的安全性,又提高了系统的可靠性。
39 0
|
4月前
|
监控 druid Java
spring boot 集成配置阿里 Druid监控配置
spring boot 集成配置阿里 Druid监控配置
291 6
|
4月前
|
Java 关系型数据库 MySQL
如何实现Springboot+camunda+mysql的集成
【7月更文挑战第2天】集成Spring Boot、Camunda和MySQL的简要步骤: 1. 初始化Spring Boot项目,添加Camunda和MySQL驱动依赖。 2. 配置`application.properties`,包括数据库URL、用户名和密码。 3. 设置Camunda引擎属性,指定数据源。 4. 引入流程定义文件(如`.bpmn`)。 5. 创建服务处理流程操作,创建控制器接收请求。 6. Camunda自动在数据库创建表结构。 7. 启动应用,测试流程启动,如通过服务和控制器开始流程实例。 示例代码包括服务类启动流程实例及控制器接口。实际集成需按业务需求调整。
372 4
|
4月前
|
消息中间件 Java 测试技术
【RocketMQ系列八】SpringBoot集成RocketMQ-实现普通消息和事务消息
【RocketMQ系列八】SpringBoot集成RocketMQ-实现普通消息和事务消息
323 1