子龙 子龙
首页
学习指南
工具
AI副业
开源项目
技术书籍

程序员子龙

Java 开发从业者
首页
学习指南
工具
AI副业
开源项目
技术书籍
  • 基础

  • JVM

  • Spring

  • 并发编程

  • Mybatis

  • 网络编程

  • 数据库

  • 缓存

  • 设计模式

  • 分布式

  • 高并发

    • 秒杀系统扣减库存
    • 聊聊大厂都怎么防止重复下单?
    • 一个注解,优雅的实现接口幂等性
    • SpringBoot

    • SpringCloudAlibaba

    • Nginx

    • 面试

    • 生产问题

    • 系统设计

    • 消息中间件

    • Java
    • 高并发
    程序员子龙
    2024-11-17
    目录

    一个注解,优雅的实现接口幂等性

    # 一、什么是幂等性?

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

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

    首先,我们要知道查询类的请求一般都是天然幂等的,除此之外,删除请求在大多数情况下也是幂等的,但是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;
    import java.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();
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    # 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;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 3.AOP处理
    import com.springboot.micrometer.annotation.Idempotent;
    import com.springboot.micrometer.entity.RequestData;
    import com.springboot.micrometer.idempotent.RedisIdempotentStorage;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.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();
            Idempotent idempotent = 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(), field));
            }
    
            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;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    # 4.Token值生成
    import com.springboot.micrometer.idempotent.RedisIdempotentStorage;
    import com.springboot.micrometer.util.IdGeneratorUtil;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.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);
    }
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.io.Serializable;
    import java.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();
        }
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    # 5. 请求示例

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

    import com.springboot.micrometer.annotation.Idempotent;
    import com.springboot.micrometer.entity.Order;
    import com.springboot.micrometer.entity.RequestData;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.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";
        }
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    请求获取token值。

    图片

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

    图片

    第二次请求失败。

    图片

    聊聊大厂都怎么防止重复下单?
    SpringBoot 整合redis

    ← 聊聊大厂都怎么防止重复下单? SpringBoot 整合redis→

    最近更新
    01
    保姆级教程 用DeepSeek+飞书,批量写文案、写文章,太高效了
    06-06
    02
    还在为整理视频思维导图发愁?2 种超实用技巧,让你 10 分钟搞定,高效又省心!
    06-06
    03
    熬夜做PPT?AI一键生成高逼格幻灯片,效率提升10倍!
    06-06
    更多文章>
    Theme by Vdoing | Copyright © 2024-2025

        辽ICP备2023001503号-2

    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式