基于Redis和Spring AOP实现防重复提交机制
在Web应用程序开发中,防止用户重复提交表单是一个常见的需求。本文将详细介绍如何使用Redis分布式锁和Spring AOP来实现一个优雅的防重复提交解决方案。
基于Redis和Spring AOP实现防重复提交机制
引言
在Web应用程序开发中,防止用户重复提交表单是一个常见的需求。重复提交可能会导致数据重复、资源浪费,甚至引发业务异常。本文将详细介绍如何使用Redis分布式锁和Spring AOP来实现一个优雅的防重复提交解决方案。
整体架构
该解决方案包含以下核心组件:
- 自定义注解
@NoRepeatSubmit
:用于标记需要防重复提交的方法 - Redis分布式锁配置:使用Redisson客户端实现
- 切面处理类:实现防重复提交的核心逻辑
详细实现
1. 自定义注解
首先创建一个注解,用于标记需要防重复提交的方法:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
/**
* 锁持续时间(秒)
*/
int seconds() default 30;
/**
* 特定的锁标识位置
*/
String location() default "";
/**
* 锁标识参数位置
*/
int paramIndex() default 0;
/**
* 锁标识参数名称(优先于paramIndex)
*/
String paramName() default "";
}
2. Redis配置类
配置Redisson客户端:
@Configuration
public class RedissonConfiguration {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.database}")
private int database;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer();
singleServerConfig.setAddress(String.format("redis://%s:%s", host, port))
.setDatabase(database)
.setTimeout(timeout);
if (!StringUtils.isEmpty(password)) {
singleServerConfig.setPassword(password);
}
return Redisson.create(config);
}
}
3. 分布式锁组件
创建Redis分布式锁的管理组件:
@Component
public class RedisLockComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockComponent.class);
private static final String LOCK_FLAG = ":LOCK";
private final RedissonClient redissonClient;
public RedisLockComponent(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
/**
* 获取锁
* @param lockName 锁名称
* @param expireTime 过期时间
* @param timeUnit 时间单位
* @return 是否获取成功
*/
public boolean getLock(String lockName, int expireTime, TimeUnit timeUnit) {
try {
RLock lock = redissonClient.getLock(lockName + LOCK_FLAG);
return lock.tryLock(3, expireTime, timeUnit);
} catch (Exception e) {
LOGGER.warn("线程:[{}]获取锁:[{}]失败", Thread.currentThread().getName(), lockName, e);
return false;
}
}
/**
* 释放锁
* @param lockName 锁名称
*/
public void releaseLock(String lockName) {
try {
RLock lock = redissonClient.getLock(lockName + LOCK_FLAG);
if (lock.tryLock()) {
lock.unlock();
}
} catch (Exception e) {
LOGGER.warn("线程:[{}]释放锁:[{}]失败", Thread.currentThread().getName(), lockName, e);
}
}
}
4. 切面实现
实现防重复提交的核心切面:
@Aspect
@Component
public class NoRepeatSubmitAspect {
private static final String REPEAT_LOCK_PREFIX = "RepeatLock";
private final RedisLockComponent redisLockComponent;
public NoRepeatSubmitAspect(RedisLockComponent redisLockComponent) {
this.redisLockComponent = redisLockComponent;
}
@Pointcut("@annotation(noRepeatSubmit)")
public void noRepeatSubmitPoint(NoRepeatSubmit noRepeatSubmit) {}
@Around(value = "noRepeatSubmitPoint(noRepeatSubmit)",
argNames = "joinPoint,noRepeatSubmit")
public Object handleRepeatSubmit(ProceedingJoinPoint joinPoint,
NoRepeatSubmit noRepeatSubmit) throws Throwable {
String key = generateLockKey(joinPoint, noRepeatSubmit);
int seconds = noRepeatSubmit.seconds();
if (!redisLockComponent.getLock(key, seconds, TimeUnit.SECONDS)) {
return Result.error("请勿重复提交");
}
try {
return joinPoint.proceed();
} finally {
redisLockComponent.releaseLock(key);
}
}
private String generateLockKey(JoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) {
String urlPath = noRepeatSubmit.location();
if (StringUtils.isBlank(urlPath)) {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
urlPath = request.getRequestURI();
}
// 获取方法参数
Map<String, Object> paramMap = new HashMap<>();
Object[] argValues = joinPoint.getArgs();
String[] argNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();
for (int i = 0; i < argNames.length; i++) {
paramMap.put(argNames[i], argValues[i]);
}
// 获取锁的唯一标识
String paramValue;
String paramName = noRepeatSubmit.paramName();
if (StringUtils.isNotBlank(paramName)) {
paramValue = String.valueOf(paramMap.get(paramName));
} else {
paramValue = String.valueOf(argValues[noRepeatSubmit.paramIndex()]);
}
// 组装锁的key
String userId = getCurrentUserId(); // 获取当前用户ID的方法
return String.format("%s:%s:%s:%s",
REPEAT_LOCK_PREFIX, userId, urlPath.replaceAll("/", "_"), paramValue);
}
}
5. 统一返回结果类
@Data
@AllArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> error(String message) {
return new Result<>(500, message, null);
}
}
使用示例
@RestController
@RequestMapping("/order")
public class OrderController {
@PostMapping("/submit")
@NoRepeatSubmit(seconds = 30, paramName = "orderId")
public Result<String> submitOrder(@RequestBody OrderRequest request) {
// 订单处理逻辑
return Result.success("订单提交成功");
}
}
工作原理
- 当标注了
@NoRepeatSubmit
的方法被调用时,切面会拦截该调用 - 根据以下信息生成唯一的锁键:
- 用户ID
- 请求路径
- 指定的参数值
- 尝试获取Redis分布式锁
- 如果获取锁成功:
- 执行原方法
- 方法执行完成后释放锁
- 如果获取锁失败:
- 返回重复提交的错误信息
方案优势
- 分布式支持:可以在集群环境下正常工作
- 灵活配置:可自定义锁定时间和锁标识生成规则
- 代码解耦:通过AOP实现,对业务代码无侵入
- 性能高效:基于Redis的分布式锁实现,性能好
- 使用简单:只需添加注解即可使用
注意事项
- 合理设置锁的过期时间,避免锁时间过长影响用户体验
- 需要考虑Redis的可用性,建议添加Redis集群
- 重复提交的判断依据要根据业务场景合理设置
- 建议添加相应的监控指标,及时发现潜在问题
总结
通过Redis分布式锁和Spring AOP的结合,我们实现了一个简单而强大的防重复提交解决方案。这种实现方式具有良好的扩展性和维护性,可以根据实际业务需求进行定制化改造。
在实际应用中,还可以考虑添加以下功能:
- 自定义重复提交的判断逻辑
- 添加白名单机制
- 实现更细粒度的锁控制
- 添加更多的监控指标