一个注解实现接口幂等性,真心优雅!

一、什么是幂等性?

简单来说,就是对一个接口执行重复的多次请求,与一次请求所产生的结果是相同的,听起来非常容易理解,但要真正的在系统中要始终保持这个目标,是需要很严谨的设计的,在实际的生产环境下,我们应该保证任何接口都是幂等的,而如何正确的实现幂等,就是本文要讨论的内容。

二、哪些请求天生就是幂等的?

首先,我们要知道查询类的请求一般都是天然幂等的,除此之外,删除请求在大多数情况下也是幂等的,但是ABA场景下除外。

举一个简单的例子

比如,先请求了一次删除A的操作,但由于响应超时,又自动请求了一次删除A的操作,如果在两次请求之间,又插入了一次A,而实际上新插入的这一次A,是不应该被删除的,这就是ABA问题,不过,在大多数业务场景中,ABA问题都是可以忽略的。

除了查询和删除之外,还有更新操作,同样的更新操作在大多数场景下也是天然幂等的,其例外是也会存在ABA的问题,更重要的是,比如执行update table set a = a + 1 where v = 1这样的更新就非幂等了。

最后,就还剩插入了,插入大多数情况下都是非幂等的,除非是利用数据库唯一索引来保证数据不会重复产生。

三、为什么需要幂等

1.超时重试

当发起一次RPC请求时,难免会因为网络不稳定而导致请求失败,一般遇到这样的问题我们希望能够重新请求一次,正常情况下没有问题,但有时请求实际上已经发出去了,只是在请求响应时网络异常或者超时,此时,请求方如果再重新发起一次请求,那被请求方就需要保证幂等了。

2.异步回调

异步回调是提升系统接口吞吐量的一种常用方式,很明显,此类接口一定是需要保证幂等性的。

3.消息队列

现在常用的消息队列框架,比如:Kafka、RocketMQ、RabbitMQ在消息传递时都会采取At least once原则(也就是至少一次原则,在消息传递时,不允许丢消息,但是允许有重复的消息),既然消息队列不保证不会出现重复的消息,那消费者自然要保证处理逻辑的幂等性了。

四、实现幂等的关键因素

关键因素1

幂等唯一标识,可以叫它幂等号或者幂等令牌或者全局ID,总之就是客户端与服务端一次请求时的唯一标识,一般情况下由客户端来生成,也可以让第三方来统一分配。

关键因素2

有了唯一标识以后,服务端只需要确保这个唯一标识只被使用一次即可,一种常见的方式就是利用数据库的唯一索引。

五、注解实现幂等性

下面演示一种利用Redis来实现的方式。

1.自定义注解

import java.lang.annotation.ElementType;
importjava.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value =ElementType. METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent{
    /**
    *参数名,表示将从哪个参数中获取属性值。
    *获取到的属性值将作为KEY。
    *
    *@return
    */
    String name() default "";
    /**
    *属性,表示将获取哪个属性的值。
    *
    *@return
    */
    String field() default "";
    /**
    *参数类型
    *
    *@return
    */
    Class type();
}

2.统一的请求入参对象

@Data
public class RequestData<T>{
    private Header header;
    private T body;
}

@Data
public class Header{
    private String token;
}

@Data
public class Order{
    String orderNo;
}

3.AOP处理

importcom.springboot.micrometer.annotation.Idempotent;
importcom.springboot.micrometer.entity.RequestData;
importcom.springboot.micrometer.idempotent.RedisIdempotentStorage;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
importorg.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
importorg.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Map;
@Aspect
@Component
public class IdempotentAspect{
    @Resource
    private RedisIdempotentStorage redisIdempotentStorage;
    @Pointcut("@annotation(com.springboot.micrometer.annotation.Idempotent)")
        public void idempotent(){
    }
    @Around("idempotent()")
    public Object methodAround(ProceedingJoinPoint joinPoint) throws Throwable{
        MethodSignature signature=(MethodSignature)joinPoint.getSignature();
        Method method= signature. getMethod();
        Idempotentidempotent=method.getAnnotation(Idempotent.class);
        String field = idempotent. field();
        String name=idempotent. name();
        Class clazzType = idempotent.type();
        String token = "";
        Object object= clazzType.newInstance();
        Map<String, Object>paramValue=AopUtils.getParamValue(joinPoint);
        if (object instanceof RequestData){
            RequestData idempotentEntity=(RequestData) paramValue.get(name);
            token=String.valueOf(AopUtils.getFieldValue(idempotentEntity.getHeader(),fi
        }
        if(redisIdempotentStorage.delete(token)){
            return joinPoint.proceed();
        }
        return"重复请求";
    }
}


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.CodeSignature;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class AopUtils{
    public static Object getFieldValue(Object obj, String name) throws Exception{
        Field[] fields = obj.getClass().getDeclaredFields();
        Object object = null;
        for (Field field : fields)(
            field.setAccessible(true);
            if(field.getName().toUpperCase().equals(name.toUpperCase())){
                object= field.get(obj);
                break;
            }
        }
        return object;
    }
    
    public static Map<String, Object> getParamValue(ProceedingJoinPoint joinPoint){
        Object[] paramValues=joinPoint.getArgs();
        String[]paramNames=(CodeSignature)joinPoint.getSignature()).getParameterNames
        Map<String, Object> param= new HashMap<>(paramNames.length);
        for (int i= 0; i < paramNames.length; i+){
            param.put(paramNames[i],paramValues[i]);
        }
        return param;
    }
}

4.Token值生成

importcom.springboot.micrometer.idempotent.RedisIdempotentStorage;
importcom.springboot.micrometer.util.IdGeneratorUtil;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/idGenerator")
public class IdGeneratorController{
    @Resource
    private RedisIdempotentStorage redisIdempotentStorage;
    @RequestMapping("/getIdGeneratorToken")
    public String getIdGeneratorToken(){
        String generateId=IdGeneratorUtil.generateId();
        redisIdempotentStorage.save(generateId);
        return generateId;
    }
}

public interface IdempotentStorage{
    void save(String idempotentId);
    boolean delete(String idempotentId);
}



importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.Serializable;
importjava.util.concurrent.TimeUnit;
@Component
public class RedisIdempotentStorage implements IdempotentStorage{
    @Resource
    private RedisTemplate<String, Serializable> redisTemplate;
    @Override
    public void save(String idempotentId){
        redisTemplate.opsForValue().set(idempotentId,idempotentId,10,TimeUnit.MINUTES);
    }
    @Override
    public boolean delete(String idempotentId){
        return redisTemplate.delete(idempotentId);
    }
}


import java.util.UUID;
public class IdGeneratorUtil{
    public static String generateId(){
        return UUID.randomUUID().toString();
    }
}

5. 请求示例

调用接口之前,先申请一个token,然后带着服务端返回的token值,再去请求。

importcom.springboot.micrometer.annotation.Idempotent;
importcom.springboot.micrometer.entity.Order;
importcom.springboot.micrometer.entity.RequestData;
importorg.springframework.web.bind.annotation.RequestBody;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
public class OrderController{
    @RequestMapping("/saveOrder")
    @Idempotent(name="requestData",type=RequestData.class,field="token")
    public String saveOrder(@RequestBody RequestData<Order> requestData){
        return "success";
    }
}

请求获取token值。

带着token值,第一次请求成功。

第二次请求失败。

  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值