• 注解实现接口幂等性


    一、什么是幂等性

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

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

    首先,我们要知道查询类的请求一般都是天然幂等的,除此之外,删除请求在大多数情况下也是幂等的,但是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.自定义注解
    1. import java.lang.annotation.ElementType;
    2. import java.lang.annotation.Retention;
    3. import java.lang.annotation.RetentionPolicy;
    4. import java.lang.annotation.Target;
    5. @Target(value = ElementType.METHOD)
    6. @Retention(RetentionPolicy.RUNTIME)
    7. public @interface Idempotent {
    8.     /**
    9.      * 参数名,表示将从哪个参数中获取属性值。
    10.      * 获取到的属性值将作为KEY
    11.      *
    12.      * @return
    13.      */
    14.     String name() default "";
    15.     /**
    16.      * 属性,表示将获取哪个属性的值。
    17.      *
    18.      * @return
    19.      */
    20.     String field() default "";
    21.     /**
    22.      * 参数类型
    23.      *
    24.      * @return
    25.      */
    26.     Class type();
    27. }
    2.统一的请求入参对象
    1. @Data
    2. public class RequestData<T> {
    3.     private Header header;
    4.     private T body;
    5. }
    6. @Data
    7. public class Header {
    8.     private String token;
    9. }
    10. @Data
    11. public class Order {
    12.     String orderNo;
    13. }
    3.AOP处理
    1. import com.springboot.micrometer.annotation.Idempotent;
    2. import com.springboot.micrometer.entity.RequestData;
    3. import com.springboot.micrometer.idempotent.RedisIdempotentStorage;
    4. import org.aspectj.lang.ProceedingJoinPoint;
    5. import org.aspectj.lang.annotation.Around;
    6. import org.aspectj.lang.annotation.Aspect;
    7. import org.aspectj.lang.annotation.Pointcut;
    8. import org.aspectj.lang.reflect.MethodSignature;
    9. import org.springframework.stereotype.Component;
    10. import javax.annotation.Resource;
    11. import java.lang.reflect.Method;
    12. import java.util.Map;
    13. @Aspect
    14. @Component
    15. public class IdempotentAspect {
    16.     @Resource
    17.     private RedisIdempotentStorage redisIdempotentStorage;
    18.     @Pointcut("@annotation(com.springboot.micrometer.annotation.Idempotent)")
    19.     public void idempotent() {
    20.     }
    21.     @Around("idempotent()")
    22.     public Object methodAround(ProceedingJoinPoint joinPoint) throws Throwable {
    23.         MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    24.         Method method = signature.getMethod();
    25.         Idempotent idempotent = method.getAnnotation(Idempotent.class);
    26.         String field = idempotent.field();
    27.         String name = idempotent.name();
    28.         Class clazzType = idempotent.type();
    29.         String token = "";
    30.         Object object = clazzType.newInstance();
    31.         Map paramValue = AopUtils.getParamValue(joinPoint);
    32.         if (object instanceof RequestData) {
    33.             RequestData idempotentEntity = (RequestData) paramValue.get(name);
    34.             token = String.valueOf(AopUtils.getFieldValue(idempotentEntity.getHeader(), field));
    35.         }
    36.         if (redisIdempotentStorage.delete(token)) {
    37.             return joinPoint.proceed();
    38.         }
    39.         return "重复请求";
    40.     }
    41. }
    1. import org.aspectj.lang.ProceedingJoinPoint;
    2. import org.aspectj.lang.reflect.CodeSignature;
    3. import java.lang.reflect.Field;
    4. import java.util.HashMap;
    5. import java.util.Map;
    6. public class AopUtils {
    7.     public static Object getFieldValue(Object obj, String name) throws Exception {
    8.         Field[] fields = obj.getClass().getDeclaredFields();
    9.         Object object = null;
    10.         for (Field field : fields) {
    11.             field.setAccessible(true);
    12.             if (field.getName().toUpperCase().equals(name.toUpperCase())) {
    13.                 object = field.get(obj);
    14.                 break;
    15.             }
    16.         }
    17.         return object;
    18.     }
    19.     public static Map<StringObject> getParamValue(ProceedingJoinPoint joinPoint) {
    20.         Object[] paramValues = joinPoint.getArgs();
    21.         String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
    22.         Map<StringObject> param = new HashMap<>(paramNames.length);
    23.         for (int i = 0; i < paramNames.length; i++) {
    24.             param.put(paramNames[i], paramValues[i]);
    25.         }
    26.         return param;
    27.     }
    28. }
    4.Token值生成
    1. import com.springboot.micrometer.idempotent.RedisIdempotentStorage;
    2. import com.springboot.micrometer.util.IdGeneratorUtil;
    3. import org.springframework.web.bind.annotation.RequestMapping;
    4. import org.springframework.web.bind.annotation.RestController;
    5. import javax.annotation.Resource;
    6. @RestController
    7. @RequestMapping("/idGenerator")
    8. public class IdGeneratorController {
    9.     @Resource
    10.     private RedisIdempotentStorage redisIdempotentStorage;
    11.     @RequestMapping("/getIdGeneratorToken")
    12.     public String getIdGeneratorToken() {
    13.         String generateId = IdGeneratorUtil.generateId();
    14.         redisIdempotentStorage.save(generateId);
    15.         return generateId;
    16.     }
    17. }
    1. public interface IdempotentStorage {
    2.     void save(String idempotentId);
    3.     boolean delete(String idempotentId);
    4. }
    1. import org.springframework.data.redis.core.RedisTemplate;
    2. import org.springframework.stereotype.Component;
    3. import javax.annotation.Resource;
    4. import java.io.Serializable;
    5. import java.util.concurrent.TimeUnit;
    6. @Component
    7. public class RedisIdempotentStorage implements IdempotentStorage {
    8.     @Resource
    9.     private RedisTemplate<StringSerializable> redisTemplate;
    10.     @Override
    11.     public void save(String idempotentId) {
    12.         redisTemplate.opsForValue().set(idempotentId, idempotentId, 10TimeUnit.MINUTES);
    13.     }
    14.     @Override
    15.     public boolean delete(String idempotentId) {
    16.         return redisTemplate.delete(idempotentId);
    17.     }
    18. }
    1. import java.util.UUID;
    2. public class IdGeneratorUtil {
    3.     public static String generateId() {
    4.         return UUID.randomUUID().toString();
    5.     }
    6. }
    5. 请求示例

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

    1. import com.springboot.micrometer.annotation.Idempotent;
    2. import com.springboot.micrometer.entity.Order;
    3. import com.springboot.micrometer.entity.RequestData;
    4. import org.springframework.web.bind.annotation.RequestBody;
    5. import org.springframework.web.bind.annotation.RequestMapping;
    6. import org.springframework.web.bind.annotation.RestController;
    7. @RestController
    8. @RequestMapping("/order")
    9. public class OrderController {
    10.     @RequestMapping("/saveOrder")
    11.     @Idempotent(name = "requestData", type = RequestData.class, field = "token")
    12.     public String saveOrder(@RequestBody RequestData requestData) {
    13.         return "success";
    14.     }
    15. }

    请求获取token值。

    图片

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

    图片

    第二次请求失败。

    图片

  • 相关阅读:
    windows 服务器 应用自启动
    Kafka自带zookeeper---集群安装部署
    黑马C++ 02 核心6 —— 类和对象_多态_文件操作(重难点)
    区间素数 马蹄集
    如何测试接口?首先你得知道如何开发接口。
    我的备考分享:良好的企业治理需要CGEIT
    【技术积累】HTML+CSS+JavaScript中的基础知识【三】
    【scikit-learn基础】--『监督学习』之 随机森林分类
    redis的解决分布式锁的bug 和 redis面试题
    CentOS8 安装Yapi
  • 原文地址:https://blog.csdn.net/yuechuzhixing/article/details/133016303