背景
进来开发了一个新增的接口,有开发伙伴反馈连续点击新增保存时,增加了两条数据
原因
正常的业务流程应该是点击 “新增保存” 按钮,等待返回成功,跳转查询列表,or 返回失败,当前页面提醒。问题原因如下:
1.后端没有做防重复提交
2.前端伙伴没有在点击新增保存按钮时loading等待返回结果。
解决方案
该问题前端做放重复提交or后端做防重复提交均可,因主要做后端,所以提供一个后端解决方案。
前端打开新增or修改页面时,生成一个唯一字符串(可调用后端接口生成or前端生成),然后请求新增or修改接口时,携带该参数,后端进行分布式锁竞争校验。
代码
No bb, show coding
代码地址:framework: 项目结构https://gitee.com/kwins/framework
注解定义(切面切入点标记,及 接口定制化参数的配置)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* 防重复提交
* @author kwin
* @Date 2022/1/16 14:59
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/**
* 锁过期时间
* @return
*/
int timeout() default 5;
/**
* 加锁等待时间
* @return
*/
int waittime() default 0;
/**
* 锁过期时间单位
* @return
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 锁的位置
*/
String location() default "RepeatSubmit";
/**
* 参数位置
*/
int argInx() default 0;
/**
* 参数名称
*/
String argName() default "";
}
上述分布式锁过期时间默认是5s,具体时间应该根据接口及业务具体分析。
参数位置:前端传入的唯一字符串在接口入参的位置,从0开始
参数名称:如果上述参数位置所对应的参数为对象,该参数为对象的field name。
切面(切入点:使用了RepeatSubmit 注解的方法)
import com.kwin.demo.server.framework.common.repeatsubmit.annotation.RepeatSubmit;
import com.kwin.demo.server.framework.common.lock.RedissLockUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.util.Date;
/**
* @author kwin
* @Date 2022/1/16 15:17
**/
@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {
private static final String REPEAT_LOCK_PREFIX = "repeatLock_";
/**
* 切点
* @param repeatSubmit
*/
@Pointcut("@annotation(repeatSubmit)")
public void repeatPoint(RepeatSubmit repeatSubmit) {
}
/**
*
* @return
*/
@Around(value = "repeatPoint(repeatSubmit)")
public Object doAround(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) {
String key = REPEAT_LOCK_PREFIX + repeatSubmit.location();
String argName = repeatSubmit.argName();
int argInx = repeatSubmit.argInx();
Object[] args = joinPoint.getArgs();
String suffix;
if(!StringUtils.hasText(argName)) {
suffix = String.valueOf(args[argInx]);
} else {
suffix = generateSuffix(args[argInx], argName);
}
key = key + ":" + suffix;
if(!RedissLockUtil.tryLock(key, repeatSubmit.timeUnit(), repeatSubmit.waittime(), repeatSubmit.timeout())) {
return "操作过于频繁,请稍后重试";
}
System.out.println(new Date());
try {
Object proceed = joinPoint.proceed();
return proceed;
} catch (Throwable throwable) {
log.error("", throwable);
throw new RuntimeException(throwable.getMessage());
} finally {
try {
RedissLockUtil.unlock(key);
} catch (Exception ex) {
log.error("解锁失败,key is {}", key, ex);
}
}
}
public String generateSuffix(Object obj, String argName) {
Class clzz = obj.getClass();
Field argField = null;
try {
argField = clzz.getDeclaredField(argName);
argField.setAccessible(Boolean.TRUE);
Object arg = argField.get(obj);
return String.valueOf(arg);
} catch (NoSuchFieldException e) {
log.error("no such field", e);
} catch (IllegalAccessException e) {
log.error("IllegalAccessException ", e);
}
return "SUFFIX";
}
}
这部分代码,首先通过注解的argInx获取到唯一字符串,or通过argInx和argName反射获取到唯一字符串。
然后对生成的key进行分布式锁竞争,竞争到锁执行业务惭怍,竞争失败则抛回提示“操作过于频繁,请稍后重试”。
关于分布式锁的实现借助redission,具体自己翻代码,或者参考自己项目实现。
验证
测试代码
import com.alibaba.fastjson.JSON;
import com.kwin.demo.api.dto.req.TestReq;
import com.kwin.demo.server.framework.common.repeatsubmit.annotation.RepeatSubmit;
import org.springframework.web.bind.annotation.*;
/**
* @author kwin
* @Date 2022/1/16 18:36
**/
@RestController
public class TestController {
@GetMapping("/test/{random}")
@RepeatSubmit(location = "test", timeout = 10)
public String test(@PathVariable("random") String random) throws InterruptedException {
Thread.sleep(5000);
return random;
}
}
同时传入相同random,调用该接口(这点需要注意:谷歌浏览器同时只能对同一个URL提出一个请求,如果有更多的请求的话,则会串行执行。如果请求阻塞,后续相同请求也会阻塞。),如下图:
测试代码(argInx 和argName同时使用)
import com.alibaba.fastjson.JSON;
import com.kwin.demo.api.dto.req.TestReq;
import com.kwin.demo.server.framework.common.repeatsubmit.annotation.RepeatSubmit;
import org.springframework.web.bind.annotation.*;
/**
* @author kwin
* @Date 2022/1/16 18:36
**/
@RestController
public class TestController {
@PostMapping("/test2")
@RepeatSubmit(location = "test2", timeout = 10, argName = "random")
public String test2(@RequestBody TestReq req) throws InterruptedException {
Thread.sleep(5000);
return JSON.toJSONString(req);
}
}
入参
import lombok.Data;
/**
* @author kwin
* @Date 2022/1/17 13:48
**/
@Data
public class TestReq {
private String test;
private String test2;
private String random;
}
结果如下:
具体可以根据自己的业务需求和项目需求进行一些更复杂的开发。