任何技术的解决方案都是有逻辑的,不可能凭空产生。
什么是重复提交。(张三买裤子这个场景)
张三下单买一条黑色的型号是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博客搜索:标题关键字。以获取最新全部资料 ❤
第三方平台不会及时更新本文最新内容。如果发现本文资料不全,可访问本人的Java博客搜索:标题关键字。以获取最新全部资料 ❤