当前位置: 代码网 > it编程>编程语言>Java > Java中防止重复提交的八种解决方案(最后一种很优雅)

Java中防止重复提交的八种解决方案(最后一种很优雅)

2026年03月24日 Java 我要评论
在web开发中,防止重复提交是一个常见且重要的需求。本文将详细介绍java中防止重复提交的8种解决方案,并分析各自的优缺点。1. 什么是重复提交?为什么要防止?1.1 重复提交的定义重复提交是指用户在

在web开发中,防止重复提交是一个常见且重要的需求。本文将详细介绍java中防止重复提交的8种解决方案,并分析各自的优缺点。

1. 什么是重复提交?为什么要防止?

1.1 重复提交的定义

重复提交是指用户在短时间内对同一业务请求进行多次提交的行为。常见场景包括:

  • 网络延迟:用户点击提交后页面无响应,多次点击

  • 误操作:用户双击提交按钮

  • 恶意请求:攻击者故意重复提交

1.2 重复提交的危害

  • 数据不一致:创建重复订单、重复扣款等

  • 系统资源浪费:增加数据库和服务器压力

  • 业务逻辑错误:影响统计数据和业务流程

2. 前端解决方案

2.1 按钮禁用(最基础)

// 提交后禁用按钮
function submitform() {
    const submitbtn = document.getelementbyid('submitbtn');
    submitbtn.disabled = true;
    // 执行提交逻辑
    document.forms[0].submit();
}

2.2 加载状态提示

// 显示加载状态
function submitform() {
    const submitbtn = document.getelementbyid('submitbtn');
    submitbtn.innerhtml = '<i class="loading"></i> 提交中...';
    submitbtn.disabled = true;
}

前端方案的局限性:无法防止恶意请求和浏览器刷新重复提交。

3. 后端解决方案

3.1 同步锁(不推荐)

public class orderservice {
    private final object lock = new object();
    
    public result createorder(orderdto orderdto) {
        synchronized(lock) {
            // 业务逻辑
            return processorder(orderdto);
        }
    }
}

缺点:集群环境下无效,性能差。

3.2 数据库唯一索引

-- 为订单号添加唯一索引
alter table orders add unique index uk_order_no (order_no);

-- 或者为业务关键字段添加联合唯一索引
alter table orders add unique index uk_business_key (user_id, product_id, create_date);

优点:最可靠的防重方案
缺点:数据库压力大,不友好的错误提示

3.3 数据库乐观锁

@mapper
public interface ordermapper {
    // 通过版本号控制
    @update("update orders set status = #{status}, version = version + 1 " +
            "where id = #{id} and version = #{version}")
    int updatewithversion(order order);
}

@service
@transactional
public class orderservice {
    public result createorder(orderdto orderdto) {
        // 1. 查询当前版本号
        order order = ordermapper.selectbyid(orderdto.getid());
        
        // 2. 业务处理...
        
        // 3. 更新时校验版本号
        int count = ordermapper.updatewithversion(order);
        if (count == 0) {
            throw new runtimeexception("订单已处理,请勿重复提交");
        }
        return result.success();
    }
}

4. token令牌方案(推荐)

4.1 实现原理

  1. 页面加载时向后端请求token

  2. 提交时携带token

  3. 后端校验token并删除

4.2 具体实现

token生成工具类

@component
public class tokenutil {
    
    private static final string token_prefix = "submit_token:";
    
    @autowired
    private redistemplate<string, string> redistemplate;
    
    /**
     * 生成token
     */
    public string generatetoken(string key) {
        string token = uuid.randomuuid().tostring();
        string rediskey = token_prefix + key + ":" + token;
        redistemplate.opsforvalue().set(rediskey, "1", duration.ofminutes(5));
        return token;
    }
    
    /**
     * 验证token
     */
    public boolean validatetoken(string key, string token) {
        string rediskey = token_prefix + key + ":" + token;
        boolean result = redistemplate.delete(rediskey);
        return boolean.true.equals(result);
    }
}

controller层实现

@restcontroller
public class ordercontroller {
    
    @autowired
    private tokenutil tokenutil;
    
    /**
     * 获取提交token
     */
    @getmapping("/token")
    public result<string> gettoken() {
        string token = tokenutil.generatetoken("order");
        return result.success(token);
    }
    
    /**
     * 提交订单
     */
    @postmapping("/order")
    public result createorder(@requestbody orderdto orderdto, 
                             @requestheader("x-submit-token") string token) {
        // 验证token
        if (!tokenutil.validatetoken("order", token)) {
            return result.fail("请勿重复提交");
        }
        
        // 业务逻辑
        return orderservice.createorder(orderdto);
    }
}

前端调用

// 获取token
async function gettoken() {
    const response = await fetch('/token');
    const result = await response.json();
    return result.data;
}

// 提交订单
async function submitorder(orderdata) {
    const token = await gettoken();
    
    const response = await fetch('/order', {
        method: 'post',
        headers: {
            'content-type': 'application/json',
            'x-submit-token': token
        },
        body: json.stringify(orderdata)
    });
    
    return response.json();
}

5. 基于aop的防重注解(优雅方案)

5.1 自定义防重提交注解

@target(elementtype.method)
@retention(retentionpolicy.runtime)
public @interface preventduplicatesubmit {
    /**
     * 防重key,支持spel表达式
     */
    string key() default "";
    
    /**
     * 过期时间(秒)
     */
    int expire() default 5;
    
    /**
     * 提示消息
     */
    string message() default "请勿重复提交";
}

5.2 aop切面实现

@aspect
@component
public class preventduplicatesubmitaspect {
    
    @autowired
    private redistemplate<string, object> redistemplate;
    
    @autowired
    private httpservletrequest request;
    
    private static final string lock_prefix = "submit_lock:";
    
    @around("@annotation(preventduplicatesubmit)")
    public object around(proceedingjoinpoint joinpoint, 
                        preventduplicatesubmit preventduplicatesubmit) throws throwable {
        
        string lockkey = generatelockkey(joinpoint, preventduplicatesubmit);
        
        // 尝试获取锁
        boolean success = redistemplate.opsforvalue()
                .setifabsent(lockkey, "1", duration.ofseconds(preventduplicatesubmit.expire()));
        
        if (boolean.true.equals(success)) {
            try {
                // 获取锁成功,执行方法
                return joinpoint.proceed();
            } finally {
                // 删除锁(可选,等自动过期也行)
                // redistemplate.delete(lockkey);
            }
        } else {
            // 获取锁失败,重复提交
            throw new runtimeexception(preventduplicatesubmit.message());
        }
    }
    
    /**
     * 生成锁的key
     */
    private string generatelockkey(proceedingjoinpoint joinpoint, 
                                 preventduplicatesubmit preventduplicatesubmit) {
        string key = preventduplicatesubmit.key();
        
        if (stringutils.hastext(key)) {
            // 解析spel表达式
            return lock_prefix + parsespel(key, joinpoint);
        } else {
            // 默认生成方式:方法名 + 参数
            methodsignature signature = (methodsignature) joinpoint.getsignature();
            string methodname = signature.getmethod().getname();
            string args = arrays.tostring(joinpoint.getargs());
            string useragent = request.getheader("user-agent");
            
            return lock_prefix + methodname + ":" + 
                   digestutils.md5digestashex((args + useragent).getbytes());
        }
    }
    
    /**
     * 解析spel表达式
     */
    private string parsespel(string expression, proceedingjoinpoint joinpoint) {
        methodsignature signature = (methodsignature) joinpoint.getsignature();
        evaluationcontext context = new standardevaluationcontext();
        
        // 设置参数
        string[] parameternames = signature.getparameternames();
        object[] args = joinpoint.getargs();
        
        for (int i = 0; i < parameternames.length; i++) {
            context.setvariable(parameternames[i], args[i]);
        }
        
        expressionparser parser = new spelexpressionparser();
        return parser.parseexpression(expression).getvalue(context, string.class);
    }
}

5.3 使用示例

@restcontroller
public class ordercontroller {
    
    @preventduplicatesubmit(key = "#orderdto.userid + ':' + #orderdto.productid", 
                           expire = 10, 
                           message = "订单正在处理中,请勿重复提交")
    @postmapping("/order")
    public result createorder(@requestbody orderdto orderdto) {
        // 业务逻辑
        return orderservice.createorder(orderdto);
    }
    
    @preventduplicatesubmit(expire = 30)
    @postmapping("/payment")
    public result payment(@requestparam string orderno) {
        // 支付逻辑
        return paymentservice.processpayment(orderno);
    }
}

6. 分布式锁方案

6.1 基于redis的分布式锁

@component
public class redisdistributedlock {
    
    @autowired
    private redistemplate<string, string> redistemplate;
    
    private static final string lock_prefix = "global_lock:";
    
    /**
     * 尝试获取锁
     */
    public boolean trylock(string key, long expireseconds) {
        string lockkey = lock_prefix + key;
        string value = uuid.randomuuid().tostring();
        
        boolean result = redistemplate.opsforvalue()
                .setifabsent(lockkey, value, duration.ofseconds(expireseconds));
        
        return boolean.true.equals(result);
    }
    
    /**
     * 释放锁
     */
    public void unlock(string key) {
        string lockkey = lock_prefix + key;
        redistemplate.delete(lockkey);
    }
}

6.2 使用redisson分布式锁

@component
public class redissonlockservice {
    
    @autowired
    private redissonclient redissonclient;
    
    public <t> t executewithlock(string lockkey, long waittime, long leasetime, 
                                supplier<t> supplier) {
        rlock lock = redissonclient.getlock(lockkey);
        
        try {
            // 尝试获取锁
            boolean locked = lock.trylock(waittime, leasetime, timeunit.seconds);
            if (locked) {
                return supplier.get();
            } else {
                throw new runtimeexception("系统繁忙,请稍后重试");
            }
        } catch (interruptedexception e) {
            thread.currentthread().interrupt();
            throw new runtimeexception("获取锁失败", e);
        } finally {
            if (lock.isheldbycurrentthread()) {
                lock.unlock();
            }
        }
    }
}

// 使用示例
@service
public class orderservice {
    
    @autowired
    private redissonlockservice lockservice;
    
    public result createorder(orderdto orderdto) {
        string lockkey = "order_submit:" + orderdto.getuserid();
        
        return lockservice.executewithlock(lockkey, 3, 10, () -> {
            // 业务逻辑
            return processorder(orderdto);
        });
    }
}

7. 本地限流器

7.1 guava ratelimiter

@component
public class ratelimitservice {
    
    private final map<string, ratelimiter> limitermap = new concurrenthashmap<>();
    
    /**
     * 尝试获取令牌
     */
    public boolean tryacquire(string key, int permitspersecond) {
        ratelimiter limiter = limitermap.computeifabsent(key, 
            k -> ratelimiter.create(permitspersecond));
        return limiter.tryacquire();
    }
}

// 使用示例
@restcontroller
public class apicontroller {
    
    @autowired
    private ratelimitservice ratelimitservice;
    
    @postmapping("/api/submit")
    public result submitdata(@requestbody requestdata data) {
        string clientid = getclientid(); // 获取客户端标识
        
        if (!ratelimitservice.tryacquire(clientid, 5)) {
            return result.fail("请求过于频繁,请稍后重试");
        }
        
        // 处理业务
        return processdata(data);
    }
}

8. 综合方案:注解 + 分布式锁 + 限流

@target(elementtype.method)
@retention(retentionpolicy.runtime)
public @interface submitprotection {
    /** 防重提交key */
    string key() default "";
    /** 锁过期时间 */
    int lockexpire() default 10;
    /** 限流配置:每秒允许的请求数 */
    double ratelimit() default 1.0;
    /** 提示消息 */
    string message() default "请求过于频繁,请稍后重试";
}

@aspect
@component
public class submitprotectionaspect {
    
    @autowired
    private redissonclient redissonclient;
    
    private final map<string, ratelimiter> ratelimitermap = new concurrenthashmap<>();
    
    @around("@annotation(protection)")
    public object around(proceedingjoinpoint joinpoint, submitprotection protection) throws throwable {
        string protectionkey = generateprotectionkey(joinpoint, protection);
        
        // 1. 限流检查
        if (protection.ratelimit() > 0) {
            ratelimiter limiter = ratelimitermap.computeifabsent(
                protectionkey, k -> ratelimiter.create(protection.ratelimit()));
            if (!limiter.tryacquire()) {
                throw new runtimeexception(protection.message());
            }
        }
        
        // 2. 分布式锁防重
        rlock lock = redissonclient.getlock("submit_protection:" + protectionkey);
        try {
            if (lock.trylock(0, protection.lockexpire(), timeunit.seconds)) {
                return joinpoint.proceed();
            } else {
                throw new runtimeexception("请求正在处理中,请勿重复提交");
            }
        } finally {
            if (lock.isheldbycurrentthread()) {
                lock.unlock();
            }
        }
    }
    
    private string generateprotectionkey(proceedingjoinpoint joinpoint, submitprotection protection) {
        // 生成key的逻辑,参考前面的aop方案
        return "default_key";
    }
}

9. 方案对比总结

方案适用场景优点缺点
前端控制普通表单提交实现简单,用户体验好安全性低,可绕过
同步锁单机简单业务实现简单集群无效,性能差
数据库唯一索引数据强一致性要求可靠性最高数据库压力大
乐观锁并发更新场景性能较好实现复杂,需要版本字段
token令牌web表单提交安全性好,实现简单需要前后端配合
aop注解需要灵活控制的业务无侵入,使用方便学习成本稍高
分布式锁分布式系统集群有效,可靠性高依赖redis等中间件
限流器高频请求场景防止恶意请求可能误伤正常用户

10. 最佳实践建议

  1. 分层防护:前端 + 后端多重防护

  2. 合理超时:根据业务设置合理的锁超时时间

  3. 友好提示:给用户明确的重复提交提示

  4. 监控告警:对频繁的重复提交进行监控

  5. 性能考虑:避免防重逻辑影响正常业务流程

结语

防止重复提交是保证系统数据一致性的重要手段。在实际项目中,建议根据具体业务场景选择合适的方案,或者组合多种方案实现更完善的防护。aop注解方案因其灵活性和无侵入性,是目前比较推荐的做法。

希望本文对您有所帮助!如有疑问,欢迎在评论区讨论。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com