Java接口防重案例

wen java案例 1

Java接口防重案例:从基础到高并发的完整实战指南

目录导读

  1. 为什么接口需要防重?——业务痛点与场景剖析
  2. 防重设计的核心原则与常见误区
  3. 基于数据库唯一索引的防重(最稳妥)
  4. 基于Redis的分布式锁防重(高性能首选)
  5. Token令牌机制(适用于表单提交)
  6. 状态机与幂等表(适用于复杂业务流)
  7. 高并发场景下的防重优化策略
  8. 常见问题问答(FAQ)
  9. 总结与最佳实践建议

为什么接口需要防重?——业务痛点与场景剖析

在真实的Java后端开发中,接口重复请求是导致数据错乱、重复扣款、库存超卖等严重问题的“头号元凶”。

Java接口防重案例

  • 电商下单:用户快速点击“提交订单”按钮两次,可能生成两个相同订单,导致重复扣款。
  • 支付回调:第三方支付平台因网络超时多次通知同一笔支付结果,若不做防重,用户余额会被重复增加。
  • 表单提交:前端未做按钮禁用,用户狂点提交,后台写入多条相同数据。

这些场景的共同点是:同一业务请求在极短时间内被客户端或上游系统发送多次,防重设计的本质是保证接口的幂等性(即无论调用多少次,业务结果一致)。


防重设计的核心原则与常见误区

核心原则

  1. 唯一性标识:每个业务请求必须有全局唯一的业务ID(如订单号、流水号、token)。
  2. 先检查后操作:在业务逻辑执行前,先验证该ID是否已处理过。
  3. 原子性:检查与状态标记必须原子化执行,否则并发下依然会穿透。

常见误区

  • ❌ 只用前端按钮置灰:用户可绕过前端直接发HTTP请求。
  • ❌ 用同步锁(synchronized):只能锁单机进程,无法解决分布式部署问题。
  • ❌ 依赖数据库查询后再插入:先查后插非原子,并发下仍会重复。

方案一:基于数据库唯一索引的防重(最稳妥)

适用场景:对性能要求不高,但必须绝对可靠的数据写入操作。

实现方式

在数据表的关键字段上创建唯一索引(如订单号、业务流水号),当重复请求尝试插入相同记录时,数据库会抛出DuplicateKeyException,程序捕获该异常即可优雅处理。

示例代码

public void createOrder(OrderDTO orderDTO) {
    try {
        orderMapper.insert(orderDTO);
    } catch (DuplicateKeyException e) {
        log.warn("重复订单请求,订单号:{}", orderDTO.getOrderNo());
        throw new BizException("请勿重复提交");
    }
}

优缺点

  • ✅ 实现简单,强一致,天然支持分布式。
  • ❌ 每次插入都要触发数据库索引维护,高并发小规模下存在性能瓶颈。
  • ❌ 无法防住“先查后改”类业务(如更新库存)。

方案二:基于Redis的分布式锁防重(高性能首选)

适用场景:高并发、低延迟场景(如秒杀、抢红包)。

实现方式

使用Redis的SETNX命令(或Redisson的tryLock)实现分布式锁,以业务ID作为key,设置过期时间,如果SETNX返回true,则执行业务;否则直接拒绝。

核心代码

public boolean tryLock(String bizKey, long expireTime) {
    String lockKey = "lock:" + bizKey;
    return redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "locked", Duration.ofSeconds(expireTime));
}
// 使用示例
if (tryLock("order:" + orderNo, 30)) {
    try {
        // 执行业务逻辑
    } finally {
        redisTemplate.delete("order:" + orderNo); // 释放锁
    }
} else {
    throw new BizException("请求处理中,请勿重复提交");
}

注意事项

  • 锁必须设置过期时间:防止业务异常导致死锁。
  • 释放锁前验证持有者:使用Lua脚本保证原子性,避免误删他人锁。
  • 分布式锁的粒度:应以业务ID(如订单号)为维度,而不是锁住整张表。

方案三:Token令牌机制(适用于表单提交)

适用场景:用户交互型页面,如提交报名、发布文章。

实现方式

  1. 前端请求后端获取一个一次性token(存入Redis,key为token,value为用户ID)。
  2. 提交表单时必须携带该token。
  3. 后端收到请求后,原子删除Redis中的token(使用DEL命令),删除成功则继续执行,否则拒绝。

核心代码

// 获取token
public String getToken(String userId) {
    String token = UUID.randomUUID().toString();
    redisTemplate.opsForValue().set("form:" + token, userId, Duration.ofMinutes(10));
    return token;
}
// 校验并消费token
public boolean checkAndConsume(String token) {
    String key = "form:" + token;
    Boolean success = redisTemplate.delete(key);
    return Boolean.TRUE.equals(success); // true表示首次使用,false表示重复提交
}

优点

  • ✅ 实现简单,无需处理数据库锁。
  • ✅ 天然防重复,因为token一次性失效。

缺点

  • ❌ 需要前后端配合,前端必须获取token。
  • ❌ 若token泄露,可能被恶意利用(需增加用户绑定校验)。

方案四:状态机与幂等表(适用于复杂业务流)

适用场景:订单状态流转、支付退款等需要跟踪“当前进度”的复杂业务。

实现方式

创建一张idempotent_record表,记录业务ID、请求时间、处理状态(0=处理中,1=完成),业务执行前插入一条状态为0的记录,执行成功后更新状态为1,重复请求发现存在状态为0或1的记录时直接返回。

关键表结构

CREATE TABLE idempotent_record (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    biz_id VARCHAR(64) NOT NULL,
    status TINYINT NOT NULL DEFAULT 0 COMMENT '0处理中,1完成',
    create_time TIMESTAMP,
    UNIQUE KEY uk_biz_id (biz_id)
);

状态机流转

  • 插入记录(状态0)→ 执行业务 → 更新状态1。
  • 若业务失败,可删除记录或标记为失败,允许重试(需额外字段)。

对比优点

  • ✅ 可感知业务执行结果(成功/失败)。
  • ✅ 天然支持异步重试(重试前检查状态)。

高并发场景下的防重优化策略

  1. 双写一致性优化:使用Redis SETNX作为第一道防线,数据库唯一索引作为兜底,先快速拦截,再防穿透。
  2. 批量请求防重:对批量提交接口,将整个请求体进行MD5哈希后作为防重key,注意排除时间戳等随机字段。
  3. 防重与缓存结合:对于读多写少场景,可用Redis缓存处理结果,重复请求直接返回缓存中的结果,减少业务计算。
  4. 流量削峰:接入消息队列(如RabbitMQ)将请求异步化,由消费端通过幂等表去重。
  5. 监控与告警:对防重拦截次数进行Prometheus监控,及时发现恶意攻击或异常流量。

常见问题问答(FAQ)

Q1:数据库唯一索引和Redis锁,我该怎么选?

  • A:如果业务是纯插入(如用户注册、日志记录),优先选唯一索引——零运维成本,如果业务是“读-改-写”(如扣库存),必须用Redis锁或状态机,因为唯一索引无法防止并发下的更新覆盖。

Q2:Redis锁过期了但业务还没执行完怎么办?

  • A:这是经典的“锁过期”问题,解决方案有:①使用Redisson的看门狗自动续期;②业务结束时主动删除锁,并设置合理过期时间为最长执行时间的1.5倍。

Q3:防重成功是返回“成功”还是“失败”?

  • A:需区分场景,如果重复请求是用户误提交,应返回“重复提交”错误提示,但如果是支付回调重试,则应返回“成功”(幂等设计),避免上游无限重试。

Q4:防重和幂等是同一个概念吗?

  • A:不完全相同,防重是“防止第二次请求执行”,幂等是“无论执行多少次,结果一致”,防重是实现幂等的一种手段,采用Token防重后,第二次请求被拒绝,而真正幂等的接口会返回第一次请求的结果。

Q5:如何测试防重逻辑的正确性?

  • A:使用JMeter模拟100个并发请求携带相同业务ID,断言数据库只产生一条数据;同时测试Redis锁的过期时间是否触发;最后做故障演练,模拟Redis宕机时数据库唯一索引能否兜底。

总结与最佳实践建议

防重没有“银弹”,需要根据业务场景权衡,以下是我在实际项目中总结的选型参考:

业务类型 推荐方案 理由
低并发简单插入 数据库唯一索引 简单可靠,无需额外组件
高并发秒杀/扣库存 Redis分布式锁 + 数据库乐观锁 高性能且保证数据一致
表单提交 Token令牌 用户感知良好,拦截率100%
订单状态流转 幂等表 + 状态机 可追踪执行状态,支持重试

最佳实践口诀

  • 前端必防:按钮禁用+Token隐藏域,减少无效请求。
  • 后端兜底:Redis快拦截,数据库慢兜底(最终一致性)。
  • 日志记录:每次防重拦截都记录请求方、业务ID、原因,方便排查。
  • 过期清理:对于幂等表或Redis Key,设置TTL,避免无限制堆积。

请记住:防重设计要提前融入系统架构,而不是事后打补丁,一个优秀的防重方案,能让你在“用户狂点”、“上游重试”、“恶意刷接口”三座大山下,依然保持数据坚如磐石。

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