任何技术的解决方案都是有逻辑的,不可能凭空产生。

什么是重复提交。(张三买裤子这个场景)

张三下单买一条黑色的型号是AA的裤子,点击下单的时候,卡了一下,半天不出来,暴躁张三连续点击了10下,于是后台识别为:张三下单买一条黑色的型号是AA的裤子。张三下单买一条黑色的型号是AA的裤子。张三下单买一条黑色的型号是AA的裤子。..........

张三实际只要1条裤子,这就是重复提交。

如何识别重复提交

  • 用户账号:针对某个人重复提交。好处是:不会影响到其他用户。
  • 请求资源:针对某个资源限制重复提交。好处是:不影响该用户的其他操作。
  • 请求参数:通过请求参数来确定是重复提交。下单了一个黑色裤子,也可以下单一个红色裤子。

通过上述3个条件来判断是否是重复提交。比如:张三、下单、型号是AAA的黑色裤子。

这样不会影响到李四、下单、型号是AAA的黑色裤子。

解决方案

解决重复提交时机

  • 在请求入口限制:在OpenRestry搭配Lua实现。(这里是非常小众的写法,我就不上代码了)
  • 后端服务入口限制:在拦截器(过滤器也行)实现。

防止表单重复提交 实际实施问题

单体服务(借助caffeine缓存法)

单体服务不需要关注其他服务共享session问题。这里我放的是Coffine缓存代码,不想引入Redis,所以比较爽,直接暴力撸他。

引入caffeine依赖

        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>2.9.3</version> <!--For Java 11 or above, use 3.x otherwise use 2.x.-->
        </dependency>

定义一个caffeine交给Spring管理

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<String, Integer> initCaffeineNumberObj() {
        return Caffeine.newBuilder()
                .initialCapacity(100) // 设置初始化大小是1000
                .maximumSize(1_000) // 设置缓存最多存储1000个
                .expireAfterWrite(Duration.ofSeconds(10)) // 60秒后标记为可清除
                .build();
    }

}

AOP实现 BlockSubmitAspect

import com.github.benmanes.caffeine.cache.Cache;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

@Aspect
@Component
public class BlockSubmitAspect {

@Resource
private Cache<String, Integer> blockCommitDB;

@Pointcut("execution(public * com.zanglikun..*Controller.*(..))")
public void blockReCommit() {
}

@Before("blockReCommit()")
public void checkDuplicateSubmit(JoinPoint joinPoint) throws Exception {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String sessionId = request.getSession().getId(); // 获取Session ID
String requestURI = request.getRequestURI(); // 获取请求URI
requestURI = requestURI.split("\\?")[0]; // 避免get请求前端拼接?time=时间戳。
String key = sessionId + ":" + requestURI; // 生成缓存 Key
if (null != blockCommitDB.getIfPresent(key)) {
throw new RuntimeException("请勿重复提交表单!"));
} else {
blockCommitDB.put(key, 1); // 把key存一下。value不能为null,所以放了一个1
}
}

}

集群服务(借助Redis)

解决共享Session的方法,大多说都是选择Redis。所以解决重复提交的集群方案,我也采用中间件Redis来实现了。

必备的相关依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

定义相关注解 PreventDuplicateSubmit

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
    long timeout() default 10;
}

实现AOP PreventDuplicateSubmitAspect

@Aspect
@Component
public class PreventDuplicateSubmitAspect {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Around("@annotation(preventDuplicateSubmit)")
    public Object preventDuplicateSubmit(ProceedingJoinPoint joinPoint, PreventDuplicateSubmit preventDuplicateSubmit) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String sessionId = request.getSession().getId();
        String key = sessionId + ":" + request.getRequestURI();
        if (redisTemplate.hasKey(key)) {
            throw new RuntimeException("请勿重复提交");
        }
        redisTemplate.opsForValue().set(key, "1", preventDuplicateSubmit.timeout(), TimeUnit.SECONDS);
        return joinPoint.proceed();
    }
}

测试实例

@RestController
public class DemoController {
    @PreventDuplicateSubmit(timeout = 5)
    @PostMapping("/submit")
    public String submit() {
        // 处理表单提交逻辑
        return "提交成功";
    }
}
特殊说明:
上述文章均是作者实际操作后产出。烦请各位,请勿直接盗用!转载记得标注原文链接:www.zanglikun.com
第三方平台不会及时更新本文最新内容。如果发现本文资料不全,可访问本人的Java博客搜索:标题关键字。以获取全部资料 ❤