在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 实现原理
页面加载时向后端请求token
提交时携带token
后端校验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. 最佳实践建议
分层防护:前端 + 后端多重防护
合理超时:根据业务设置合理的锁超时时间
友好提示:给用户明确的重复提交提示
监控告警:对频繁的重复提交进行监控
性能考虑:避免防重逻辑影响正常业务流程
结语
防止重复提交是保证系统数据一致性的重要手段。在实际项目中,建议根据具体业务场景选择合适的方案,或者组合多种方案实现更完善的防护。aop注解方案因其灵活性和无侵入性,是目前比较推荐的做法。
希望本文对您有所帮助!如有疑问,欢迎在评论区讨论。
发表评论