接口幂等性校验(使用拦截器+自定义注解+redis解决问题)

概念:

幂等性,通俗的说就是一个接口,多次发起同一个请求,必须保证操作只能执行一次

比如:

  • 订单接口,不能多次创建订单
  • 支付接口,重复支付同一笔订单只能扣一次钱
  • 支付宝回调接口,可能会多次回调,必须处理重复回调
  • 普通表单提交接口,因为网络超时等原因多次点击提交,只能成功一次

等等…

常见解决方案有:

  1. 唯一索引 – 防止新增脏数据

  2. token机制 – 防止页面重复提交

  3. 悲观锁 – 获取数据的时候加锁(锁表或锁行)

  4. 乐观锁 – 基于版本号version实现,在更新数据那一刻校验数据

  5. 分布式锁 – redis(jedis、redisson)或zookeeper实现

    等等

流程:

  1. 当页面加载的时候通过接口获取token(UUID)
  2. 当访问接口时,会经过拦截器,如果发现该接口有自定义的幂等检查注解,说明该接口需要验证幂等性
  3. 查看请求头里是否有key=token的值,如果有,并且删除成功,那么接口就访问成功,否则为重复提交
  4. 如果发现该接口没有自定义的幂等检查注解,则直接放行

代码

自定义注解 MidengCheck
  • 自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。
  • 后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等
  • 使用元注解ElementType.METHOD表示它只能放在方法上,RetentionPolicy.RUNTIME表示它在运行时。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解做接口的幂等校验 annotation
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MidengCheck {
}
service层 redis工具类 RedisService
/**
 * Redis工具服务类
 */
@Component
public class RedisService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 写入缓存
     * @param key
     * @param value
     * @return
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }


    /**
     * 写入缓存设置时效时间
     * @param key
     * @param value
     * @param expireTime
     * @return
     */
    public boolean setEx(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }


    /**
     * 根据key判断缓存中是否有对应的value
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 根据key读取缓存
     * @param key
     * @return
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }

    /**
     * 根据key删除对应的value
     * @param key
     * @return
     */
    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;
    }
}
token创建和检验 TokenService
  1. token引用了redis服务,创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,最后返回这个token值。
  2. checkToken方法就是从header中获取token到值,如若不存在,直接抛出异常(这个异常信息可以被拦截器捕捉到,然后返回给前端),如存在,则查询Redis看是否存在,不存在也抛出异常,Redis存在,那么token校验通过。

/**
 * 获取和校验Token
 */
@Component
public class TokenService {

    @Autowired
    private RedisService redisService;

    //获取token
    public String getToken() {
        String uuid = UUID.randomUUID().toString();
        //存入Redis的key添加一个统一前缀字符串
        String token = "mideng_check_prefix:" + uuid;
        //存入Redis
        boolean result = redisService.setEx(token, uuid, 10000L);
        if(result){
            return token;
        }
        return null;
    }

    public boolean checkToken(HttpServletRequest request) throws Exception {
        //从请求头中获取token的值
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
            //请求头中不存在token,那就是非法请求,直接抛出异常
            throw new Exception("Illegal request");
        }
        if (!redisService.exists(token)) {
            //请求头中存在token,但是Redis中不存在的话,也抛出异常
            throw new Exception("token error");
        }
        //代码执行到这里,说明token校验成功了,那么就需要删除Redis中的值
        boolean remove = redisService.remove(token);
        if (!remove) {
            //删除失败了
            throw new Exception("token delete error");
        }
        return true;
    }
}

拦截器的配置

web配置类,实现WebMvcConfigurationSupport,主要作用就是添加CheckIdempotentInterceptor拦截器到配置类中,这样我们写的拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中

import org.colin.interceptor.CheckIdempotentInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import javax.annotation.Resource;

/**
 * @ClassName: WebConfiguration
 * @description: 统一拦截器配置类
 * @Version 1.1.0.1
 */
@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {

    @Resource
    private CheckIdempotentInterceptor checkIdempotentInterceptor;

    //添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //checkIdempotentInterceptor拦截器只对 /saveUser 请求拦截
        registry.addInterceptor(checkIdempotentInterceptor).addPathPatterns("/saveUser");
        super.addInterceptors(registry);
    }
}

下面我们就开始写拦截处理器,主要的功能是拦截到被CheckIdempotent注解的方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息返回给前端。

/**
 * 幂等校验的拦截器
 */
@Component
public class MidengCheckInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    /**
     * 前置处理
     * 该方法将在请求处理之前进行调用
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        //handler instanceof HandlerMethod:作用:判断请求是否是请求的方法(有些请求是请求的静态资源)
        if (!(handler instanceof HandlerMethod)) {
            //直接放行:放行非方法的请求
            return true;
        }
        //代码运行到此处,说明请求的资源就是方法了
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        //handlerMethod.getMethod()   获取到请求的方法对象
        Method method = handlerMethod.getMethod();

        //获取请求方法上面的 MidengCheck 注解
        MidengCheck methodAnnotation = method.getAnnotation(MidengCheck.class);
        if (methodAnnotation != null) {
            //进到此处,表示请求的方法上面打了 MidengCheck 注解
            //此时我就需要做接口幂等性校验了
            try {
                return tokenService.checkToken(request);
            }catch (Exception ex){
                writeJson(response, ex.getMessage());
                return false;
            }
        }
        //必须返回true,否则会拦截掉所有请求,不会执行controller方法中的内容了
        return true;
    }

    /**
     * 返回提示信息给前端
     */
    private void writeJson(HttpServletResponse response, String message){
        response.reset();
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=utf-8");
        response.setStatus(404);
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.print(message);
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

启动类
@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class);
    }
}
yml配置文件
server:
  port: 1010
spring:
  #redis配置
  redis:
    #使用第几个数据库(0-15)
    database: 0
    host: 127.0.0.1
    port: 6379
    #password: 123456 #密码
    timeout: 5000
测试类
@RestController
@Slf4j
public class TestController {

    @Autowired
    private TokenService tokenService;

    //获取token值
    @GetMapping("/getToken")
    public String getToken(){
        return tokenService.getToken();
    }

    //保存用户信息
    @PostMapping("/saveUser")
    @MidengCheck
    public String saveUser(){
        //我们希望的情况是:打印一次
        log.info("----------------------------保存用户信息成功----------------------------");
        //保存用户信息的业务逻辑代码-省略....
        return "add user success";
    }

}
测试

1.浏览器输入 http://localhost:1100/getToken 获取token:mideng_check_prefix:7c29f136-9b39-4786-a15d-509428681364
2.使用Apache JMeter 测压工具 请求一百次
发现只会成功一次
在这里插入图片描述

总结:

  1. 编写自定义注解,不需要参数
  2. 编写拦截器,作用:做接口幂等性校验
  • 不是请求方法的请求,全部放行
  • 请求方法的这些请求中,需要做过滤,过滤出包含自定义注解的方法,其余的方法全部放行
  • 获取请求头中的token值,然后做非空判断、redis是否存在判断,根据该token删除Redis的值:如果删除成功,放行,如果删除失败,那么就抛出异常,不放行。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值