当前位置: 代码网 > it编程>数据库>Redis > 使用Redis控制表单重复提交和控制接口访问频率方式

使用Redis控制表单重复提交和控制接口访问频率方式

2025年06月20日 Redis 我要评论
场景一:控制表单重复提交防重提交有很多方案,从前端的按钮置灰,到后端synchronize锁、lock锁、借助redis语法实现简单锁、redis+lua分布式锁、redisson分布式锁,再到db的

场景一:控制表单重复提交

防重提交有很多方案,从前端的按钮置灰,到后端synchronize锁、lock锁、借助redis语法实现简单锁、redis+lua分布式锁、redisson分布式锁,再到db的悲观锁、乐观锁、借助表唯一索引等等都可以实现防重提交,以保证数据的安全性。

这篇文章我们介绍其中一种方案–借助redis语法实现简单锁,最终实现防重提交。

背景

我们项目中,为了控制表单重复提交问题,会在点击页面按钮(向后端发起业务请求)后就会置灰按钮,直到后端响应后解除按钮置灰。通过按钮置灰来防止重启提交问题。但postman、jmeter和其他服务调用(绕过前端页面)呢?所以后端接口也要根据控制表单重复提交的问题。

后端代码可以在2个位置做控制:

一是放在gateway网关做:

  • 好处是只在一个地方加上控制代码,就可以控制所有接口的重复提交问题。
  • 坏处是控制的范围太广(比如查询接口无需控制,控制了反而多余)、定义重复提交的时间段不能灵活调整。

二是放在aop切面做:

  • 好处是只有需要的地方才会被控制(哪里需要引用一下自定义注解即可),另外也能灵活调整定义重复提交的时间段(自定义注解里定义时间字段开放给使用者填写)。
  • 坏处是每个需要控制的地方都要加注解,会有侵入性和一定的工作量。

实现代码

1、添加自定义注解

package com.xxx.annotations;

import java.lang.annotation.*;

/**
 * 自定义注解防止表单重复提交
 *
 * @author wanglingqiang
 * @date 2023/9/6 10:11
 */
@target(elementtype.method)
@retention(retentionpolicy.runtime)
@documented
public @interface repeatsubmit {

    /**
     * 过期时间,单位毫秒
     */
    long expiretime() default 500l;

}

2、添加aop切面

package com.xxx.aop;

import com.xxx.annotations.repeatsubmit;
import com.xxx.exception.serviceexception;
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.aspectj.lang.reflect.methodsignature;
import org.springframework.data.redis.core.redistemplate;
import org.springframework.stereotype.component;
import org.springframework.web.context.request.requestcontextholder;
import org.springframework.web.context.request.servletrequestattributes;

import javax.annotation.resource;
import javax.servlet.http.httpservletrequest;
import java.lang.reflect.method;
import java.util.concurrent.timeunit;

/**
 * 防止表单重复提交切面
 *
 * @author wanglingqiang
 * @date 2023/9/6 10:13
 */
@slf4j
@aspect
@component
public class repeatsubmitaspect {
    private static final string key_prefix = "repeat_submit:";
    @resource
    private redistemplate redistemplate;

    @pointcut("@annotation(com.xxx.annotations.repeatsubmit)")
    public void repeatsubmit() {}

    @around("repeatsubmit()")
    public object around(proceedingjoinpoint joinpoint) throws throwable {
    	//joinpoint获取方法对象
        method method = ((methodsignature) joinpoint.getsignature()).getmethod();
        //获取方法上的@repeatsubmit注解
        repeatsubmit annotation = method.getannotation(repeatsubmit.class);
        //获取httpservletrequest对象,以获取请求uri
        servletrequestattributes requestattributes = (servletrequestattributes) requestcontextholder.getrequestattributes();
        httpservletrequest request = requestattributes.getrequest();
        string uri = request.getrequesturi();
        //拼接redis的key,这里只是简单根据uri来判断是否重复提交。可以根据自己业务调整,比如根据用户id或者请求token等
        string cachekey = key_prefix.concat(uri);
        boolean flag = null;
        try {
            //借助setifabsent(),key不存在才能设值成功
            flag = redistemplate.opsforvalue().setifabsent(cachekey, "", annotation.expiretime(), timeunit.milliseconds);
        } catch (exception e) {
            //如果redis不可用,则打印日志记录,但依然对请求放行
            log.error("", e);
            return joinpoint.proceed();
        }
        //redis可用的情况,如果flag=true说明单位时间内这是第一次请求,放行
        if (flag) {
            return joinpoint.proceed();
        } else {
            //进入else说明单位时间内进行了多次请求,则拦截请求并提示稍后重试
            throw new serviceexception("系统繁忙,请稍后重试");
        }
    }
}

这里利用redistemplate的setifabsent()实现的,如果存在就不能set成功,set的同时设置过期时间,可以是用使用默认,也可以自己根据业务调整。

另外,cachekey的定义,也可以根据自己的需要去调整,比如根据当前登录用户的userid、当前登录的token等。

3、使用

@slf4j
@restcontroller
@requestmapping("/user")
public class usercontroller {

	@repeatsubmit
    @postmapping
    public ajaxresult add(@validated @requestbody sysuser user) {
    	//....
    }

场景二:控制接口调用频率

背景

忘记密码后通过发送手机验证码找回密码的场景。因为每发一条短信都需要收费,所以要控制发短信的频率。

比如,同一个手机号在3分钟内只能发送3次短信,超过3次后则提示用户“短信发送过于频繁,请10分钟后再试”。

实现代码

@slf4j
@restcontroller
@requestmapping("/sms")
public class smscontroller {
    @resource
    private ismsservice smsservice;
    @resource
    public redistemplate redistemplate;

    @postmapping("/sendvalidcode")
    public result sendvalidcode(@requestbody @valid smsdto smsdto) {
        //验证手机号格式
        checkphonenumber(smsdto.getphonenumber());
        
        //...其他验证
        
		//拼接redis的key(key为手机号,以控制一个手机号有限时间内容发送的次数)
        string cachekey = "sms:code:resetpwd:"+smsdto.getphonenumber();
        //验证发送短信次数,超过则拦截(阈值是3次,超时时间是3分钟,重试时间是10分钟)
        checksendcount(cachekey, threshold, timeout, retry_time);
        return smsservice.sendmsg(smsdto);
    }
    
    /**
     * 验证发送短信次数,超过则拦截
     * 该方法用lua脚本替换实现更好
     */
    private void checksendcount(string cachekey, long threshold, long timeout, string retrytime) {
   		//首先进方法就先+1
        long count = redistemplate.opsforvalue().increment(cachekey);
        //然后比较次数,是否超过阈值
        if (count > threshold) {
            //超过则设置过期时间为10分钟,并提示10分钟后重试
            redistemplate.expire(cachekey, 10l, timeunit.minutes);
            throw new serviceexception("短信发送过于频繁,请" + retrytime + "分钟后再试");
        } else {
            //没超过3次,则累加上这一次
            redistemplate.expire(cachekey, timeout, timeunit.minutes);
        }
    }

}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com