使用拦截器+Redis实现接口幂等

文章目录

使用拦截器+Redis实现接口幂等

1.思路分析

接口幂等有很多种实现方式,拦截器/AOP+Redis,拦截器/AOP+本地缓存等等,本文讲解一下通过拦截器+Redis实现幂等的方式。

其原理就是在拦截器中拦截请求,然后根据一个标识符去redis中查询是否已经存在,如果存在,则说明当前请求正在处理,抛出异常告诉前端请勿重复请求。

标识符:一般可以使用token+methodType+uri作为标识符,具体业务具体分析。

2.具体实现

2.1 创建redis工具类

java 复制代码
import com.yunling.sys.config.exception.ParamValidateException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

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

/**
 * Redis工具类
 *
 * @author 谭永强
 * @date 2023-08-15
 */
@Component
public class RedisUtils {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

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


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


    /**
     * 判断缓存中是否有对应的value
     *
     * @param key 键
     * @return 成功/失败
     */
    public boolean exists(final String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    /**
     * 读取缓存
     *
     * @param key 键
     * @return 成功/失败
     */
    public Object get(final String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 删除对应的value
     *
     * @param key 键
     * @return 成功/失败
     */
    public boolean remove(final String key) {
        if (exists(key)) {
            return Boolean.TRUE.equals(redisTemplate.delete(key));
        }
        return false;
    }

    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于0)
     * @return 结果
     */
    public Long incr(String key, long delta) {
        if (ObjectUtils.isEmpty(key)) {
            throw new ParamValidateException("key值不能为空");
        }
        if (delta < 0) {
            throw new ParamValidateException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于0)
     * @return 结果
     */
    public Long decr(String key, long delta) {
        if (ObjectUtils.isEmpty(key)) {
            throw new ParamValidateException("key值不能为空");
        }
        if (delta < 0) {
            throw new ParamValidateException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
}

2.2 自定义幂等注解

自定义幂等注解,将seconds设置为该注解的属性,在拦截器中判断方法上是否有该注解,如果有该注解,则说明当前方法需要做幂等校验。

java 复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自动幂等
 * 该注解加在需要幂等的方法上,即可自动上线方法的幂等。
 *
 * @author 谭永强
 * @date 2023-08-15
 */

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {

    /**
     * 限定时间(秒)
     * 限制多少秒内,每个用户只能请求一次该接口。
     */
    long seconds() default 1;
}

2.2 自定义幂等拦截器

定义幂等接口用于拦截处理请求。

java 复制代码
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.common.utils.MD5Utils;
import com.yunling.sys.annotate.AutoIdempotent;
import com.yunling.sys.common.RedisUtils;
import com.yunling.sys.common.ResultData;
import com.yunling.sys.common.ReturnCode;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

/**
 * 自动幂等拦截器
 *
 * @author 谭永强
 * @date 2023-08-15
 */
@Component
public class AutoIdempotentInterceptor extends HandlerInterceptorAdapter {

    @Resource
    private RedisUtils redisUtils;

    /**
     * @param request  请求
     * @param response 响应
     * @param handler  处理
     * @return 结果
     * @throws Exception 异常
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断请求是否为方法的请求
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod method = (HandlerMethod) handler;
        //获取方法中是否有幂等性注解
        AutoIdempotent anno = method.getMethodAnnotation(AutoIdempotent.class);
        //若注解为空则直接返回
        if (Objects.isNull(anno)) {
            return true;
        }
        //限定时间
        long seconds = anno.seconds();
        //token
        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (Objects.isNull(token)) {
            ResultData<String> resultData = ResultData.fail(ReturnCode.ACCESS_DENIED.getCode(), "token不能为空");
            write(response, JSON.toJSONString(resultData));
            return false;
        }
        //此处转MD5的原因就是token长度太长了,转成md5短一些,此操作并不是必须的
        String md5 = MD5Utils.md5Hex(token, StandardCharsets.UTF_8.toString());
        //使用token+method+uri作为key值,此处需要通过key值确定请求的唯一性,也可以使用其他的组合,只要保证唯一性即可
        String key = md5 + ":" + request.getMethod() + ":" + request.getRequestURI();
        Object requestCountObj = redisUtils.get(key);
        if (!ObjectUtils.isEmpty(requestCountObj)) {
            //不为空,说明不是第一次请求,直接报错
            ResultData<String> resultData = ResultData.fail(ReturnCode.RC206.getCode(), "请求已提交,请勿重复请求");
            write(response, JSON.toJSONString(resultData));
            return false;
        }
        //若为空则为第一次请求
        return redisUtils.set(key, 1, seconds);
    }

    /**
     * 返回结果到前端
     *
     * @param response 响应
     * @param body     结果
     * @throws IOException 异常
     */
    private void write(HttpServletResponse response, String body) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ServletOutputStream os = response.getOutputStream();
        os.write(body.getBytes());
        os.flush();
        os.close();
    }
}

2.3 注入拦截器到容器

将拦截器注册到容器中。

java 复制代码
package com.yunling.sys.config;

import com.yunling.sys.config.interceptor.AutoIdempotentInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

/**
 * 将拦截器注入到容器中
 *
 * @author 谭永强
 * @date 2023-08-15
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Resource
    private AutoIdempotentInterceptor autoIdempotentInterceptor;

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

3.测试

java 复制代码
@RestController
@RequestMapping("user")
public class SysUserController {
    
  
    /**
     * 用户新增
     *
     * @param user 用户信息
     */
    @AutoIdempotent(seconds = 60)
    @PostMapping("add")
    public void add(@RequestBody SysUser user) {
       //业务代码.....
    }
}

请求该接口,如果在60s内再次请求,就会返回重复请求的结果。seconds具体值设置多少由该接口的实际响应时间为标准,默认值为1秒。

相关推荐
Channing Lewis24 分钟前
sql server如何创建表导入excel的数据
数据库·oracle·excel
秃头摸鱼侠25 分钟前
MySQL安装与配置
数据库·mysql·adb
UGOTNOSHOT30 分钟前
每日八股文6.3
数据库·sql
行云流水行云流水1 小时前
数据库、数据仓库、数据中台、数据湖相关概念
数据库·数据仓库
John Song1 小时前
Redis 集群批量删除key报错 CROSSSLOT Keys in request don‘t hash to the same slot
数据库·redis·哈希算法
IvanCodes1 小时前
七、Sqoop Job:简化与自动化数据迁移任务及免密执行
大数据·数据库·hadoop·sqoop
tonexuan1 小时前
MySQL 8.0 绿色版安装和配置过程
数据库·mysql
JohnYan1 小时前
工作笔记- 记一次MySQL数据移植表空间错误排除
数据库·后端·mysql
我最厉害。,。2 小时前
Windows权限提升篇&数据库篇&MYSQL&MSSQL&ORACLE&自动化项目
数据库·mysql·sqlserver
远方16092 小时前
20-Oracle 23 ai free Database Sharding-特性验证
数据库·人工智能·oracle