SpringBoot/Web项目防止表单/请求重复提交(单体和分布式)

SpringBoot/Web项目防止表单/请求重复提交(单体和分布式)

一、场景/方案

说起web项目的防止表单/请求重复提交,不得不说幂等性。

幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次。

1.1、常见场景:

	•	订单接口, 不能多次创建订单
	•	支付接口, 重复支付同一笔订单只能扣一次钱
	•	支付宝回调接口, 可能会多次回调, 必须处理重复回调
	•	普通表单提交接口, 因为网络超时,卡顿等原因多次点击提交, 只能成功一次等等

1.2、常见方案

解决思路:

  1. 从数据库方面考虑,数据设计的时候,如果有唯一性,考虑建立唯一索引。
  2. 从应用层面考虑,首先判断是单机服务还是分布式服务?
    • 单机服务:考虑一些缓存Cacha,利用缓存,来保证数据的重复提交。
    • 分布式服务,考虑将用户的信息,例如token和请求的url进行组装在一起形成令牌,存储到缓存中,例如redis,并设置超时时间为xx秒,如此来保证数据的唯一性。(利用了redis的分布式锁)

解决方案大致总结为:

	- 唯一索引 -- 防止新增脏数据
	- token机制 -- 防止页面重复提交,实现接口幂等性校验
	- 分布式锁 -- redis(jedis、redisson)或zookeeper实现
	- 悲观锁 -- 获取数据的时候加锁(锁表或锁行)
	- 乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据
	- 状态机 -- 状态变更, 更新数据时判断状态

前三种方式最为常见,本文则从应用层面考虑,给出单机服务还是分布式服务下的解决方案。

二、单体服务项目-防止重复提交

比如你的项目是一个单独springboot项目,SSM项目,或者其他的单体服务,就是打个jar或者war直接扔服务器上跑的。

采用【AOP解析自定义注解+google的Cache缓存机制】来解决表单/请求重复的提交问题。

思路:

  1. 建立自定义注解 @NoRepeatSubmit 标记所有Controller中的提交请求。
  2. 通过AOP机制对所有标记了@NoRepeatSubmit 的方法拦截。
  3. 在业务方法执行前,使用google的缓存Cache技术,来保证数据的重复提交。
  4. 业务方法执行后,释放缓存。

好了,接下里就是新建一个springboot项目,然后开整了。
在这里插入图片描述

2.1 pom.xml新增依赖

需要新增一个google.common.cache.Cache;

源码如下:

  <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>24.0-jre</version>
  </dependency>

2.2 新建NoRepeatSubmit.java自定义注解类

一个自定义注解接口类,是interface类哟 ,里面什么都不写,为了就是个重构。
源码如下:

package com.gitee.taven.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @title: NoRepeatSubmit
 * @Description:  自定义注解,用于标记Controller中的提交请求
 * @Author: ZouTao
 * @Date: 2020/4/14
 */
@Target(ElementType.METHOD)  // 作用到方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时有效
public @interface NoRepeatSubmit {
   

}

2.3 新建NoRepeatSubmitAop.java

这是个AOP的解析注解类,使用到了Cache缓存机制。
以cache.getIfPresent(key)的url值来进行if判断,如果不为空,证明已经发过请求,那么在规定时间内的再次请求,视为无效,为重复请求。如果为空,则正常响应请求。
源码如下:

package com.gitee.taven.aop;

import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.google.common.cache.Cache;

/**
 * @Description: aop解析注解-配合google的Cache缓存机制
 * @Author: Zoutao
 * @Date: 2020/4/14
 */
@Aspect
@Component
public class NoRepeatSubmitAop {
   

    private Log logger = LogFactory.getLog(getClass());

    @Autowired
    private Cache<String, Integer> cache;

    @Pointcut("@annotation(noRepeatSubmit)")
    public void pointCut(NoRepeatSubmit noRepeatSubmit) {
   
    }
    
    @Around("pointCut(noRepeatSubmit)")
    public Object arround(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) {
   
        try {
   
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            String sessionId = RequestContextHolder.getRequestAttributes().getSessionId();
            HttpServletRequest request = attributes.getRequest();
            String key = sessionId + "-" + request.getServletPath();
            if (cache.getIfPresent(key) == null) {
   // 如果缓存中有这个url视为重复提交
                Object o = pjp.proceed();
                cache.put(key, 0);
                return o;
            } else {
   
                logger.error("重复请求,请稍后在试试。");
                return null;
            }
        } catch (Throwable e) {
   
            e.printStackTrace();
            logger.error("验证重复提交时出现未知异常!");
            return "{\"code\":-889,\"message\":\"验证重复提交时出现未知异常!\"}";
        }
    }
}

2.4 新建缓存类UrlCache.java

用来获取缓存和设置有效期,目前设置有效期为2秒。
源码如下:

package com.gitee.taven;

import java.util.concurrent.TimeUnit;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

/**
 * @Description: 内存缓存配置类
 * @Author: Zoutao
 * @Date: 2020/4/14
 */

@Configuration
public class UrlCache {
   
    @Bean
    public Cache<String, Integer> getCache() {
   
        return CacheBuilder.newBuilder().expireAfterWrite(2L, TimeUnit.SECONDS).build();// 缓存有效期为2秒
    }
}

2.5 新建CacheTestController.java

一个请求控制类,用来模拟响应请求和业务处理。

源码如下:

package com.gitee.taven.controller;

import com.gitee.taven.ApiResult;
import com.gitee.taven.aop.NoRepeatSubmit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Description: 测试Cache方式的Controller
 * @Author: Zoutao
 * @Date: 2020/4/14
 */
@RestController
public class CacheTestController {
   

    private Object data;

    @RequestMapping("/TestSubmit")
    @NoRepeatSubmit()
    public Object test() {
   
        data = "程序逻辑返回,假设是一大堆DB来的数据。。。";
        return new ApiResult(200, "请求成功",data);
        // 也可以直接返回。return (",请求成功,程序逻辑返回");
    }
}

ps:这里可以在建立一个ApiResult.java类,来规范返回的数据格式体:
ApiResult.java(非必须)
源码如下:

package com.gitee.taven;

/** 
 * @title: ApiResult
 * @Description:  统一规范结果格式
 * @Param:   code, message, data
 * @return:  ApiResult
 * @Author: ZouTao 
 * @Date: 2020/4/14 
 */
public class ApiResult {
   
    private Integer code; //状态码
    
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江湖一点雨

原创不易,鼓励鼓励~~~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值