实现接口幂等性之使用Token解决方案

场景描述

用户在进行创建订单操作时,客户端发起创建订单请求到服务器,服务器需要处理创建订单逻辑并确保即使在高并发或者网络不稳定的情况下,同一笔订单不会被重复创建。

实现方案

我们将采用唯一标识(Token)法结合数据库的唯一索引来实现接口幂等性。

步骤1:客户端生成唯一标识

客户端在发起创建订单请求前,生成一个唯一的订单Token(例如UUID),并将这个Token作为请求的一部分发送到服务器。

package your.package;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 *
 * 关键因素1
 * 幂等唯一标识,就是客户端与服务端一次请求时的唯一标识,一般情况下由客户端来生成,也可以让第三方来统一分配。
 *
 * 关键因素2
 * 有了唯一标识以后,服务端只需要确保这个唯一标识只被使用一次即可,一种常见的方式就是利用数据库的唯一索引。
 */
@RestController
@RequestMapping("/generate")
public class IdGeneratorController {
    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @RequestMapping("/getIdGeneratorToken")
    @ResponseBody
    public String getIdGeneratorToken() {
        String generateId = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(generateId, generateId, 10, TimeUnit.SECONDS);
        return generateId;
    }

}
步骤2:创建自定义注解
package your.package;

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();

}
步骤3:创建请求对象
package your.package;


import lombok.Data;

@Data
public class RequestData<T> {

    private Header header;

    private T body;

}
package your.package;


import lombok.Data;

@Data
public class Header {

    private String token;
}

步骤4:使用AOP机制实现环绕通知
package your.package;


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.data.redis.core.RedisTemplate;
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 RedisTemplate<String, String> redisTemplate;

    @Pointcut("@annotation(your.package.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 (redisTemplate.delete(token)) {
            return joinPoint.proceed();
        }
        return "重复请求";
    }
}

package your.package;


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

步骤5:使用Postman测试效果
  • 生成唯一token
    在这里插入图片描述

  • 带上刚才生成的token创建订单,返回success
    在这里插入图片描述

  • 重复请求,返回重复请求
    在这里插入图片描述

步骤6:对于有效的订单进行落库操作,使用生成的Token作为唯一性约束,进行后续操作

总结

通过上述方案,即使在高并发的场景下,也能有效地保证接口的幂等性,避免重复处理导致的数据错误或资源浪费。

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值