使用AOP防止请求重复提交

使用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会保证只实现一个,从而确保唯一性,避免冲突

posted @   zsandyzw  阅读(49)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· [AI/GPT/综述] AI Agent的设计模式综述
点击右上角即可分享
微信分享提示