利用Redis中SETNX命令特性实现防止重复提交(防抖动机制)的策略

第一章:防止重复提交的需求

  • 1.1 背景介绍

        在现代Web应用中,用户与服务器的交互频繁,表单提交是最常见的操作之一。然而,由于用户的操作习惯或网络延迟等原因,有时用户可能会无意中多次点击提交按钮,或者使用一些自动化工具进行频繁提交。这种行为可能导致数据的不一致性、后端服务的压力增大,甚至可能被恶意利用,造成系统资源的浪费或数据的不准确。

例如,在以下场景中,重复提交问题尤为突出:

  • 表单提交:用户填写完一个表单后,由于网络延迟或其他原因,可能会多次点击“提交”按钮。
  • 投票系统:在在线投票或评分系统中,用户可能尝试通过多次提交来增加自己的票数或评分。
  • 订单系统:在电子商务平台中,用户可能尝试多次提交订单,以期望获得更好的交易条件。
  • 1.2 解决方案概述

        为了防止重复提交,可以采用多种策略,其中一种有效的方法是使用Redis的SETNX命令。SETNX是“SET if Not eXists”的缩写,它用于在Redis中设置键值对,但只有在键不存在时才设置成功。这个特性使得SETNX成为防止重复提交的理想选择。

基本思路如下:

生成唯一键:当用户提交表单时,后端生成一个唯一的键,这个键可以基于用户的身份标识(如用户ID)、会话ID、表单的唯一标识符等信息生成。

使用SETNX设置键值对:使用SETNX命令尝试在Redis中设置这个唯一键。如果键已经存在,说明用户在限制时间内已经提交过表单,因此拒绝此次提交。

设置过期时间:为了防止键值对占用Redis空间,可以为键设置一个过期时间,过期后键会自动被删除。

提交表单:如果SETNX命令执行成功,说明表单可以被提交;如果失败,则拒绝提交并抛出异常。

第二章:Redis和Jwt

第三章:创建防止重复提交的注解

  • 3.1 @RepeatSubmit注解定义

  • /**
     * 防止重复提交注解
     *
     * @author WangWenXin
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RepeatSubmit {
    
        /**
         * 内部枚举,后续如果有其他类型再加
         */
        enum Type {
            IP, USER_ID
        }
    
        /**
         * key前缀
         */
        String keyPrefix() default "repeat_submit:";
    
        /**
         * 时间(秒)
         */
        int time() default 3;
    
        /**
         * 限制类型
         */
        Type type() default Type.IP;
    }
  • 3.2 使用@RepeatSubmit注解

        在需要的方法上直接使用该注解就行,不使用注解默认参数可以重新赋值

@RepeatSubmit(type = RepeatSubmit.Type.USER_ID, time = 5)
@GetMapping("/test")
public String test() throws InterruptedException {
    System.out.println("请求正在执行中");
    // 模拟业务执行时间
    Thread.sleep(3000);
    System.out.println("请求完成");
    return "订单号:"+new java.util.Random().nextInt();
}

第四章:AOP切面实现

想要了解Spring AOP 可以看这篇贴子 Spring AOP入门:为初学者准备的指南-CSDN博客

  • 4.1 创建切面

  • /**
     * 自动注入用户信息切面
     *
     * @author WangWenXin
     */
    @Aspect
    @Component
    public class RepeatSubmitAspect {
    
        @Autowired
        private RedisUtils redisUtils;
    
        @Autowired
        private TokenService tokenService;
    
        @Around("@annotation(repeatSubmit)")
        private Object aroundAdvice(ProceedingJoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
            final Object[] args = point.getArgs();
    
            final String key = getKey(point, repeatSubmit);
            final boolean ifAbsent = redisUtils.setIfAbsent(key, "", repeatSubmit.time(), TimeUnit.SECONDS);
            if (!ifAbsent)
                throw new RuntimeException("请勿重复提交");
            try {
                return point.proceed(args);
            } catch (Throwable e) {
                throw new RuntimeException(e);
            } finally {
                redisUtils.delete(key);
            }
        }
    
    
        /**
         * 生成重复提交key
         *
         * @param point        JoinPoint
         * @param repeatSubmit RepeatSubmit
         * @return key
         */
        private String getKey(ProceedingJoinPoint point, RepeatSubmit repeatSubmit) {
            // 获取方法声明所在的类
            Object target = point.getTarget();
            Class<?> targetClass = target.getClass();
            // 获取签名
            MethodSignature signature = (MethodSignature) point.getSignature();
            // 获取方法名称
            String methodName = signature.getName();
            // 方法完整路径
            String fullPath = targetClass.getName() + "." + methodName;
    
            final StringBuilder stringBuffer = new StringBuilder(repeatSubmit.keyPrefix());
    
            if (repeatSubmit.type() == RepeatSubmit.Type.IP) {
                // 获取请求的ip地址
                HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                String ip = HttpRequestUtils.getIpAddress(request);
                // 拼接key
                stringBuffer.append(ip).append(":");
            }
            if (repeatSubmit.type() == RepeatSubmit.Type.USER_ID) {
                final Long userId = getUserId();
                // 拼接key
                stringBuffer.append(userId).append(":");
            }
    
            // 拼接方法名
            stringBuffer.append(fullPath);
    
            return stringBuffer.toString();
        }
    
        /**
         * 从token中解析userId
         *
         * @return userId
         */
        private Long getUserId() {
            try {
                return tokenService.getUserId();
            } catch (Exception e) {
                throw new RuntimeException("无法获取用户ID", e);
            }
        }
    
    }

  • 4.2 环绕通知逻辑

环绕通知aroundAdvice的逻辑

        使用setnx存储key,不存在该键,则存储成功就放行请求执行对应的逻辑。如果已经存在该键,则抛出异常,Spring Data Redis中封装的setIfAbsent就是Redis的SETNX命令

生成key策略

        由前缀repeat_submit:和访问的方法全部路径名,以及用户Id或者用户的IP地址组成

示例

repeat_submit:org.example.repeatsubmit.controller.test:123456(127.0.0.1)

第六章:测试和验证

  • 6.1 测试方法

    •         使用Thread.sleep()方法模拟业务执行的时间,在这期间再发起第二次请求,查看是否拦截第二次请求。
  • 6.2 验证结果

    •         这里可以看到上次请求没结束的时候,下次请求过来会直接抛出异常。这里只限制请求,如果要是秒杀订单指定秒杀某个商品一次可以在业务中查询数据库是否已经创建该商品的订单。

总结

        通过结合使用Redis的SETNX命令和AOP,我们可以有效地防止用户在一定时间内重复提交表单。这种方法不仅提高了应用程序的安全性,而且通过AOP的使用,还保持了代码的整洁和可维护性。

使用Redis可以很好地实现防止重复提交的功能。具体实现方法如下: 1.在用户提交操作前,先在Redis查询该操作是否已经被提交过,如果已经提交过,则直接返回重复提交的提示信息。 2.如果该操作没有被提交过,则将该操作的唯一标识(如用户ID、操作类型等)作为key,将当前时间戳作为value,存入Redis,并设置过期时间,过期时间可以根据实际情况设置,一般为操作的有效时间。 3.在用户提交操作后,再次查询Redis是否存在该操作的唯一标识,如果存在,则表示该操作已经被提交过,直接返回重复提交的提示信息。 4.如果不存在,则表示该操作是第一次提交,可以进行后续的业务处理。 下面是一个使用Python Redis实现防止重复提交的示例代码: ```python import redis # 连接Redis r = redis.Redis(host='localhost', port=6379, db=0) # 判断操作是否已经提交过 def is_submitted(key): return r.get(key) is not None # 将操作标识存入Redis def set_submitted(key, value, expire_time): r.set(key, value, ex=expire_time) # 示例:防止用户重复提交订单 def submit_order(user_id, order_id): key = f'order:{user_id}:{order_id}' if is_submitted(key): return '订单已经提交,请勿重复提交' else: set_submitted(key, '1', 60) # 处理订单提交逻辑 return '订单提交成功' # 示例:防止用户重复提交评论 def submit_comment(user_id, article_id): key = f'comment:{user_id}:{article_id}' if is_submitted(key): return '评论已经提交,请勿重复提交' else: set_submitted(key, '1', 300) # 处理评论提交逻辑 return '评论提交成功' ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值