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

打印 上一主题 下一主题

主题 869|帖子 869|积分 2607

一、什么是幂等性?

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

首先,我们要知道查询类的请求一般都是天然幂等的,除此之外,删除请求在大多数情况下也是幂等的,但是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来实现的方式。
推荐一个开源免费的 Spring Boot 实战项目:
https://github.com/javastacks/spring-boot-best-practice
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<String, Object> 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. }
  42. import org.aspectj.lang.ProceedingJoinPoint;
  43. import org.aspectj.lang.reflect.CodeSignature;
  44. import java.lang.reflect.Field;
  45. import java.util.HashMap;
  46. import java.util.Map;
  47. public class AopUtils {
  48.     public static Object getFieldValue(Object obj, String name) throws Exception {
  49.         Field[] fields = obj.getClass().getDeclaredFields();
  50.         Object object = null;
  51.         for (Field field : fields) {
  52.             field.setAccessible(true);
  53.             if (field.getName().toUpperCase().equals(name.toUpperCase())) {
  54.                 object = field.get(obj);
  55.                 break;
  56.             }
  57.         }
  58.         return object;
  59.     }
  60.     public static Map<String, Object> getParamValue(ProceedingJoinPoint joinPoint) {
  61.         Object[] paramValues = joinPoint.getArgs();
  62.         String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
  63.         Map<String, Object> param = new HashMap<>(paramNames.length);
  64.         for (int i = 0; i < paramNames.length; i++) {
  65.             param.put(paramNames[i], paramValues[i]);
  66.         }
  67.         return param;
  68.     }
  69. }
复制代码
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. }
  18. public interface IdempotentStorage {
  19.     void save(String idempotentId);
  20.     boolean delete(String idempotentId);
  21. }
  22. import org.springframework.data.redis.core.RedisTemplate;
  23. import org.springframework.stereotype.Component;
  24. import javax.annotation.Resource;
  25. import java.io.Serializable;
  26. import java.util.concurrent.TimeUnit;
  27. @Component
  28. public class RedisIdempotentStorage implements IdempotentStorage {
  29.     @Resource
  30.     private RedisTemplate<String, Serializable> redisTemplate;
  31.     @Override
  32.     public void save(String idempotentId) {
  33.         redisTemplate.opsForValue().set(idempotentId, idempotentId, 10, TimeUnit.MINUTES);
  34.     }
  35.     @Override
  36.     public boolean delete(String idempotentId) {
  37.         return redisTemplate.delete(idempotentId);
  38.     }
  39. }
  40. import java.util.UUID;
  41. public class IdGeneratorUtil {
  42.     public static String generateId() {
  43.         return UUID.randomUUID().toString();
  44.     }
  45. }
复制代码
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<Order> requestData) {
  13.         return "success";
  14.     }
  15. }
复制代码
请求获取token值。

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

第二次请求失败。

版权声明:本文为CSDN博主「码拉松」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/CSDN_WYL2016/article/details/122993086
更多文章推荐:
1.Spring Boot 3.x 教程,太全了!
2.2,000+ 道 Java面试题及答案整理(2024最新版)
3.《Java开发手册(嵩山版)》最新发布!
觉得不错,别忘了随手点赞+转发哦!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

八卦阵

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表