最近由于系统中的接口需要调用外部接口,接口响应时间过长,前端的小伙提交后也没有做处理,导致用户同一个按钮多次点击,导致数据重复提交,针对这个问题,整理了下重复提交问题。
对于重复提交的问题,主要由于重复点击或者网络重发请求, 我要先了解产生原因几种方式:
对于重复提交的问题 主要涉及到时 幂等 问题,那么先说一下什么是幂等。
幂等:F(F(X)) = F(X)多次运算结果一致;简单点说就是对于完全相同的操作,操作一次与操作多次的结果是一样的。
在开发中,我们都会涉及到对数据库操作。例如:
序号 | 前后端 | 方案 | 优点 | 缺点 |
---|---|---|---|---|
1 | 前端 | 提交请求后,返回结果前提交按钮禁用 | 简单方便 | 只能前端控制,通过工具可以绕过,不安全 |
2 | 后端 | 提交后重定向到其他页面,防止用户F5和浏览器前进后退等重复提交问题 | 简单 方便 | 体验不好,适用部分场景,若是遇到网络问题 还会出现 |
3 | 后端 | 在表单、session、token 放入唯一标识符(如:UUID),每次操作时,保存标识一定时间后移除,保存期间有相同的标识就不处理或提示 | 相对简单 | 表单:有时需要前后端协商配合; session、token:加大服务性能开销 |
4 | 后端 | ConcurrentHashMap 、LRUMap 、google Cache 都是采用唯一标识(如:用户ID+请求路径+参数) | 相对简单 | 适用于单机部署的应用 |
5 | 后端 | redis 是线程安全的,可以实现redis分布式锁。设置唯一标识(如:用户ID+请求路径+参数)当做key ,value值可以随意(推荐设置成过期的时间点),在设置key的过期时间 | 单机、分布式、高并发都可以决绝 | 相对复杂需要部署维护redis |
java后台实现
这里通过redission实现
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface MesJRepeat{
/**
* 超时时间
*
* @return
*/
int lockTime();
/**
* redis 锁key的
*
* @return redis 锁key
*/
String lockKey() default "";
}
@Aspect
@Component
public class MesRepeatSubmitAspect extends BaseAspect {
@Resource
private RedissonLockClient redissonLockClient;
/***
* 定义controller切入点拦截规则,拦截JRepeat注解的业务方法
*/
@Pointcut("@annotation(jRepeat)")
public void pointCut(MesJRepeat jRepeat) {
}
/**
* AOP分布式锁拦截
*
* @param joinPoint
* @return
* @throws Exception
*/
@Around("pointCut(jRepeat)")
public Object repeatSubmit(ProceedingJoinPoint joinPoint, MesJRepeat jRepeat) throws Throwable {
String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
if (Objects.nonNull(jRepeat)) {
// 获取参数
Object[] args = joinPoint.getArgs();
// 进行一些参数的处理,比如获取订单号,操作人id等
StringBuffer lockKeyBuffer = new StringBuffer();
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
String reqestParams = getReqestParams(request, joinPoint);
String key =getValueBySpEL(jRepeat.lockKey(), parameterNames, args,"RepeatSubmit").get(0) + reqestParams;
// 公平加锁,lockTime后锁自动释放
boolean isLocked = false;
try {
isLocked = redissonLockClient.fairLock(key, TimeUnit.SECONDS, jRepeat.lockTime());
// 如果成功获取到锁就继续执行
if (isLocked) {
// 执行进程
return joinPoint.proceed();
} else {
// 未获取到锁
throw new ServiceException("请勿重复提交");
}
} finally {
// 如果锁还存在,在方法执行完成后,释放锁
if (isLocked) {
redissonLockClient.unlock(key);
}
}
}
return joinPoint.proceed();
}
private String getReqestParams(HttpServletRequest request, JoinPoint joinPoint) {
String httpMethod = request.getMethod();
String params = "";
if (CommonConstant.HTTP_POST.equals(httpMethod) || CommonConstant.HTTP_PUT.equals(httpMethod) || CommonConstant.HTTP_PATCH.equals(httpMethod)) {
Object[] paramsArray = joinPoint.getArgs();
Object[] arguments = new Object[paramsArray.length];
for (int i = 0; i < paramsArray.length; i++) {
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile) {
continue;
}
arguments[i] = paramsArray[i];
}
//update-begin-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
PropertyFilter profilter = new PropertyFilter() {
@Override
public boolean apply(Object o, String name, Object value) {
int length = 500;
if (value != null && value.toString().length() > length) {
return false;
}
return true;
}
};
params = JSONObject.toJSONString(arguments, profilter);
} else {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 请求的方法参数值
Object[] args = joinPoint.getArgs();
// 请求的方法参数名称
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paramNames = u.getParameterNames(method);
if (args != null && paramNames != null) {
for (int i = 0; i < args.length; i++) {
params += " " + paramNames[i] + ": " + args[i];
}
}
}
return params;
}
}
/**
* 分布式锁实现基于Redisson
*
* @date 2022-11-03
*/
@Slf4j
@Component
public class RedissonLockClient {
@Autowired
private RedissonClient redissonClient;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 获取锁
*/
public RLock getLock(String lockKey) {
return redissonClient.getLock(lockKey);
}
/**
* 加锁操作
*
* @return boolean
*/
public boolean tryLock(String lockName, long expireSeconds) {
return tryLock(lockName, 0, expireSeconds);
}
/**
* 加锁操作
*
* @return boolean
*/
public boolean tryLock(String lockName, long waitTime, long expireSeconds) {
RLock rLock = getLock(lockName);
boolean getLock = false;
try {
getLock = rLock.tryLock(waitTime, expireSeconds, TimeUnit.SECONDS);
if (getLock) {
log.info("获取锁成功,lockName={}", lockName);
} else {
log.info("获取锁失败,lockName={}", lockName);
}
} catch (InterruptedException e) {
log.error("获取式锁异常,lockName=" + lockName, e);
getLock = false;
}
return getLock;
}
public boolean fairLock(String lockKey, TimeUnit unit, int leaseTime) {
RLock fairLock = redissonClient.getFairLock(lockKey);
try {
boolean existKey = existKey(lockKey);
// 已经存在了,就直接返回
if (existKey) {
return false;
}
return fairLock.tryLock(3, leaseTime, unit);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
public boolean existKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 锁lockKey
*
* @param lockKey
* @return
*/
public RLock lock(String lockKey) {
RLock lock = getLock(lockKey);
lock.lock();
return lock;
}
/**
* 锁lockKey
*
* @param lockKey
* @param leaseTime
* @return
*/
public RLock lock(String lockKey, long leaseTime) {
RLock lock = getLock(lockKey);
lock.lock(leaseTime, TimeUnit.SECONDS);
return lock;
}
/**
* 解锁
*
* @param lockName 锁名称
*/
public void unlock(String lockName) {
try {
redissonClient.getLock(lockName).unlock();
} catch (Exception e) {
log.error("解锁异常,lockName=" + lockName, e);
}
}
}
@Slf4j
public class BaseAspect {
/**
* 通过spring SpEL 获取参数
*
* @param key 定义的key值 以#开头 例如:#user
* @param parameterNames 形参
* @param values 形参值
* @param keyConstant key的常亮
* @return
*/
public List<String> getValueBySpEL(String key, String[] parameterNames, Object[] values, String keyConstant) {
List<String> keys = new ArrayList<>();
if (!key.contains("#")) {
String s = "redis:lock:" + key + keyConstant;
log.debug("lockKey:" + s);
keys.add(s);
return keys;
}
//spel解析器
ExpressionParser parser = new SpelExpressionParser();
//spel上下文
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], values[i]);
}
Expression expression = parser.parseExpression(key);
Object value = expression.getValue(context);
if (value != null) {
if (value instanceof List) {
List value1 = (List) value;
for (Object o : value1) {
addKeys(keys, o, keyConstant);
}
} else if (value.getClass().isArray()) {
Object[] obj = (Object[]) value;
for (Object o : obj) {
addKeys(keys, o, keyConstant);
}
} else {
addKeys(keys, value, keyConstant);
}
}
log.info("表达式key={},value={}", key, keys);
return keys;
}
private void addKeys(List<String> keys, Object o, String keyConstant) {
keys.add("redis:lock:" + o.toString() + keyConstant);
}
}
测试接口:
@RestController
@RequestMapping("/hello")
public class HelloController {
@MesJRepeat(lockKey = "test", lockTime = 60)
@GetMapping("/test")
public Result<Integer> test(Long str) throws Exception {
// 业务代码
Thread.sleep(3000);
return Result.OK(str + " - 操作成功");
}
}