本文目录导读:

我就从实际开发的角度,给你整理几种拦截Java重复数据的常见案例和实现方案。
接口幂等性拦截(高频请求)
场景
用户快速点击提交按钮,导致同一请求多次提交
解决方案:基于Token+Redis
// 1. 生成Token
@PostMapping("/token")
public Result<String> getToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("idempotent:" + token, "1", 30, TimeUnit.MINUTES);
return Result.success(token);
}
// 2. 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String message() default "请勿重复提交";
}
// 3. AOP拦截
@Aspect
@Component
public class IdempotentAspect {
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
// 获取当前请求
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest();
String token = request.getHeader("token");
if (token == null) {
throw new BusinessException("请求未携带token");
}
// 判断token是否存在(存在则删除,实现一次有效)
Boolean success = redisTemplate.opsForValue()
.setIfAbsent("idempotent:" + token, "1", 1, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(success)) {
throw new BusinessException(idempotent.message());
}
return joinPoint.proceed();
}
}
数据库唯一索引拦截
场景
用户注册、订单号重复等问题
解决方案:唯一约束 + 优雅捕获
// 1. 数据库字段加唯一索引
ALTER TABLE `user` ADD UNIQUE INDEX `uk_phone` (`phone`);
// 2. 插入时捕获异常
@Service
public class UserService {
public Result<String> register(User user) {
try {
userMapper.insert(user);
return Result.success("注册成功");
} catch (DuplicateKeyException e) {
// 捕获数据库唯一索引异常
return Result.error("该手机号已注册");
}
}
}
基于数据库锁的防重复
场景
抢红包、优惠券领取等并发场景
解决方案:数据库乐观锁
// 1. 数据库增加版本号字段
ALTER TABLE `coupon` ADD COLUMN `version` INT DEFAULT 0;
// 2. 更新时检查版本号
@Service
public class CouponService {
public boolean grabCoupon(Long userId, Long couponId) {
// 1. 查询是否已领取(业务层面判断)
CouponRecord record = couponRecordMapper.selectByUserIdAndCouponId(userId, couponId);
if (record != null) {
return false; // 已领取
}
// 2. 更新库存(乐观锁)
int count = couponMapper.updateCouponStock(couponId);
if (count <= 0) {
return false; // 库存不足或并发失败
}
// 3. 插入领取记录(唯一索引兜底)
try {
couponRecordMapper.insert(new CouponRecord(userId, couponId));
} catch (DuplicateKeyException e) {
// 库存回滚
couponMapper.rollbackStock(couponId);
return false;
}
return true;
}
}
// Mapper层
@Mapper
public interface CouponMapper {
@Update("UPDATE coupon SET stock = stock - 1, version = version + 1 " +
"WHERE id = #{couponId} AND stock > 0")
int updateCouponStock(Long couponId);
}
Redisson分布式锁(微服务场景)
场景
多个服务实例同时处理同一订单
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
public String createOrder(OrderDTO orderDTO) {
// 构建锁key(业务唯一标识)
String lockKey = "order:lock:" + orderDTO.getUserId() + ":" + orderDTO.getProductId();
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁(等待5秒,持有锁30秒)
boolean isLock = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!isLock) {
throw new BusinessException("系统繁忙,请稍后再试");
}
// 二次检查是否已创建(防止重复创建)
Order order = orderMapper.selectByOrderNo(orderDTO.getOrderNo());
if (order != null) {
return "订单已存在";
}
// 执行业务逻辑
return doCreateOrder(orderDTO);
} catch (Exception e) {
throw new BusinessException("创建订单失败");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
Guava本地缓存(单机场景)
场景
防止短时间内重复请求
@Component
public class RepeatRequestInterceptor extends HandlerInterceptorAdapter {
// 创建本地缓存(10分钟过期,最大1000条)
private Cache<String, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (handler instanceof HandlerMethod) {
// 生成唯一标识(url + 参数 + 用户ID)
String key = generateKey(request);
String value = cache.getIfPresent(key);
if (value != null) {
throw new BusinessException("请求过于频繁,请稍后再试");
}
cache.put(key, "1");
}
return true;
}
private String generateKey(HttpServletRequest request) {
// 结合请求参数生成唯一标识
String uri = request.getRequestURI();
String params = request.getQueryString();
String userId = request.getHeader("userId");
return MD5.encode(uri + ":" + params + ":" + userId);
}
}
综合方案推荐
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Redis+Token | 表单提交 | 实现简单,用户体验好 | 依赖Redis |
| 唯一索引 | 数据唯一性 | 可靠,最终一致性 | 业务侵入 |
| 乐观锁 | 库存扣减 | 性能好 | 需要重试机制 |
| 分布式锁 | 微服务多实例 | 集群安全 | 性能损耗 |
| 本地缓存 | 单机防抖 | 速度快 | 不支持集群 |
实战建议
- 防重设计要尽早:在架构设计阶段就考虑,后面加比较困难
- 多种方案组合:
- 前端:按钮灰化+Loading
- 业务层:Token+Redis
- 持久层:唯一索引兜底
- 日志记录要详尽:记录谁、什么时间、请求了什么接口、参数是什么
需要的话,我可以给你一个完整的防重复提交的Demo项目代码。