Java重复数据案例如何拦截

wen java案例 28

本文目录导读:

Java重复数据案例如何拦截

  1. 接口幂等性拦截(高频请求)
  2. 数据库唯一索引拦截
  3. 基于数据库锁的防重复
  4. Redisson分布式锁(微服务场景)
  5. Guava本地缓存(单机场景)
  6. 综合方案推荐

我就从实际开发的角度,给你整理几种拦截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
唯一索引 数据唯一性 可靠,最终一致性 业务侵入
乐观锁 库存扣减 性能好 需要重试机制
分布式锁 微服务多实例 集群安全 性能损耗
本地缓存 单机防抖 速度快 不支持集群

实战建议

  1. 防重设计要尽早:在架构设计阶段就考虑,后面加比较困难
  2. 多种方案组合
    • 前端:按钮灰化+Loading
    • 业务层:Token+Redis
    • 持久层:唯一索引兜底
  3. 日志记录要详尽:记录谁、什么时间、请求了什么接口、参数是什么

需要的话,我可以给你一个完整的防重复提交的Demo项目代码。

抱歉,评论功能暂时关闭!