SpringBoot接口+Redis解决用户重复提交问题
Posted sum墨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot接口+Redis解决用户重复提交问题相关的知识,希望对你有一定的参考价值。
前言
1. 为什么会出现用户重复提交
- 网络延迟的情况下用户多次点击submit按钮导致表单重复提交;
- 用户提交表单后,点击【刷新】按钮导致表单重复提交(点击浏览器的刷新按钮,就是把浏览器上次做的事情再做一次,因为这样也会导致表单重复提交);
- 用户提交表单后,点击浏览器的【后退】按钮回退到表单页面后进行再次提交。
2. 重复提交不拦截可能导致的问题
- 重复数据入库,造成脏数据。即使数据库表有UK索引,该操作也会增加系统的不必要负担;
- 会成为黑客爆破攻击的入口,大量的请求会导致应用崩溃;
- 用户体验差,多条重复的数据还需要一条条的删除等。
3. 解决办法
办法有很多,我这里只说一种,利用Redis的set方法搞定(不是redisson)
项目代码
项目结构
配置文件
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>RequestLock</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>RequestLock</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 切面 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
spring.application.name=RequestLock
server.port=8080
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=20
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000
代码文件
RequestLockApplication.java
package com.example.requestlock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RequestLockApplication
public static void main(String[] args)
SpringApplication.run(RequestLockApplication.class, args);
User.java
package com.example.requestlock.model;
import com.example.requestlock.lock.annotation.RequestKeyParam;
public class User
private String name;
private Integer age;
@RequestKeyParam(name = "phone")
private String phone;
public String getName()
return name;
public void setName(String name)
this.name = name;
public Integer getAge()
return age;
public void setAge(Integer age)
this.age = age;
public String getPhone()
return phone;
public void setPhone(String phone)
this.phone = phone;
@Override
public String toString()
return "User" +
"name='" + name + '\\'' +
", age=" + age +
", phone='" + phone + '\\'' +
'';
RequestKeyParam.java
package com.example.requestlock.lock.annotation;
import java.lang.annotation.*;
/**
* @description 加上这个注解可以将参数也设置为key,唯一key来源
*/
@Target(ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestKeyParam
/**
* key值名称
*
* @return 默认为空
*/
String name() default "";
RequestLock.java
package com.example.requestlock.lock.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @description 请求防抖锁,用于防止前端重复提交导致的错误
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestLock
/**
* redis锁前缀
*
* @return 默认为空,但不可为空
*/
String prefix() default "";
/**
* redis锁过期时间
*
* @return 默认2秒
*/
int expire() default 2;
/**
* redis锁过期时间单位
*
* @return 默认单位为秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* redis key分隔符
*
* @return 分隔符
*/
String delimiter() default ":";
RequestLockMethodAspect.java
package com.example.requestlock.lock.aspect;
import com.example.requestlock.lock.annotation.RequestLock;
import com.example.requestlock.lock.keygenerator.RequestKeyGenerator;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
/**
* @description 请求锁切面处理器
*/
@Aspect
@Configuration
public class RequestLockMethodAspect
private final StringRedisTemplate stringRedisTemplate;
private final RequestKeyGenerator requestKeyGenerator;
@Autowired
public RequestLockMethodAspect(StringRedisTemplate stringRedisTemplate, RequestKeyGenerator requestKeyGenerator)
this.requestKeyGenerator = requestKeyGenerator;
this.stringRedisTemplate = stringRedisTemplate;
@Around("execution(public * * (..)) && @annotation(com.example.requestlock.lock.annotation.RequestLock)")
public Object interceptor(ProceedingJoinPoint joinPoint)
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
RequestLock requestLock = method.getAnnotation(RequestLock.class);
if (StringUtils.isEmpty(requestLock.prefix()))
// throw new RuntimeException("重复提交前缀不能为空");
return "重复提交前缀不能为空";
//获取自定义key
final String lockKey = requestKeyGenerator.getLockKey(joinPoint);
final Boolean success = stringRedisTemplate.execute(
(RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), new byte[0], Expiration.from(requestLock.expire(), requestLock.timeUnit())
, RedisStringCommands.SetOption.SET_IF_ABSENT));
if (!success)
// throw new RuntimeException("您的操作太快了,请稍后重试");
return "您的操作太快了,请稍后重试";
try
return joinPoint.proceed();
catch (Throwable throwable)
// throw new RuntimeException("系统异常");
return "系统异常";
RequestKeyGenerator.java
package com.example.requestlock.lock.keygenerator;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 加锁key生成器
*/
public interface RequestKeyGenerator
/**
* 获取AOP参数,生成指定缓存Key
*
* @param joinPoint 切入点
* @return 返回key值
*/
String getLockKey(ProceedingJoinPoint joinPoint);
RequestKeyGeneratorImpl.java
package com.example.requestlock.lock.keygenerator.impl;
import com.example.requestlock.lock.annotation.RequestKeyParam;
import com.example.requestlock.lock.annotation.RequestLock;
import com.example.requestlock.lock.keygenerator.RequestKeyGenerator;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
@Service
public class RequestKeyGeneratorImpl implements RequestKeyGenerator
@Override
public String getLockKey(ProceedingJoinPoint joinPoint)
//获取连接点的方法签名对象
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//Method对象
Method method = methodSignature.getMethod();
//获取Method对象上的注解对象
RequestLock requestLock = method.getAnnotation(RequestLock.class);
//获取方法参数
final Object[] args = joinPoint.getArgs();
//获取Method对象上所有的注解
final Parameter[] parameters = method.getParameters();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parameters.length; i++)
final RequestKeyParam cacheParams = parameters[i].getAnnotation(RequestKeyParam.class);
//如果属性不是CacheParam注解,则不处理
if (cacheParams == null)
continue;
//如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam
sb.append(requestLock.delimiter()).append(args[i]);
//如果方法上没有加CacheParam注解
if (StringUtils.isEmpty(sb.toString()))
//获取方法上的多个注解(为什么是两层数组:因为第二层数组是只有一个元素的数组)
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
//循环注解
for (int i = 0; i < parameterAnnotations.length; i++)
final Object object = args[i];
//获取注解类中所有的属性字段
final Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields)
//判断字段上是否有CacheParam注解
final RequestKeyParam annotation = field.getAnnotation(RequestKeyParam.class);
//如果没有,跳过
if (annotation == null)
continue;
//如果有,设置Accessible为true(为true时可以使用反射访问私有变量,否则不能访问私有变量)
field.setAccessible(true);
//如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam
sb.append(requestLock.delimiter()).append(ReflectionUtils.getField(field, object));
//返回指定前缀的key
return requestLock.prefix() + sb;
UserController.java
package com.example.requestlock.controller;
import com.example.requestlock.lock.annotation.RequestLock;
import com.example.requestlock.model.User;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(以上是关于SpringBoot接口+Redis解决用户重复提交问题的主要内容,如果未能解决你的问题,请参考以下文章