一、前言

接口幂等就是对一个接口执行重复的多次请求,与一次请求所产生的结果是相同的。对数据库的查询和删除是天然幂等的,更新操作在大多数场景下也是天然幂等。插入大多数情况下都是非幂等的,除非利用数据库的唯一索引来保证数据不会重复保存。

二、为什么需要幂等

1.超时重试

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

2.异步回调

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

3.消息队列

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

三、实现幂等的关键因素

关键因素1

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

关键因素2

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

四、代码实现

1.添加依赖

<!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
 <!--aop-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

2.自定义注解

package com.example.aopdemo.annotation;

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 {

    /**
     * 参数名
     */
    String name() default "";

    /**
     * 属性值
     */
    String field() default "";

    /**
     * 参数类型
     */
    Class type();

}

3.统一请求入参对象

package com.example.aopdemo.request;

import lombok.Getter;
import lombok.Setter;

/**
 * @author qx
 * @date 2024/3/7
 * @des 请求实体
 */
@Getter
@Setter
public class RequestData<T> {

    private String token;

    private T body;
}

4.业务服务类

package com.example.aopdemo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.io.Serializable;
import java.util.concurrent.TimeUnit;

/**
 * @author qx
 * @date 2024/3/7
 * @des 订单服务类
 */
@Service
public class OrderService {

    @Autowired
    private RedisTemplate<String, Serializable> redisTemplate;

    public void save(String idempotentId) {
        redisTemplate.opsForValue().set(idempotentId, idempotentId, 10, TimeUnit.MINUTES);
    }

    public boolean delete(String idempotentId) {
        return redisTemplate.delete(idempotentId);
    }
}

5.工具类

package com.example.aopdemo.util;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.CodeSignature;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/**
 * @author qx
 * @date 2024/3/7
 * @des
 */
public class AopUtil {

    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;
    }

}
package com.example.aopdemo.util;

import java.util.UUID;

/**
 * @author qx
 * @date 2024/3/7
 * @des
 */
public class IdGeneratorUtil {

    /**
     * 生成唯一标识ID
     */
    public static String generatedId() {
        return UUID.randomUUID().toString();
    }
}

6.Aop处理类

package com.example.aopdemo.aspect;

import com.example.aopdemo.annotation.Idempotent;
import com.example.aopdemo.request.RequestData;
import com.example.aopdemo.service.OrderService;
import com.example.aopdemo.util.AopUtil;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Map;

/**
 * @author qx
 * @date 2024/3/7
 * @des Aop处理
 */
@Aspect
@Component
public class IdempotentAspect {

    @Autowired
    private OrderService orderService;

    @Pointcut("@annotation(com.example.aopdemo.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 = AopUtil.getParamValue(joinPoint);
        if (object instanceof RequestData) {
            RequestData idempotentEntity = (RequestData) paramValue.get(name);
            token = idempotentEntity.getToken();
        }

        if (orderService.delete(token)) {
            return joinPoint.proceed();
        }
        return "重复请求";
    }
}

7.控制层

package com.example.aopdemo.controller;

import com.example.aopdemo.service.OrderService;
import com.example.aopdemo.util.IdGeneratorUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author qx
 * @date 2024/3/7
 * @des
 */
@RestController
@RequestMapping("/idGenerator")
public class IdGeneratorController {

    @Autowired
    private OrderService orderService;

    @RequestMapping("/getIdGeneratorToken")
    public String getIdGeneratorToken() {
        String generateId = IdGeneratorUtil.generatedId();
        orderService.save(generateId);
        return generateId;
    }
}
package com.example.aopdemo.controller;

import com.example.aopdemo.annotation.Idempotent;
import com.example.aopdemo.request.RequestData;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author qx
 * @date 2024/3/7
 * @des
 */
@RestController
@RequestMapping("/order")
public class OrderController {

    @RequestMapping("/saveOrder")
    @Idempotent(name = "requestData", type = RequestData.class, field = "token")
    public String saveOrder(@RequestBody RequestData<String> requestData) {
        return "success";
    }
    
}

8.测试

启动程序,先调用生成token的接口。

SpringBoot自定义注解实现接口幂等_接口幂等

第一次请求成功。

SpringBoot自定义注解实现接口幂等_SpringBoot_02

第二次请求提示重复请求。

SpringBoot自定义注解实现接口幂等_SpringBoot_03