使用token机制实现接口幂等性校验

简介: 为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token: 如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示, 如果不存在, 说明参数不合法或者是重复请求, 返回提示即可。

接口的幂等性原则

1、接口调用存在的问题

现如今我们的系统大多拆分为分布式SOA,或者微服务,一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务,而服务调用服务无非就是使用RPC通信或者restful,既然是通信,那么就有可能在服务器处理完毕后返回结果的时候挂掉,这个时候用户端发现很久没有反应,那么就会多次点击按钮,这样请求有多次,那么处理数据的结果是否要统一呢?那是肯定的!尤其在支付场景。

2、什么是接口幂等性

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,这就没有保证接口的幂等性。

实现思路

为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token: 如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示, 如果不存在, 说明参数不合法或者是重复请求, 返回提示即可。

代码实现

pom.xml

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

RedisUtil


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @description
 */
@Component
public class RedisUtil {

    private static final Logger logger = LoggerFactory.getLogger(RedisUtil.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 设值
     * @param key
     * @param value
     * @return
     */
    public void set(String key, String value) {
        logger.info("set key:{} value:{}", key, value);
        stringRedisTemplate.opsForValue().set(key,value);
    }

    /**
     * 设值
     * @param key
     * @param value
     * @param expireTime 过期时间, 单位: s
     * @return
     */
    public void set(String key, String value, int expireTime) {
        logger.info("set key:{} value:{} expireTime:{}", key, value, expireTime);
        stringRedisTemplate.opsForValue().set(key,value, expireTime,TimeUnit.SECONDS);
    }

    /**
     * 取值
     * @param key
     * @return
     */
    public String get(String key) {
        logger.info("get key:{}", key);
        return stringRedisTemplate.opsForValue().get(key);
    }

    /**
     * 删除key
     * @param key
     * @return
     */
    public Boolean del(String key) {
        if (exists(key)) {
            return stringRedisTemplate.delete(key);
        } else {
            logger.error("del key:{}", key+" 不存在");
            return false;
        }

    }

    /**
     * 判断key是否存在
     * @param key
     * @return
     */
    public Boolean exists(String key) {
        Boolean exists = stringRedisTemplate.hasKey(key);
        logger.info("exists key:{} hasKey:{}", key, exists);
        return exists;
    }
}

自定义注解 @ApiIdempotent

package com.smartMap.media.common.apiIdempotent.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @description
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {

}

响应状态码 ResponseCode


/**
 * @description
 */
public enum ResponseCode {

    ILLEGAL_ARGUMENT(10000, "参数不合法"),
    REPETITIVE_OPERATION(10001, "请勿重复操作"),
    ;

    ResponseCode(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    private Integer code;

    private String msg;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

常量 Constant


/**
 * @description
 */
public class Constant {

    public interface Redis {
        String OK = "OK";
        Integer EXPIRE_TIME_MINUTE = 60;// 过期时间, 60s, 一分钟
        Integer EXPIRE_TIME_FIVE_MINUTE = 5 * 60;// 过期时间, 60s, 一分钟
        Integer EXPIRE_TIME_HOUR = 60 * 60;// 过期时间, 一小时
        Integer EXPIRE_TIME_DAY = 60 * 60 * 24;// 过期时间, 一天
        String TOKEN_PREFIX = "API_IDEMPOTENT_TOKEN:";
    }
}

token接口 ApiIdempotentTokenService


import com.smartMap.media.common.utils.R;

import javax.servlet.http.HttpServletRequest;

/**
 * @description
 */
public interface ApiIdempotentTokenService {

    R createToken();

    void checkToken(HttpServletRequest request);
}

token接口实现类 ApiIdempotentTokenServiceImpl

package com.smartMap.media.common.apiIdempotent.service.impl;

import com.smartMap.media.common.apiIdempotent.common.Constant;
import com.smartMap.media.common.apiIdempotent.common.ResponseCode;
import com.smartMap.media.common.apiIdempotent.service.ApiIdempotentTokenService;
import com.smartMap.media.common.apiIdempotent.utils.RedisUtil;
import com.smartMap.media.common.exception.RRException;
import com.smartMap.media.common.utils.R;
import com.smartMap.media.common.utils.UuidUtils;
import org.apache.commons.lang.text.StrBuilder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;

/**
 * @description
 */
@Service("apiIdempotentTokenService")
public class ApiIdempotentTokenServiceImpl implements ApiIdempotentTokenService {

    private static final String API_IDEMPOTENT_TOKEN_NAME = "apiIdempotentToken";

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public R createToken() {
        String str = UuidUtils.randomUUID();
        StrBuilder token = new StrBuilder();
        token.append(Constant.Redis.TOKEN_PREFIX).append(str);
        redisUtil.set(token.toString(), token.toString(), Constant.Redis.EXPIRE_TIME_FIVE_MINUTE);
        return R.ok().put("token",token.toString());
    }

    @Override
    public void checkToken(HttpServletRequest request) {
        String token = request.getHeader(API_IDEMPOTENT_TOKEN_NAME);
        // header中不存在token
        if (StringUtils.isBlank(token)) {
            token = request.getParameter(API_IDEMPOTENT_TOKEN_NAME);
            // parameter中也不存在token
            if (StringUtils.isBlank(token)) {
                throw new RRException(ResponseCode.ILLEGAL_ARGUMENT.getMsg(),ResponseCode.ILLEGAL_ARGUMENT.getCode());
            }
        }

        if (!redisUtil.exists(token)) {
            throw new RRException(ResponseCode.REPETITIVE_OPERATION.getMsg(),ResponseCode.REPETITIVE_OPERATION.getCode());
        }

        Boolean del = redisUtil.del(token);
        if (!del) {
            throw new RRException(ResponseCode.REPETITIVE_OPERATION.getMsg(),ResponseCode.REPETITIVE_OPERATION.getCode());
        }
    }
}

拦截器 ApiIdempotentInterceptor


import com.smartMap.media.common.apiIdempotent.annotation.ApiIdempotent;
import com.smartMap.media.common.apiIdempotent.service.ApiIdempotentTokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * @description
 */
@Component
public class ApiIdempotentInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private ApiIdempotentTokenService apiIdempotentTokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);
        if (methodAnnotation != null) {
            check(request);
        }

        return true;
    }

    private void check(HttpServletRequest request) {
        apiIdempotentTokenService.checkToken(request);
    }
}

拦截器注册 WebConfig


import com.smartMap.media.common.apiIdempotent.interceptor.ApiIdempotentInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @description
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private ApiIdempotentInterceptor apiIdempotentInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiIdempotentInterceptor);
    }
}

测试验证 controller


import com.smartMap.media.common.apiIdempotent.annotation.ApiIdempotent;
import com.smartMap.media.common.apiIdempotent.service.ApiIdempotentTokenService;
import com.smartMap.media.common.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @description
 */
@RestController
@RequestMapping("/mobile/test")
public class TestController {

    @Autowired
    private ApiIdempotentTokenService apiIdempotentTokenService;

    /**
     * 获取token
     * @return
     */
    @RequestMapping("getToken")
    public R getToken() {
        return apiIdempotentTokenService.createToken();
    }
    
    /**
     * 测试接口幂等性, 在需要幂等性校验的方法上声明此注解即可
     * @return
     */
    @ApiIdempotent
    @RequestMapping("testIdempotence")
    public R testIdempotence() {
        return R.ok("测试接口幂等性");
    }
}

1、获取token

image.png

2、查看redis

image.png

3、利用jmeter测试工具模拟200个并发请求, 将上一步获取到的token作为参数

image.png

image.png

image.png

4、header或参数均不传token, 或者token值为空, 或者token值乱填, 均无法通过校验, 如token值为"123456"

image.png

image.png

注意点(非常重要)

image.png

上图中, 不能单纯的直接删除token而不校验是否删除成功, 会出现并发安全性问题, 因为, 有可能多个线程同时走到第51行, 此时token还未被删除, 所以继续往下执行, 如果不校验redisUtil.del(token)的删除结果而直接放行, 那么还是会出现重复提交问题, 即使实际上只有一次真正的删除操作。

总结

通过拦截器加注解的方式,就不用写很多重复的代码啦。

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海

上一篇:手把手教你接入抖音小程序发送模板消息通知

相关文章
|
存储 SQL 安全
加密后的数据如何进行模糊查询?
在数据安全和隐私保护日益重要的今天,加密技术成为保护敏感数据的重要手段。然而,加密后的数据在存储和传输过程中虽然安全性得到了提升,但如何对这些数据进行高效查询,尤其是模糊查询,成为了一个挑战。本文将深入探讨如何在保证数据安全的前提下,实现加密数据的模糊查询功能。
2056 0
|
7月前
|
人工智能 Java 数据库
如何保证接口幂等性?
在分布式系统中,接口幂等性至关重要。本文详解其定义、重要性及实现方案,包括唯一索引、Token机制、分布式锁、状态机与版本号机制,并提供最佳实践建议,助你提升系统可靠性与用户体验。
1208 1
|
SQL 缓存 NoSQL
接口的幂等性设计和防重保证,详细分析幂等性的几种实现方法
本篇文章详细说明了幂等性,解释了什么是幂等性,幂等性的使用场景,讨论了幂等和防重的概念。分析了幂等性的情况以及如何设计幂等性服务。阐述了幂等性实现防重的几种策略,包括乐关锁,防重表,分布式锁,token令牌以及支付缓冲区。
8974 0
接口的幂等性设计和防重保证,详细分析幂等性的几种实现方法
|
4月前
|
消息中间件 Ubuntu Java
SpringBoot整合MQTT实战:基于EMQX实现双向设备通信
本教程指导在Ubuntu上部署EMQX 5.9.0并集成Spring Boot实现MQTT双向通信,涵盖服务器搭建、客户端配置及生产实践,助您快速构建企业级物联网消息系统。
1705 1
|
6月前
|
存储 安全 Java
synchronized 原理
`synchronized` 是 Java 中实现线程同步的关键字,通过对象头中的 Monitor 和锁机制确保同一时间只有一个线程执行同步代码。其底层依赖 Mark Word 和 Monitor 控制锁状态,支持偏向锁、轻量级锁和重量级锁的升级过程,以优化性能。同步方法和同步块在实现方式上有所不同,前者通过 `ACC_SYNCHRONIZED` 标志隐式加锁,后者通过 `monitorenter` 和 `monitorexit` 指令显式控制。此外,`synchronized` 还保证内存可见性和 Happens-Before 关系,使共享变量在多线程间正确同步。
586 0
|
canal 缓存 NoSQL
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
根据对一致性的要求程度,提出多种解决方案:同步删除、同步删除+可靠消息、延时双删、异步监听+可靠消息、多重保障方案
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
|
Prometheus 监控 Cloud Native
在 Java 中,如何使用线程池监控以及动态调整线程池?
【10月更文挑战第22天】线程池的监控和动态调整是一项重要的任务,需要我们结合具体的应用场景和需求,选择合适的方法和策略,以确保线程池始终处于最优状态,提高系统的性能和稳定性。
2231 2
|
NoSQL 算法 Java
Java Redis多限流
通过本文的介绍,我们详细讲解了如何在Java中使用Redis实现三种不同的限流策略:固定窗口限流、滑动窗口限流和令牌桶算法。每种限流策略都有其适用的场景和特点,根据具体需求选择合适的限流策略可以有效保护系统资源和提高服务的稳定性。
302 18
|
监控 NoSQL Java
在Spring Boot中集成Redisson实现延迟队列
在Spring Boot中集成Redisson实现延迟队列
950 6
|
Java 数据库 微服务
使用OpenFeign进行服务调用
本文档介绍如何在微服务架构中使用Spring Cloud的OpenFeign进行服务间的远程调用。首先,需在项目中引入OpenFeign及其负载均衡器依赖。接着,通过`@EnableFeignClients`启用Feign客户端功能,并定义客户端接口以声明远程服务调用逻辑。为确保启动类能正确扫描到这些接口,需指定`basePackages`属性。最后,演示了如何在购物车服务中利用Feign客户端接口调用商品服务,以实现跨服务的数据整合和查询。Feign通过动态代理机制简化了HTTP请求的发起过程,使服务间交互更为直观和便捷。
496 0

热门文章

最新文章