`
m17165851127
  • 浏览: 15066 次
文章分类
社区版块
存档分类
最新评论

SpringBoot Redis 解决重复提交问题

 
阅读更多

前言

在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段:

1、数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据。

2、token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。

3、悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)

4、先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。

redis 实现自动幂等的原理图:

搭建 Redis 服务 API

1、首先是搭建redis服务器。

2、引入springboot中到的redis的stater,或者Spring封装的jedis也可以,后面主要用到的api就是它的set方法和exists方法,这里我们使用springboot的封装好的redisTemplate。

推荐一个 Spring Boot 基础教程及实战示例:

/**java项目 fhadmin.cn
 * redis工具类
 */
@Component
public class RedisService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 写入缓存
     * @param key
     * @param value
     * @return
     */
    public boolean set(finalString 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(finalString 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(finalString key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 读取缓存
     * @param key
     * @return
     */
    public Objectget(finalString key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }

    /**
     * 删除对应的value
     * @param key
     */
    public boolean remove(finalString key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        returnfalse;

    }

}

自定义注解 AutoIdempotent

自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在运行时。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {

}

token 创建和检验

token服务接口:我们新建一个接口,创建token服务,里面主要是两个方法,一个用来创建token,一个用来验证token。创建token主要产生的是一个字符串,检验token的话主要是传达request对象,为什么要传request对象呢?主要作用就是获取header里面的token,然后检验,通过抛出的Exception来获取具体的报错信息返回给前端。

publicinterface TokenService {

    /**java项目 fhadmin.cn
     * 创建token
     * @return
     */
    public  String createToken();

    /**
     * 检验token
     * @param request
     * @return
     */
    public boolean checkToken(HttpServletRequest request) throws Exception;

}

token的服务实现类:token引用了redis服务,创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,最后返回这个token值。checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。

@Service
publicclass TokenServiceImpl implements TokenService {

    @Autowired
    private RedisService redisService;

    /**
     * 创建token
     * java fhadmin.cn
     * @return
     */
    @Override
    public String createToken() {
        String str = RandomUtil.randomUUID();
        StrBuilder token = new StrBuilder();
        try {
            token.append(Constant.Redis.TOKEN_PREFIX).append(str);
            redisService.setEx(token.toString(), token.toString(),10000L);
            boolean notEmpty = StrUtil.isNotEmpty(token.toString());
            if (notEmpty) {
                return token.toString();
            }
        }catch (Exception ex){
            ex.printStackTrace();
        }
        returnnull;
    }

    /**
     * 检验token
     *
     * @param request
     * @return
     */
    @Override
    public boolean checkToken(HttpServletRequest request) throws Exception {

        String token = request.getHeader(Constant.TOKEN_NAME);
        if (StrUtil.isBlank(token)) {// header中不存在token
            token = request.getParameter(Constant.TOKEN_NAME);
            if (StrUtil.isBlank(token)) {// parameter中也不存在token
                thrownew ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
            }
        }

        if (!redisService.exists(token)) {
            thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
        }

        boolean remove = redisService.remove(token);
        if (!remove) {
            thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
        }
        returntrue;
    }
}

拦截器的配置

web配置类,实现WebMvcConfigurerAdapter,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中。

@Configuration
publicclass WebConfiguration extends WebMvcConfigurerAdapter {

    @Resource
   private AutoIdempotentInterceptor autoIdempotentInterceptor;

    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
        super.addInterceptors(registry);
    }
}

拦截处理器:主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。

/** fhadmin.cn
 * 拦截器
 */
@Component
publicclass AutoIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService 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)) {
            returntrue;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //被ApiIdempotment标记的扫描
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {
            try {
                return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
            }catch (Exception ex){
                ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
                writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
                throw ex;
            }
        }
        //必须返回true,否则会被拦截一切请求
        returntrue;
    }

    @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();
        }
    }

}

测试用例

模拟业务请求类,首先我们需要通过/get/token路径通过getToken()方法去获取具体的token,然后我们调用testIdempotence方法,这个方法上面注解了@AutoIdempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用TokenService中的checkToken()方法,如果捕获到异常会将异常抛出调用者,下面我们来模拟请求一下:

@RestController
publicclass BusinessController {

    @Resource
    private TokenService tokenService;

    @Resource
    private TestService testService;

    @PostMapping("/get/token")
    public String  getToken(){
        String token = tokenService.createToken();
        if (StrUtil.isNotEmpty(token)) {
            ResultVo resultVo = new ResultVo();
            resultVo.setCode(Constant.code_success);
            resultVo.setMessage(Constant.SUCCESS);
            resultVo.setData(token);
            return JSONUtil.toJsonStr(resultVo);
        }
        return StrUtil.EMPTY;
    }

    @AutoIdempotent
    @PostMapping("/test/Idempotence")
    public String testIdempotence() {
        String businessResult = testService.testIdempotence();
        if (StrUtil.isNotEmpty(businessResult)) {
            ResultVo successResult = ResultVo.getSuccessResult(businessResult);
            return JSONUtil.toJsonStr(successResult);
        }
        return StrUtil.EMPTY;
    }
}

使用postman请求,首先访问get/token路径获取到具体到token:

利用获取到到token,然后放到具体请求到header中,可以看到第一次请求成功,接着我们请求第二次:

第二次请求,返回到是重复性操作,可见重复性验证通过,再多次请求到时候我们只让其第一次成功,第二次就是失败:

 

0
0
分享到:
评论

相关推荐

    springboot2.1+redis+拦截器 防止表单重复提交

    首先,我们需要理解表单重复提交的问题。当用户点击提交按钮多次或者由于网络延迟导致的重复提交,服务器可能会接收到相同的数据请求多次,这可能会对业务逻辑产生不良影响。例如,在电商网站中,如果用户多次点击...

    redis专栏 002 springboot redis 防止表单重复提交

    在现代Web应用中,防止表单...总的来说,结合Redis和Spring Boot可以有效地解决Web应用中的表单重复提交问题,提供了健壮性和可扩展性。在实际项目中,可以根据具体需求进行优化和调整,以满足更高的性能和安全性要求。

    基于springboot实现表单重复提交.docx

    基于 SpringBoot 实现表单重复提交解决方案 表单重复提交是指在一次请求完成之前防止重复提交,解决表单重复提交有多种形式,以下以 Aop+自定义注解+Redis 为例来介绍。 解决方案的详细流程 1. 当页面加载时,...

    springboot+redis+AOP 防止表单重复提交

    总结起来,结合Spring Boot、Redis和AOP,我们可以构建出一个优雅的解决方案,防止表单的重复提交,保障系统的稳定性和数据一致性。这种方法充分利用了Spring Boot的便利性、Redis的高速缓存能力和AOP的代码组织优势...

    spring boot 防止重复提交实现方法详解

    Spring Boot 防止重复提交是指在用户提交表单或请求时,防止同一客户端在短时间内对同一 URL 的重复提交,从而避免服务器端的处理压力和数据的一致性问题。下面将详细介绍 Spring Boot 防止重复提交实现方法的相关...

    自定义注解解决API接口幂等设计防止表单重复提交(生成token存放到redis中)

    为了解决这一问题,我们可以采用自定义注解结合Redis来实现一个防止表单重复提交的解决方案。 首先,让我们理解自定义注解的核心思想。注解是一种元数据,它提供了在代码中添加信息的方式,这些信息可以被编译器或...

    #资源达人分享计划# SpringBoot自定义注解轻松解决防重复提交问题,每一步代码都有详细注释!!拿来即用!!!

    接口上面加上@NoRepeatSubmitAspect这个注解即可轻松完美解决重复提交问题,这个是Redis版本,性能最好,RedisUtils静态工具类也一并打包在内。如果项目不用redis,可以自行改成数据库查存校验!

    springboot防重复提交工具包

    这个名为"springboot防重复提交工具包"的资源很可能提供了一种解决方案,帮助开发者防止用户因网络延迟或其他原因导致的多次点击提交,从而避免数据库中的数据异常。 Spring Boot是基于Spring框架的轻量级开发工具...

    SpringBoot系列——防重放与操作幂等.doc

    在日常开发中,我们经常会遇到需要防止重复提交和操作幂等的问题,本文将记录 SpringBoot 实现简单防重放与幂等的方法。 防重放是指防止数据重复提交,例如用户多次点击提交按钮或接口短时间内被多次调用。操作幂等...

    基于 Springboot + Redis + Kafka 的秒杀系统,乐观锁 + 缓存 + 限流 + 异步

    系统的特点 高性能:秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键 一致性:秒杀商品减库存的实现方式同样关键,有限数量的商品在同一时刻被很多倍的请求同时来减...禁止重复提交:限定每个用户发起

    基于注解+redis实现表单防重复提交.zip

    本项目"基于注解+redis实现表单防重复提交.zip"提供了一种利用SpringBoot框架来解决这一问题的方法。这里我们将深入探讨如何通过注解和Redis缓存技术来防止表单的重复提交。 首先,SpringBoot是一个快速开发框架,...

    springboot和redis以及通用mapper等的一个结合框架.zip

    标题中的"springboot和redis以及通用mapper等的一个结合框架.zip"指的是一个整合了Spring Boot、Redis和通用Mapper的Java开发框架。这个框架旨在简化开发流程,提高开发效率,特别是对于处理数据存储和缓存操作的...

    分布式下,springboot一个注解防重复提交starter

    springboot一个注解防重复提交,实现原理是使用spring的aop功能,允许用户使用SPEL表达式设置防重key,支持自定义超时时间,结合redis实现分布式防重复

    Spring Boot如何防止重复提交

    为了解决这个问题,Spring Boot 提供了一些方法来防止重复提交。 首先,从数据库方面考虑,可以通过设置唯一索引来避免脏数据的产生。例如,在用户表中,可以设置用户名或邮箱为唯一索引,以防止重复提交相同的用户...

    Spring Boot使用AOP防止重复提交的方法示例

    Spring Boot 使用 AOP 防止重复提交的方法示例 在传统的 Web 项目中,防止重复提交的方法通常是:后端生成一个唯一的提交令牌(uuid),并存储在服务端。页面提交请求携带这个提交令牌,后端验证并在第一次验证后...

    avoidRepeatSubmit.rar

    总的来说,"avoidRepeatSubmit.rar"提供的组件结合了自定义注解、AOP拦截和Redis的setNX功能,为SpringBoot应用提供了一套完整的防重复提交解决方案。开发者可以参考其中的代码和示例,理解其工作原理,并将其应用于...

    seconds-kill:基于Springboot + Redis + Kafka的秒杀系统,乐观锁+缓存+限流+异步,TPS从500优化到3000

    如何设计一个秒杀系统系统的特点高效:秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键一致...前端答题或验证码,来分散用户的请求禁止重复提交:限定每个用户发起一次秒杀后,需等待才可以发起另一次请

    springboot-study:SpringBoot学习

    案例3:自定义注解+拦截器解决表单重复提交 博客地址: : 案例4:SpringBoot利用@Scheduled创建定时任务 博客地址: : 案例5:自定义注解+拦截器?秒防刷新 博客地址: : 案例6:发送文本邮件,html邮件,带附件...

    Java Springboot学习资料.rar

    SpringBoot配置详解 SpringBoot日志配置 SpringBoot整合Thymeleaf模板 使用JdbcTemplate访问数据库 ...重复提交(分布式锁) 重复提交(本地锁) WebSocket 安全框架(Shiro) 分布式限流 集成hadoop、hive、oozie

    springboot aspect通过 annotation进行拦截.docx

    在`RepeatSubmitAspect`的`around`方法中,我们可以实现具体的防止重复提交的逻辑,比如利用Redis存储请求信息并判断是否在指定时间内重复提交。这种设计模式在Spring Boot应用中非常常见,因为它提供了很好的灵活性...

Global site tag (gtag.js) - Google Analytics