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

本文涉及的产品
云数据库 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
相关文章
|
22天前
|
消息中间件 Java Kafka
Springboot集成高低版本kafka
Springboot集成高低版本kafka
|
4天前
|
Java Spring
Spring Boot脚手架集成校验框架
Spring Boot脚手架集成校验框架
12 0
|
5天前
|
Java Docker 容器
SpringBoot项目集成XXL-job
SpringBoot项目集成XXL-job
|
7天前
|
Java 关系型数据库 数据库
【SpringBoot系列】微服务集成Flyway
【4月更文挑战第7天】SpringBoot微服务集成Flyway
【SpringBoot系列】微服务集成Flyway
|
12天前
|
前端开发 安全 Java
springboot集成springdoc-openapi(模拟前端请求)
springboot集成springdoc-openapi(模拟前端请求)
|
12天前
|
NoSQL 数据可视化 Java
Springboot整合redis
Springboot整合redis
|
13天前
|
人工智能 前端开发 Java
Java语言开发的AI智慧导诊系统源码springboot+redis 3D互联网智导诊系统源码
智慧导诊解决盲目就诊问题,减轻分诊工作压力。降低挂错号比例,优化就诊流程,有效提高线上线下医疗机构接诊效率。可通过人体画像选择症状部位,了解对应病症信息和推荐就医科室。
160 10
|
22天前
|
NoSQL Java Redis
Springboot整合redis
Springboot整合redis
|
22天前
|
SQL Java 调度
SpringBoot集成quartz定时任务trigger_state状态ERROR解决办法
SpringBoot集成quartz定时任务trigger_state状态ERROR解决办法
|
2月前
|
NoSQL Java Redis
SpringBoot集成Redis
SpringBoot集成Redis
464 0