您好,登录后才能下订单哦!
密码登录
登录注册
点击 登录注册 即表示同意《亿速云用户服务条款》
# SpringBoot 2.x 中怎么避免重复提交
## 前言
在Web应用开发中,重复提交是一个常见但危害严重的问题。当用户因网络延迟、误操作或恶意行为导致同一请求被多次提交时,可能引发数据重复插入、订单重复创建、资源重复扣减等业务异常。SpringBoot 2.x作为当前主流的Java Web开发框架,提供了多种解决方案来应对这一挑战。本文将深入探讨6种实用方案,涵盖从基础到高级的完整防御体系。
## 一、重复提交的典型场景与危害
### 1.1 常见触发场景
- **网络延迟**:用户点击提交后未及时响应导致重复点击
- **页面回退**:提交成功后按浏览器返回按钮再次提交
- **恶意攻击**:使用工具自动化重复提交关键业务请求
- **程序缺陷**:前端未做防重复点击控制
### 1.2 业务危害分析
- **数据一致性破坏**:重复生成相同业务数据
- **资源异常消耗**:多次扣减库存/余额
- **系统性能下降**:无效请求占用服务器资源
- **用户体验受损**:重复提示或异常页面
## 二、前端基础防护方案
### 2.1 按钮状态控制(基础防御)
```javascript
// Vue示例
<template>
<button @click="submit" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
</template>
<script>
export default {
data() {
return {
isSubmitting: false
}
},
methods: {
async submit() {
if(this.isSubmitting) return;
this.isSubmitting = true;
try {
await axios.post('/api/submit', formData);
} finally {
this.isSubmitting = false;
}
}
}
}
</script>
// Axios全局配置
const pendingRequests = new Map();
axios.interceptors.request.use(config => {
const requestKey = `${config.method}-${config.url}`;
if (pendingRequests.has(requestKey)) {
return Promise.reject(new Error('重复请求已阻止'));
}
pendingRequests.set(requestKey, true);
return config;
});
axios.interceptors.response.use(response => {
const requestKey = `${response.config.method}-${response.config.url}`;
pendingRequests.delete(requestKey);
return response;
});
@RestController
public class TokenController {
@GetMapping("/token")
public String getToken(HttpSession session) {
String token = UUID.randomUUID().toString();
session.setAttribute("submit_token", token);
return token;
}
}
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if ("POST".equalsIgnoreCase(request.getMethod())) {
String clientToken = request.getParameter("submit_token");
String serverToken = (String) request.getSession()
.getAttribute("submit_token");
if (serverToken == null || !serverToken.equals(clientToken)) {
response.sendError(HttpStatus.BAD_REQUEST.value(),
"无效或重复的提交令牌");
return false;
}
// 验证成功后立即清除
request.getSession().removeAttribute("submit_token");
}
return true;
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenInterceptor())
.addPathPatterns("/submit");
}
}
@RestController
public class OrderController {
@Autowired
private RedissonClient redissonClient;
@PostMapping("/createOrder")
public String createOrder(@RequestBody Order order,
HttpServletRequest request) {
String userId = getCurrentUserId();
String lockKey = "order:submit:" + userId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,等待3秒,锁定10秒自动释放
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
return processOrder(order);
} else {
throw new RuntimeException("操作太频繁,请稍后重试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("系统繁忙");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
String key() default "";
int expire() default 5; // 秒
}
@Aspect
@Component
public class DuplicateSubmitAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(prevent)")
public Object checkDuplicate(ProceedingJoinPoint pjp,
PreventDuplicateSubmit prevent) throws Throwable {
String requestKey = buildRequestKey(pjp, prevent);
if (redisTemplate.opsForValue().setIfAbsent(requestKey, "1")) {
redisTemplate.expire(requestKey, prevent.expire(), TimeUnit.SECONDS);
return pjp.proceed();
} else {
throw new RuntimeException("请勿重复提交");
}
}
private String buildRequestKey(ProceedingJoinPoint pjp,
PreventDuplicateSubmit prevent) {
// 构建基于用户+方法参数的唯一key
return "submit:lock:" +
(prevent.key().isEmpty() ?
pjp.getSignature().toLongString() : prevent.key());
}
}
CREATE TABLE `idempotent_record` (
`id` bigint NOT NULL AUTO_INCREMENT,
`business_type` varchar(32) NOT NULL COMMENT '业务类型',
`business_key` varchar(64) NOT NULL COMMENT '业务唯一键',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_type_key` (`business_type`,`business_key`)
) ENGINE=InnoDB COMMENT='幂等记录表';
@Service
@Transactional
public class OrderService {
@Autowired
private IdempotentRecordRepository recordRepository;
public Order createOrder(OrderDTO dto) {
// 检查幂等记录
if (recordRepository.existsByBusinessTypeAndBusinessKey(
"order_create", dto.getOrderNo())) {
throw new BusinessException("订单已存在");
}
// 业务处理
Order order = convertToOrder(dto);
orderRepository.save(order);
// 记录幂等
IdempotentRecord record = new IdempotentRecord();
record.setBusinessType("order_create");
record.setBusinessKey(dto.getOrderNo());
recordRepository.save(record);
return order;
}
}
public class RequestFingerprintUtil {
public static String generate(HttpServletRequest request) {
StringJoiner joiner = new StringJoiner("|");
// 基础信息
joiner.add(request.getMethod())
.add(request.getRequestURI());
// 用户标识
String userId = getCurrentUserId();
if (userId != null) {
joiner.add(userId);
}
// 参数指纹
Map<String, String[]> params = request.getParameterMap();
params.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> {
String value = Arrays.stream(e.getValue())
.sorted()
.collect(Collectors.joining(","));
joiner.add(e.getKey() + "=" + value);
});
// 请求体指纹(需缓存读取)
String cachedBody = (String) request.getAttribute("cachedRequestBody");
if (cachedBody != null) {
joiner.add("body=" + DigestUtils.md5Hex(cachedBody));
}
return DigestUtils.md5Hex(joiner.toString());
}
}
@Configuration
public class CacheConfig {
@Bean
public Cache<String, Boolean> requestFingerprintCache() {
return CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}
}
@RestControllerAdvice
public class DuplicateSubmitAdvice {
@Autowired
private Cache<String, Boolean> fingerprintCache;
@ModelAttribute
public void checkFingerprint(HttpServletRequest request) {
if ("POST".equalsIgnoreCase(request.getMethod())) {
String fingerprint = RequestFingerprintUtil.generate(request);
if (fingerprintCache.getIfPresent(fingerprint) != null) {
throw new DuplicateSubmitException();
}
fingerprintCache.put(fingerprint, true);
}
}
}
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
前端控制 | 简单表单提交 | 实现简单 | 可绕过,防护弱 |
Token机制 | 常规Web应用 | 可靠性高,用户体验好 | 需前后端配合 |
分布式锁 | 高并发分布式系统 | 强一致性保证 | 性能开销较大 |
数据库唯一约束 | 核心交易场景 | 绝对可靠 | 增加数据库压力 |
请求指纹 | RESTful API接口 | 灵活精准 | 实现复杂度高 |
选型建议: 1. 对于管理后台等系统:Token机制+前端控制 2. 电商交易系统:分布式锁+数据库唯一约束 3. 开放API平台:请求指纹+限流机制
在SpringBoot 2.x项目中构建完整的防重复提交体系,需要根据实际业务场景组合多种方案。建议从以下维度进行设计: 1. 防御层次:前端防护+服务端校验+持久层保障 2. 业务关键性:非核心业务可采用轻量级方案,核心业务需强校验 3. 系统架构:单体应用与分布式系统选择不同技术方案
通过合理的方案选择和组合实现,可以有效避免重复提交带来的各类业务问题,提升系统健壮性和用户体验。 “`
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。