首页 > 其他分享 >使用AOP防止请求重复提交

使用AOP防止请求重复提交

时间:2024-12-14 16:54:17浏览次数:7  
标签:key String value 提交 AOP 注解 方法 public 请求

使用AOP防止请求重复提交

常见的重复提交场景
网络延迟:用户在提交订单后未收到确认,误以为订单未提交成功,连续点击提交按钮。
页面刷新:用户在提交订单后刷新页面,触发订单的重复提交。
用户误操作:用户无意中点击多次订单提交按钮。
防止重复提交的需求
幂等性保证:确保相同的请求多次提交只能被处理一次,最终结果是唯一的。
用户体验保障:避免由于重复提交导致用户感知的延迟或错误。
常用解决方案
前端防重机制:在前端按钮点击时禁用按钮或加锁,防止用户多次点击。
后端幂等处理:
利用Token机制:在订单生成前生成一个唯一的Token,保证每个订单提交时只允许携带一次Token。
基于数据库的唯一索引:通过对订单字段(如订单号、用户ID)创建唯一索引来防止重复数据的插入。
分布式锁:使用Redis等分布式缓存加锁,保证同一时间只允许处理一个订单请求。
通常来说,后端在数据库上会设置唯一索引,在服务中会结合使用Token和redis保证同一时间只允许一个订单请求

  1. 首先定义注解NoPepeatSubmit
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
    long value() default 1000*10;
}
  1. 定义AOP相关方法
public class RepeatSubmitAspect {

    @Autowired
    private StringRedisService stringRedisService;

    @Pointcut("@annotation(xxx.xxx.NoRepeatSubmit)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Assert.notNull(request, "request can not null");
        // 此处可以用token--作为用户唯一标识
        String token = request.getHeader("Authorization-admin");
        String key = token + "-" + request.getServletPath();
	// 获取注解
        NoRepeatSubmit annotation = ((MethodSignature) pjp.getSignature()).getMethod().getAnnotation(NoRepeatSubmit.class);
	// 获取注解相关参数:这里是时间,表示同一用户多久可以请求一次
        long expire = annotation.value();
        //超时时间:10秒,最好设为常量
        String time=String.valueOf(System.currentTimeMillis() + expire);
        //加锁 --这里需要考虑并发问题,详情见下文
        boolean islock = stringRedisService.secKilllock(key, time);
        if (islock) {
            Object result;
            try {
	        //执行请求
                result = pjp.proceed();
            } finally {
                //解锁
                stringRedisService.unlock(key,time);
            }
            return result;
        }else {
	    // 重复请求
            return new Result(CoReturnFormat.REPEAT_REQUEST);
        }
    }
}
  1. 请求上添加NoPepeatSubmit注解

注:相关概念

防止重复提交涉及到的锁相关概念

防止重复提交的基本思路是对一个请求,将它存储到redis中作为key,value是保持时间---这个时间内不对相同的请求做响应
首先是确保同一用户的同一请求,这里使用token + "-" + request.getServletPath()保证
其次,redis中锁设置如下:

public boolean secKilllock(String key,String value){
        /**
         * setIfAbsent就是setnx
         * 将key设置值为value,如果key不存在,这种情况下等同SET命令。
         * 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写
         * */
        if(redisTemplate.opsForValue().setIfAbsent(key,value)){
            //加锁成功返回true
            return true;
        }
        //避免死锁,且只让一个线程拿到锁
        //走到这里的线程都加锁失败了,有两种情况,
        //一种是在重复提交的时间范围内,
        //一种是不在重复提交的范围内,这是这可能是因为加入redis的请求出现异常,导致没有删除key
        String currentValue = (String) redisTemplate.opsForValue().get(key);
        /**
         * 下面这几行代码的作用:
         * 1、防止死锁
         * 2、防止多线程抢锁
         * */
        if(! StringUtils.isEmpty(currentValue)
                && Long.parseLong(currentValue) < System.currentTimeMillis()){
            //如果锁过期了,获取上一个锁的时间
            String oldValue = (String) redisTemplate.opsForValue().getAndSet(key,value);
            //只会让一个线程拿到锁----这里有个问题,多个线程到这里的时候,不能确保通过的线程是上锁的线程
            //这里应该保证的是立即重复点击的请求放行一个即可
            //如果旧的value和currentValue相等,只会有一个线程达成条件,因为第二个线程拿到的oldValue已经和currentValue不一样了
            if(! StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){
                return true;
            }
        }
        return false;
    }
    /**
     * 解锁
     * @param key
     * @param value
     * */
    @Override
    public void unlock(String key,String value){
        try{
            String currentValue = (String) redisTemplate.opsForValue().get(key);
            if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
                redisTemplate.opsForValue().getOperations().delete(key);
            }
        }catch (Exception e){
            e.printStackTrace();
            log.error("『redis分布式锁』解锁异常,{}", e);
        }
    }

注解--黑马

Java注解是代码中的特殊标记,比如@Override、@Test等,作用是:让其他程序根据注解信息决定怎么执行该程序。
比如:Junit框架的@Test注解可以用在方法上,用来标记这个方法是测试方法,被@Test标记的方法能够被Junit框架执行。
再比如:@Override注解可以用在方法上,用来标记这个方法是重写方法,被@Override注解标记的方法能够被IDEA识别进行语法检查。
注解不光可以用在方法上,还可以用在类上、变量上、构造器上等位置。
自定义注解的格式如下图所示:
img
比如:现在我们自定义一个MyTest注解

public @interface MyTest{
    String aaa();
    boolean bbb() default true;	//default true 表示默认值为true,使用时可以不赋值。
    String[] ccc();
}

定义好MyTest注解之后,我们可以使用MyTest注解在类上、方法上等位置做标记。注意使用注解时需要加@符号,如下

@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{
    @MyTest(aaa="铁扇公主",bbb=false, ccc={"Python","前端","Java"})
    public void test1(){
        
    }
}

注意:注解的属性名如何是value的话,并且只有value没有默认值,使用注解时value名称可以省略。比如现在重新定义一个MyTest2注解

public @interface MyTest2{
    String value(); //特殊属性
    int age() default 10;
}

定义好MyTest2注解后,再将@MyTest2标记在类上,此时value属性名可以省略,代码如下

@MyTest2("孙悟空") //等价于 @MyTest2(value="孙悟空")
@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{
    @MyTest(aaa="铁扇公主",bbb=false, ccc={"Python","前端","Java"})
    public void test1(){
        
    }
}

注解本质是什么呢?

img
1.MyTest1注解本质上是接口,每一个注解接口都继承子Annotation接口
2.MyTest1注解中的属性本质上是抽象方法
3.@MyTest1实际上是作为MyTest接口的实现类对象
4.@MyTest1(aaa="孙悟空",bbb=false,ccc={"Python","前端","Java"})里面的属性值,可以通过调用aaa()、bbb()、ccc()方法获取到。
什么是元注解?

元注解是修饰注解的注解。这句话虽然有一点饶,但是非常准确。我们看一个例子
img
接下来分别看一下@Target注解和@Retention注解有什么作用,如下图所示

@Target是用来声明注解只能用在那些位置,比如:类上、方法上、成员变量上等
@Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期

img
解析注解

我们把获取类上、方法上、变量上等位置注解及注解属性值的过程称为解析注解。
解析注解套路如下
1.如果注解在类上,先获取类的字节码对象,再获取类上的注解
2.如果注解在方法上,先获取方法对象,再获取方法上的注解
3.如果注解在成员变量上,先获取成员变量对象,再获取变量上的注解
总之:注解在谁身上,就先获取谁,再用谁获取谁身上的注解
img

接口--黑马

当定义接口是默认成员变量使用public static final修饰,方法使用public abstract修饰,可以省略

public interface A{
    //这里public static final可以加,可以不加。
    public static final String SCHOOL_NAME = "黑马程序员";
    
    //这里的public abstract可以加,可以不加。
    public abstract void test();
}

JDK8的新特性
增加了默认方法,私有方法,静态方法三种方法

public interface A {
    /**
     * 1、默认方法:必须使用default修饰,默认会被public修饰
     * 实例方法:对象的方法,必须使用实现类的对象来访问。
     */
    default void test1(){
        System.out.println("===默认方法==");
        test2();
    }

    /**
     * 2、私有方法:必须使用private修饰。(JDK 9开始才支持的)
     *   实例方法:对象的方法。
     */
    private void test2(){
        System.out.println("===私有方法==");
    }

    /**
     * 3、静态方法:必须使用static修饰,默认会被public修饰
     */
     static void test3(){
        System.out.println("==静态方法==");
     }

     void test4();
     void test5();
     default void test6(){

     }
}

1.一个接口继承多个接口,如果多个接口中存在相同的方法声明,则此时不支持多继承
2.一个类实现多个接口,如果多个接口中存在相同的方法声明,则此时不支持多实现
3.一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会优先使用父类的方法
4.一个类实现类多个接口,多个接口中有同名的默认方法,则这个类必须重写该方法。
综上所述:一个接口可以继承多个接口,接口同时也可以被类实现。

public class Test implements A,B {
    //对应情况2,一个类实现多个同名方法的接口只会实现一个方法,确保唯一,idea中这个方法显示是多个同名方法的实现
    @Override
    public void test() {
        System.out.println("hello");
    }
}
interface A{
    void test();
}
interface B{
    void test();
}
interface C extends A,B{

}
class D implements C{
    // 对应情况1,接口继承了多个同名方法接口,实现时只有一个同名方法
    @Override
    public void test() {
        System.out.println("D");
    }
}

类似的,类比到情况4,当出现接口同名方法冲突时,java会保证只实现一个,从而确保唯一性,避免冲突

标签:key,String,value,提交,AOP,注解,方法,public,请求
From: https://www.cnblogs.com/zsyzw/p/18606918

相关文章

  • gofiber: 请求参数是数组的处理
    一,js处理数组的形式:js的处理:varaddIdList=[];for(i=0;i<content.length;i++){if(content[i].checked){addIdList.push(content[i].value);}}console.log("选中的id:");......
  • CCAMap的定位方法增加权限请求
    procedureTCCAMap.StartLocation();begin{$IFDEFANDROID}PermissionsService.RequestPermissions([JStringToString(TJManifest_permission.JavaClass.ACCESS_COARSE_LOCATION),JStringToString(TJManifest_permission.Jav......
  • Post请求的两种编码格式:application/x-www-form-urlencoded和multipart/form-data
    一、前端表单提交时application/x-www-form-urlencoded表单代码:<formaction="http://localhost:8888/task/"method="POST">Firstname:<inputtype="text"name="firstName"value="Mickey&"><br>Last......
  • ASP .NET Core 中的请求-响应日志记录
    参考源码:https://download.csdn.net/download/hefeng_aspnet/90084914         记录ASP.NETCorehttp请求和响应是几乎每个.NET开发人员迟早都会面临的常见任务。长期以来,开发团队选择的最流行的方法似乎是编写自定义中间件。但是,既然 .NET6 我们有一个Micr......
  • 有没有大佬可以帮忙看一下我基于python爬取租房数据的代码,新手第一次发帖子可能有点乱
    这是我的代码,代码基本雏形是在本网站的一位大佬的帖子里复制过来的,经过更改爬取的网页基本信息之后,发现只能爬取一个数据,真的不知道问题出现在哪里了,本人基础很薄弱很菜鸡,但还是想搞清楚问题出现在哪里,就上来求助了importrequestsfromlxmlimportetreeimportcsv#fro......
  • 一个基于gevent的异步请求库 - grequests
    1.安装pipinstallgrequests-ihttp://mirrors.aliyun.com/pypi/simple/--trusted-hostmirrors.aliyun.com2.基础用法教程用grequests.map()方法时,传入的必须是生成器或列表,下面是用小括号创建的是生成器,用方括号也行,生成列表。importgrequestsimporttimeur......
  • 请求响应(Request-Response)和事件响应(Event-Driven)
    请求响应(Request-Response)和事件响应(Event-Driven)是两种常见的软件和系统设计框架,它们在目的、设计和实现方式上存在明显的区别:请求响应框架目的和概念请求响应框架是一种同步通信模式,常见于客户端-服务器架构中。在这种框架下,客户端发送请求到服务器,服务器经过处理后返回......
  • 【鸿蒙 ArkTS 开发】网络请求HTTP并渲染列表展示
    1.页面布局和网络请求(展示产品信息)在这个页面中,我们会从网络获取产品数据,并使用List组件展示产品信息。product_list_page.etsimportui;import@ohos.net.http;importohos.agp.components.List;importohos.agp.components.Text;importohos.agp.components.Image;......
  • 通过http请求下载doc文件
    通过地址请求获取文件流,并将其保存到本地packagecn.oyun.speech.module.system.service;importjava.io.IOException;importjava.io.OutputStream;importjava.net.URI;importjava.net.http.HttpClient;importjava.net.http.HttpRequest;importjava.net.http.HttpRe......
  • 两种最常用的HTTP请求方法
    简介在Web开发中,GET和POST是两种最常用的HTTP请求方法,用于从客户端向服务器传输数据。它们各自有不同的用途和特点,适用于不同的场景。1.GET方法GET方法主要用于请求从服务器获取数据。它通常用于读取操作,而不是写入或修改服务器上的数据。以下是GET方法的一些关键特点:......