首页 > 数据库 >基于redis实现防重提交

基于redis实现防重提交

时间:2023-10-25 11:06:53浏览次数:32  
标签:key return String org redis RestfulResult 提交 import 防重


自定义防重提交

1. 自定义注解
import java.lang.annotation.*;

/**
 * 自定义防重提交
 * @author 
 * @date 2023年9月6日11:19:13
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {


    /**
     * 默认防重提交,是方法参数
     * @return
     */
    Type limitType() default Type.PARAM;


    /**
     * 加锁过期时间,默认是5秒
     * @return
     */
    long lockTime() default 5;

    /**
     * 规定周期内限制次数
     */
    int maxCount() default 1;

    /**
     * 触发限制时的消息提示
     */
    String msg() default "操作频率过高";


    /**
     * 防重提交,支持两种,一个是方法参数,一个是令牌
     */
    enum Type {
        PARAM(1, "方法参数"), TOKEN(2, "令牌");

        private int id;

        private String name;


        Type() {

        }

        Type(int id, String name) {
            this.id = id;
            this.name = name;
        }

        public int getId() {
            return id;
        }

        public String getName() {
            return this.name;
        }

    }

    /**
     * 是否需要登录
     * @return
     */
    boolean needLogin() default false;
}
2. 基于环绕通知实现限流锁
import cn.hutool.extra.servlet.ServletUtil;
import com.qihoo.mssosservice.annotation.RepeatSubmit;
import com.qihoo.mssosservice.constants.Constants;
import com.qihoo.mssosservice.model.resp.RestfulResult;
import com.redxun.common.utils.ContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

/**
 * 基于环绕通知实现限流锁
 * @author 
 * @date 2023年9月6日17:11:10
 */
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {


    @Autowired
    private StringRedisTemplate redisTemplate;


    /**
     * 定义 @Pointcut注解表达式, 通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法
     * 在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点
     * <p>
     * 方式一:@annotation:当执行的方法上拥有指定的注解时生效(我们采用这)
     * 方式二:execution:一般用于指定方法的执行
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {

    }


    /**
     * 环绕通知, 围绕着方法执行
     *
     * @param joinPoint
     * @param repeatSubmit
     * @return
     * @throws Throwable
     * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
     * <p>
     * 方式一:单用 @Around("execution(* cn.mss.management.center.controller.*.*(..))")可以
     * 方式二:用@Pointcut和@Around联合注解也可以(我们采用这个)
     * <p>
     * <p>
     * 两种方式
     * 方式一:加锁 固定时间内不能重复提交
     * <p>
     * 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交
     */
    @Around("pointCutNoRepeatSubmit(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        String accountNo = ObjectUtils.isEmpty(ContextUtil.getCurrentUser()) ? "admin" :  ContextUtil.getCurrentUser().getAccount();

        //用于记录成功或者失败
        boolean res = false;

        // 超时时间
        long lockTime = repeatSubmit.lockTime();
        // 最大次数
        int maxCount = repeatSubmit.maxCount();
        // 异常信息
        String msg = repeatSubmit.msg();
        //防重提交类型
        String type = repeatSubmit.limitType().name();
        // 是否需要登录
        boolean needLogin = repeatSubmit.needLogin();
        if (needLogin) {
            String authorization = request.getHeader("Authorization");

            if (StringUtils.isBlank(authorization )) {
                authorization = request.getParameter("Authorization");
            }

            if (StringUtils.isBlank(authorization)) {
                return RestfulResult.getFailResult("没有登录");
            }
        }

        if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
            //方式一,参数形式防重提交
            String ipAddr = ServletUtil.getClientIP(request, null);

            String requestURI = request.getRequestURI();

            MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();

            Method method = methodSignature.getMethod();

            String className = method.getDeclaringClass().getName();

            String key = Constants.SUBMIT_ORDER_REPEAT_SUBMIT+  DigestUtils.md5DigestAsHex(String.format("%s-%s-%s-%s-%s",ipAddr,requestURI,className,method,accountNo).getBytes(StandardCharsets.UTF_8));


            String count = redisTemplate.opsForValue().get(key);


            if (StringUtils.isBlank(count)) {
                //在规定周期内第一次访问,存入redis
//                redisTemplate.opsForValue().increment(key);
//                redisTemplate.expire(key, lockTime, TimeUnit.SECONDS);
                //加锁
                res  = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS);
            } else {
                if (Integer.valueOf(count) > maxCount) {
                    //超出访问限制次数
                    return RestfulResult.getFailResult(msg);
                } else {
                    redisTemplate.opsForValue().increment(key);
                }
            }

        } else if (type.equalsIgnoreCase(RepeatSubmit.Type.TOKEN.name())) {
            //方式二,令牌形式防重提交
            String authorization = request.getHeader("Authorization");
            if (StringUtils.isBlank(authorization )) {
                authorization = request.getParameter("Authorization");
            }
            if (StringUtils.isBlank(authorization)) {
                return RestfulResult.getFailResult("没有登录");
            }

            String key = String.format(Constants.SUBMIT_ORDER_REPEAT_TOKEN_KEY, authorization, accountNo);

            String count = redisTemplate.opsForValue().get(key);

            if (StringUtils.isBlank(count)) {
                //在规定周期内第一次访问,存入redis
                //加锁
                res  = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS);
            } else {
                if (Integer.valueOf(count) > maxCount) {
                    //超出访问限制次数
                    return RestfulResult.getFailResult(msg);
                } else {
                    redisTemplate.opsForValue().increment(key);
                }
            }

            /**
             * 提交表单的token key
             * 方式一:不用lua脚本获取再判断,之前是因为 key组成是 os-service:submit:accountNo, value是对应的token,所以需要先获取值,再判断
             * 方式二:可以直接key是 os-service:submit:accountNo:token,然后直接删除成功则完成
             */
//            res = redisTemplate.delete(key);

        } else {
            return RestfulResult.getFailResult(type+":未定义的类型!");
        }
        if (!res) {
            log.error("请求重复提交");
            return RestfulResult.getFailResult("请求重复提交");
        }

        log.info("环绕通知执行前");

        Object obj = joinPoint.proceed();

        log.info("环绕通知执行后");

        return obj;

    }


}
3. 使用 示例
/**
 * 1. 使用默认
 **/
@RepeatSubmit   
@GetMapping("/getAllURL")
public RestfulResult getAllURL() {
    // todo 
    return  RestfulResult.getSuccessResult(result);
}
/**
 * 1.替换默认值
 **/
@RepeatSubmit(limitType =RepeatSubmit.Type.PARAM,lockTime=10,maxCount=2,msg = "test",needLogin = true)
@GetMapping("/getAllURL")
public RestfulResult getAllURL() {
    // todo 
    return  RestfulResult.getSuccessResult(result);
}


标签:key,return,String,org,redis,RestfulResult,提交,import,防重
From: https://blog.51cto.com/u_4981212/8015471

相关文章

  • Gerrit合并追加提交
    Gerrit需要review代码后才能合入,提交到Gerrit后,后面再修改一般都是先Abandoned第一笔后再重新提交这样麻烦,并且会产生Abandoned记录,Gerrit是以Change-Id作为标识,只要Change-Id一致,Gerrit就认为是同一笔提交时选中amend进行补充修改提交到上一个已提交的节点上......
  • redis实现短信登录流程
    (redis实现短信登录)最近在学习使用redis,实现一个简单的短信登录功能(没使用第三方api发送短信),使用的是黑马点评项目<aname="pf8N1"></a>先用session实现,再用redis代替session一、基于session实现短信登录的流程<aname="N2YCq"></a>发送短信验证码根据上边的流程图写@Post......
  • 分布式锁优化(基于redisson实现)
    基于setnx实现的分布式锁存在下面的问题:1.不可重入同一个线程无法多次获取同一把锁2.不可重试获取锁只尝试一次就返回false,没有重试机制3.超时释放锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患4.主从一致性(主写从读)如果Redis提供了主从集群,主......
  • Redis 6 学习笔记 4 —— 通过秒杀案例,学习并发相关和apache bench的使用,记录遇到的问
    背景这是某硅谷的redis案例,主要问题是解决计数器和人员记录的事务操作按照某硅谷的视频敲完之后出现这样乱码加报错的问题 乱码的问题要去tomcat根目录的conf文件夹下修改logging.properties,把下面两个encoding参数都改成GBK就行。其实错误也很明显(ClassNotFoundExceptio......
  • docker安装redis
    docker安装Redis拉取镜像dockerpullredis创建目录mkdir/tool/redis镜像里不包含配置文件,需要拉取redis最新的配置文件,查看下载完成直接通过ftp传到/tool/reids目录下就行因为是官方配置,需要我们手动改下配置:#常用配置bind127.0.0.1 #注释掉这部分,使redis可以......
  • 为什么单线程Redis能那么快
    单线程澄清Redis的单线程,指的是Redis的键值对读写由一个线程来完成。Redis的多线程:持久化异步删除集群数据同步网络IO(Redis6.0引入,5.0及之前都是单线程)......
  • Redis-cluster群集操作步骤(主从切换、新增、删除主从节点)
    1.进入集群客户端任意选一个redis节点,进入redis所在目录cd/redis所在目录/src/./redis-cli-h本地节点的ip-predis的端口号-a密码[root@mysql-db01~]#redis-cli-h10.0.0.51-p637910.0.0.51:6379> 2.查看集群中各个节点状态集群(cluster)clusterinfo......
  • docker-搭建一主两备redis集群
    一目的docker-搭建一主两备redis集群概述:目前要搭建一个“一主两备”redis集群,这个三个容器中redis的端口号为默认的6379,对外暴露的端口为6701,6702,6703,其中6701为master。6702和6703为slave二实现1.准备三份配置文件 1.1配置文件可从网上下载,下载后,可按该贴作修改,http......
  • ClickeOnce 打包 vc_redist
    添加RuntimeLibraries在VisualStudio项目属性中,选择发布,选择系统必备组件,然后选中VisualC++RuntimeLibraties,选中“从与我的应用程序相同的位置下载系统必备组件”下载下载vc_redist放入C:\ProgramFiles(x86)\MicrosoftSDKs\ClickOnceBootstrapper\Packages......
  • 【虹科干货】谈谈Redis Enterprise实时搜索的过人之处
    我们都知道,用户在使用应用程序时候,对于速度有着越来越高的要求,真可谓是“一秒也等不及”。而开发团队又该怎样来满足这种对于实时性的期望呢? 文章速览: RedisEnterprise实时搜索的应用场景利用索引为开发人员带来更好的体验RedisEnterprise实时搜索的优势低延迟搜索的3......