使用拦截器+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秒。

相关推荐
程序员-Benothing15 分钟前
MySQL 中的事务隔离级别有哪些?默认的事务隔离级别是什么?为什么选择这个级别?
数据库·mysql
lv__pf33 分钟前
【TL mysql 3】
数据库
xiaomici1 小时前
Datasphere的数据merge
数据库
小林ixn2 小时前
从零设计一个博客系统的数据库:表结构、索引与约束的实战思考
数据库·后端·mysql
Yang96112 小时前
一台顶三台:鼎讯DLJ-1在不同故障类型中的模式切换数据复盘
服务器·网络·数据库
潘正翔2 小时前
Memcached构建缓存服务器
运维·服务器·数据库·缓存·云原生·memcached
NineData3 小时前
NineData 与平凯数据库(TiDB 企业版)完成产品兼容性互认证
数据库·tidb·ninedata·玖章算术·平凯数据库·产品兼容性认证·tidb企业版
咖啡星人k3 小时前
2026 Text-to-SQL:一句话让 AI 写 SQL,自然语言直接查数据库(MonkeyCode 云端实战)
数据库·机器学习·语言模型·自然语言处理
GreatSQL社区3 小时前
少敲一个字母,GreatSQL 对所有查询“已读不回”
数据库·mysql·greatsql
Jul1en_3 小时前
【Java 脚手架】封装通用工具类-3
java·开发语言·redis·缓存·ai·bootstrap·rabbitmq