Java案例如何防止重复提交?

wen java案例 21

Java案例如何防止重复提交?从原理到实战的完整指南

目录导读

  1. 重复提交问题的本质与危害
  2. 前端防重:用户体验的“第一道防线”
  3. 后端防重:幂等性与Token机制详解
  4. 分布式场景下的终极方案:Redis+Token实战
  5. 问答环节:高频问题与避坑指南
  6. 总结与最佳实践

重复提交问题的本质与危害

在Web应用中,用户因网络延迟、误操作或恶意攻击,多次点击提交按钮导致同一请求被多次处理,这就是典型的“重复提交”问题。其本质是“非幂等操作被重复执行”

Java案例如何防止重复提交?

  • 订单重复创建
  • 数据库重复插入记录
  • 资金重复扣减

危害:轻则数据混乱,重则造成严重的经济损失或系统崩溃,曾有案例因未防重复提交,导致同一订单被生成3次,库存扣减3次,直接引起财务对账异常。


前端防重:用户体验的“第一道防线”

1 按钮置灰+遮罩层(最基础)

// 提交时立即禁用按钮,防止二次点击
document.getElementById('submitBtn').disabled = true;

局限:用户可通过浏览器刷新、禁用JS等绕过。

2 请求拦截器+加载动画(进阶)

使用Axios等库,在请求发送后显示loading,拦截后续相同请求:

let pending = false;
submitWithLock() {
  if (pending) return;
  pending = true;
  axios.post('/api/submit', data).finally(() => pending = false);
}

3 唯一标识符(UUID)预生成

前端提交时携带一个预生成的UUID,后端校验UUID是否已被使用。

前端防护只能防“君子”,无法防“恶意攻击”或“网络重传”,必须结合后端方案。


后端防重:幂等性与Token机制详解

1 幂等性设计:最高级解决方案

幂等性指一次或多次请求具有相同的副作用。

  • 数据库唯一索引订单号做唯一索引,重复插入直接报错。
  • 乐观锁(版本号):
    UPDATE goods SET stock=stock-1, version=version+1 WHERE id=1 AND version=2
    -- 若version不匹配,更新0行,即为重复请求
  • 状态机:订单状态从待付款已付款,若已为已付款则拒绝。

2 传统Token方案(同步场景)

流程

  1. 客户端请求/api/token获取一个唯一token。
  2. 服务端将token存入Session或数据库。
  3. 客户端提交请求时携带token。
  4. 服务端校验token存在则删除(原子操作),处理请求;若token不存在或已被删除,判定为重复提交。

代码示例(Spring Boot)

// 生成token
@RequestMapping("/getToken")
public String getToken(HttpSession session) {
    String token = UUID.randomUUID().toString();
    session.setAttribute("submitToken", token);
    return token;
}
// 校验token
@RequestMapping("/submit")
public Result submit(@RequestParam String token, HttpSession session) {
    String sessionToken = (String) session.getAttribute("submitToken");
    if (!token.equals(sessionToken)) {
        return Result.error("重复提交");
    }
    session.removeAttribute("submitToken"); // 删除token
    // 执行业务...
}

致命缺陷:Session无法在分布式集群中共享!必须改用Redis。


分布式场景下的终极方案:Redis+Token实战

1 核心思路:Redis原子性“存-删”

利用SETNXSETEX命令,将token作为key,设置过期时间

  • 存入SET token_key "value" NX EX 60(NX:不存在才存入;EX:60秒过期)
  • 删除/校验:使用Lua脚本保证原子性
    -- Lua脚本:校验并删除token
    if redis.call('get', KEYS[1]) == ARGV[1] then
      return redis.call('del', KEYS[1])
    else
      return 0
    end

2 完整Java实现(RedisTemplate)

@Service
public class TokenService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    // 预生成token
    public String generateToken(String userId) {
        String token = UUID.randomUUID().toString();
        String key = "submit:" + userId; // 每个用户独立key
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(key, token, 60, TimeUnit.SECONDS);
        if (Boolean.TRUE.equals(success)) {
            return token;
        }
        throw new RuntimeException("令牌生成失败");
    }
    // 校验token
    public boolean checkToken(String userId, String token) {
        String key = "submit:" + userId;
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "   return redis.call('del', KEYS[1]) " +
                        "else return 0 end";
        Long result = redisTemplate.execute(
                new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList(key), token);
        return result != null && result == 1L;
    }
}

3 关键优化点

  • Key设计:按用户维度隔离,如submit:userId:orderId,避免全局并发竞争。
  • 超时机制:防止token未使用而永远占用(设置合理的过期时间,如30秒)。
  • 二次校验:业务层再基于数据库唯一索引做二次拦截,稳如泰山。

问答环节:高频问题与避坑指南

Q1:为什么一定要用Redis?不能用数据库表吗?
A:数据库操作有并发写锁,高并发下性能瓶颈严重,Redis单线程模型+内存操作,原子性保证性能。
Q2:Token被劫持怎么办?
A:必须配合HTTPS加密传输,另外可增加签名验证(如HMAC),防止token被篡改。
Q3:如果业务处理时间超过token过期时间怎么办?
A:有两种方案:

  • 使用分布式锁替代Token,如Redisson的可重入锁,业务完成后释放。
  • 或增加Token续期机制,但复杂度升高。
    Q4:前后端都需要做防重吗?
    A:必须双重保障!前端提升用户体验,后端保证数据绝对安全。
    Q5:什么情况下不适合Token方案?
    A:当业务天然支持幂等性时(如“如果存在则更新”),无需token,Token多用于“只能执行一次”的场景。

总结与最佳实践

防止重复提交的黄金法则

  1. 前端兜底:按钮禁用+请求拦截,避免99%的误操作。
  2. 后端核心:幂等性设计(唯一索引/乐观锁) + Redis Token防重。
  3. 分布式扩展:高并发场景使用Lua脚本保证原子性。
  4. 监控与审计:记录重复请求日志,便于问题排查。

推荐组合方案
前端防点击 + 后端Redis Token + 数据库唯一索引,三者叠加可抵御99.99%的重复提交风险。

切记:没有100%完美的方案,但通过多层防御,足以在绝大多数场景下杜绝重复提交,实际项目请根据业务场景(同步/异步、单机/分布式)灵活选择,建议先从小范围验证开始。

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