一、为什么接口防重如此重要?
在当今的高并发业务场景中,接口被重复点击或短时间内多次提交请求,已成为一个常见但极具破坏性的隐患。想象一下这些场景:
1、电商系统:用户多次点击"提交订单"按钮,生成重复订单
2、支付系统:支付接口被重复触发,造成用户重复扣费
3、表单提交:因网络抖动导致表单重复提交,产生脏数据
4、票务系统:同一座位被多个用户重复预订,引发投诉纠纷
这些问题虽然看似小概率事件,但在真实生产环境中往往导致严重后果。防重复提交不是"锦上添花"的优化,而是"防止灾难"的必要保护。
二、传统防重方案的局限性
在深入我们的解决方案之前,先看看常见的防重方式及其优缺点:
三、前端防重:简单但不可靠
// 简单的前端防重
let isSubmitting = false;
function submitForm() {
if (isSubmitting) return;
isSubmitting = true;
// 提交逻辑
}
缺点:容易被绕过,无法防止恶意请求
四、Token标识:安全但复杂
每次请求生成唯一Token,校验后销毁。安全性高但依赖前端配合,增加系统复杂度。
五、请求特征哈希:我们的选择
通过请求路径、方法、参数生成唯一哈希值进行校验,无需前端依赖,纯后端实现防重。
六、双保险方案架构设计
我们的解决方案基于Spring Boot + AOP + Redis/Caffeine架构,具有以下特点:
1、无侵入性:通过注解方式实现,不污染业务代码
2、高性能:哈希计算耗时极短,几乎不影响接口性能
3、灵活性:支持本地缓存和分布式缓存两种模式
4、可配置:防重时间、关键字段均可灵活配置
七、系统架构流程
请求进入控制层
1、AOP拦截目标方法
2、提取URL、请求方法、参数信息
3、计算SHA-256哈希值作为Key
4、查询缓存是否存在该Key
5、存在则拒绝请求,不存在则执行方法并写入缓存
八、核心代码实现详解
1、自定义防重注解

这个注解支持灵活配置,可以根据不同接口的需求设置不同的防重时间。
2、AOP切面实现防重逻辑
@Aspect
@Component
@RequiredArgsConstructor
public class PreventDuplicateAspect {
private final HttpServletRequest request;
private final DuplicateStorageFactory storageFactory;
@Around("@annotation(preventDuplicate)")
public Object handle(ProceedingJoinPoint joinPoint,
PreventDuplicate preventDuplicate) throws Throwable {
String method = request.getMethod();
String uri = request.getRequestURI();
String params = RequestParameterUtils.getAllParamsAsString(joinPoint,
preventDuplicate.field());
// 拼接唯一签名源
String signSource = method + ":" + uri + ":" + params;
String key = DigestUtil.sha256Hex(signSource);
DuplicateStorage storage = storageFactory.getStorage();
if (storage.exists(key)) {
throw new RuntimeException(preventDuplicate.message());
}
storage.put(key, preventDuplicate.expire(), preventDuplicate.timeUnit());
return joinPoint.proceed();
}
}
2、缓存存储抽象与实现
我们设计了存储抽象层,支持Redis和Caffeine两种实现:
public interface DuplicateStorage {
boolean exists(String key);
void put(String key, int expire, TimeUnit timeUnit);
}
// Redis实现
@Component
@RequiredArgsConstructor
public class RedisStorage implements DuplicateStorage {
private final RedisTemplate<String, String> redisTemplate;
@Override
public boolean exists(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
@Override
public void put(String key, int expire, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, "1", expire, timeUnit);
}
}
// Caffeine本地缓存实现
@Component
public class CaffeineStorage implements DuplicateStorage {
private final Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS)
.maximumSize(10000)
.build();
@Override
public boolean exists(String key) {
return cache.getIfPresent(key) != null;
}
@Override
public void put(String key, int expire, TimeUnit timeUnit) {
cache.put(key, "1");
}
}
3、控制器使用示例
@RestController
@RequestMapping("/demo")
public class DemoController {
@GetMapping("/hello")
@PreventDuplicate
public String hello(String name, String age, String address) {
return "防重复测试:" + name + " " + age + " " + address;
}
@PostMapping("/saveUserInfo")
@PreventDuplicate(expire = 5)
public String saveUserInfo(@RequestBody UserInfo userInfo) {
return "请求时间:" + DateTime.now() + " 保存成功";
}
@PostMapping("/saveContent")
@PreventDuplicate(expire = 10, field = {"title", "content"})
public String saveContent(@RequestBody ArticleDTO articleDTO) {
return "请求时间:" + DateTime.now() + " 内容保存成功";
}
}
4、性能测试与优化
哈希计算性能验证,为了验证方案的可行性,我们进行了严格的性能测试:
5、测试环境:
a、生成3万字文章内容作为请求参数
b、模拟高并发场景下的重复请求
6、测试结果:
a、首次生成哈希值耗时约9ms(JVM预热阶段)
b、多次请求后平均耗时降至0ms
c、即使请求参数极大,对接口性能影响可以忽略不计
结论:SHA-256哈希算法在防重场景中既具备高度唯一性又拥有优异性能,完全满足高并发接口防重复需求。
九、缓存策略选择建议
根据业务场景选择合适的缓存方案:
1、单机应用:使用Caffeine本地缓存,性能最优
应用配置
duplicate:
storage-type: caffeine
2、分布式系统:使用Redis分布式缓存,保证集群环境一致性
应用配置
duplicate:
storage-type: redis
3、混合模式:支持根据配置动态切换
@Component
public class DuplicateStorageFactory {
@Value("${duplicate.storage-type}")
private String storageType;
public DuplicateStorage getStorage() {
if ("redis".equals(storageType)) {
return redisStorage;
} else {
return caffeineStorage;
}
}
}
十、生产环境实战指南
1、配置优化建议
application.yml 配置示例
duplicate:
storage-type: redis
redis:
# Redis连接配置
host: 127.0.0.1
port: 6379
timeout: 2000
caffeine:
# 本地缓存配置
expire-after-write: 3s
maximum-size: 10000
2、异常处理与日志记录
完善的异常处理和日志记录对生产环境至关重要:
@Slf4j
@Aspect
@Component
public class PreventDuplicateAspect {
@Around("@annotation(preventDuplicate)")
public Object handle(ProceedingJoinPoint joinPoint,
PreventDuplicate preventDuplicate) throws Throwable {
try {
// 防重逻辑
String key = generateKey(joinPoint);
if (storage.exists(key)) {
log.warn("检测到重复请求,key: {}, URI: {}", key, request.getRequestURI());
throw new BusinessException(preventDuplicate.message());
}
storage.put(key, preventDuplicate.expire(), preventDuplicate.timeUnit());
return joinPoint.proceed();
} catch (Exception e) {
log.error("防重处理异常", e);
// 异常时放行,避免防重功能影响正常业务
return joinPoint.proceed();
}
}
}
3、监控与告警
建立完善的监控体系,及时发现和处理问题:
@Component
public class DuplicateMetrics {
private final MeterRegistry meterRegistry;
private final Counter duplicateCounter;
public DuplicateMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.duplicateCounter = Counter.builder("duplicate.requests")
.description("重复请求计数")
.register(meterRegistry);
}
public void recordDuplicateRequest() {
duplicateCounter.increment();
}
}
4、高级特性与扩展方案
5、支持细粒度字段控制
通过field参数指定参与生成哈希的关键字段,实现更精细的防重控制:
@PostMapping("/updateUser")
@PreventDuplicate(field = {"userId", "updateTime"})
public String updateUser(@RequestBody User user) {
// 仅根据userId和updateTime判断是否重复
return "更新成功";
}
6、分布式环境下的增强方案
在集群环境下,可以结合分布式锁进一步提升安全性:
@Component
public class DistributedPreventDuplicateAspect {
private final RedissonClient redissonClient;
@Around("@annotation(preventDuplicate)")
public Object handleWithLock(ProceedingJoinPoint joinPoint,
PreventDuplicate preventDuplicate) throws Throwable {
String key = generateKey(joinPoint);
RLock lock = redissonClient.getLock("duplicate_lock:" + key);
try {
if (lock.tryLock(1, 3, TimeUnit.SECONDS)) {
// 获取锁成功,执行防重逻辑
return executePreventDuplicate(joinPoint, preventDuplicate, key);
} else {
throw new BusinessException("系统繁忙,请稍后重试");
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
7、防重时间动态调整
支持根据业务高峰期动态调整防重时间:
@PreventDuplicate(expire = "#{T(com.icoderoad.duplicate.util.TimeConfigUtil).getExpireTime()}")
public String dynamicExpireMethod() {
// 业务逻辑
}
8、实际应用场景案例
a、电商下单防重
@RestController
@RequestMapping("/order")
public class OrderController {
@PostMapping("/submit")
@PreventDuplicate(expire = 10, message = "订单提交过于频繁,请稍后重试")
public OrderResult submitOrder(@RequestBody OrderRequest request) {
// 下单业务逻辑
return orderService.submit(request);
}
}
b、支付接口防重
@RestController
@RequestMapping("/payment")
public class PaymentController {
@PostMapping("/pay")
@PreventDuplicate(expire = 30, field = {"orderId", "amount"})
public PaymentResult pay(@RequestBody PaymentRequest request) {
// 支付业务逻辑
return paymentService.pay(request);
}
}
c、表单提交防重

十一、总结与最佳实践
通过本文介绍的"哈希+缓存"双保险方案,我们实现了一个无侵入、通用性强、性能优异的防重复提交机制。总结一下核心要点:
核心优势
1、高性能:SHA-256哈希计算耗时极短,几乎不影响接口性能
2、灵活性:支持注解配置,可针对不同接口设置不同防重策略
3、可扩展:支持本地缓存和分布式缓存,适应不同部署环境
4、易用性:简单注解即可实现防重功能,降低使用门槛
实施建议
1、评估业务场景:根据业务特点合理设置防重时间
2、选择合适缓存:单机应用用Caffeine,分布式用Redis
3、建立监控体系:实时监控防重效果,及时调整策略
4、异常处理完善:确保防重功能不影响正常业务流程