使用AOP防止请求重复提交
常见的重复提交场景
网络延迟:用户在提交订单后未收到确认,误以为订单未提交成功,连续点击提交按钮。
页面刷新:用户在提交订单后刷新页面,触发订单的重复提交。
用户误操作:用户无意中点击多次订单提交按钮。
防止重复提交的需求
幂等性保证:确保相同的请求多次提交只能被处理一次,最终结果是唯一的。
用户体验保障:避免由于重复提交导致用户感知的延迟或错误。
常用解决方案
前端防重机制:在前端按钮点击时禁用按钮或加锁,防止用户多次点击。
后端幂等处理:
利用Token机制:在订单生成前生成一个唯一的Token,保证每个订单提交时只允许携带一次Token。
基于数据库的唯一索引:通过对订单字段(如订单号、用户ID)创建唯一索引来防止重复数据的插入。
分布式锁:使用Redis等分布式缓存加锁,保证同一时间只允许处理一个订单请求。
通常来说,后端在数据库上会设置唯一索引,在服务中会结合使用Token和redis保证同一时间只允许一个订单请求
- 首先定义注解NoPepeatSubmit
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
long value() default 1000*10;
}
- 定义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);
}
}
}
- 请求上添加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识别进行语法检查。
注解不光可以用在方法上,还可以用在类上、变量上、构造器上等位置。
自定义注解的格式如下图所示:
比如:现在我们自定义一个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(){
}
}
注解本质是什么呢?
1.MyTest1注解本质上是接口,每一个注解接口都继承子Annotation接口
2.MyTest1注解中的属性本质上是抽象方法
3.@MyTest1实际上是作为MyTest接口的实现类对象
4.@MyTest1(aaa="孙悟空",bbb=false,ccc={"Python","前端","Java"})里面的属性值,可以通过调用aaa()、bbb()、ccc()方法获取到。
什么是元注解?
元注解是修饰注解的注解。这句话虽然有一点饶,但是非常准确。我们看一个例子
接下来分别看一下@Target注解和@Retention注解有什么作用,如下图所示
@Target是用来声明注解只能用在那些位置,比如:类上、方法上、成员变量上等
@Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期
解析注解
我们把获取类上、方法上、变量上等位置注解及注解属性值的过程称为解析注解。
解析注解套路如下
1.如果注解在类上,先获取类的字节码对象,再获取类上的注解
2.如果注解在方法上,先获取方法对象,再获取方法上的注解
3.如果注解在成员变量上,先获取成员变量对象,再获取变量上的注解
总之:注解在谁身上,就先获取谁,再用谁获取谁身上的注解
接口--黑马
当定义接口是默认成员变量使用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