百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

SpringBoot+Redis实现接口幂等性,就看这篇了

wptr33 2025-04-09 21:27 27 浏览

介绍

幂等性的概念是,任意多次执行所产生的影响都与一次执行产生的影响相同,按照这个含义,最终的解释是对数据库的影响只能是一次性的,不能重复处理。手段如下

  1. 数据库建立唯一索引
  2. token机制
  3. 悲观锁或者是乐观锁
  4. 先查询后判断

小小主要带你们介绍Redis实现自动幂等性。其原理如下图所示。

实现过程

引入 maven 依赖

     
            org.springframework.boot
            spring-boot-starter-data-redis
        

spring 配置文件写入

server.port=8080
core.datasource.druid.enabled=true
core.datasource.druid.url=jdbc:mysql://192.168.1.225:3306/?useUnicode=true&characterEncoding=UTF-8
core.datasource.druid.username=root
core.datasource.druid.password=
core.redis.enabled=true
spring.redis.host=192.168.1.225 #本机的redis地址
spring.redis.port=16379
spring.redis.database=3
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=5s
spring.redis.jedis.pool.min-idle=10

引入 Redis

引入 Spring boot 中的redis相关的stater,后面需要用到 Spring Boot 封装好的 RedisTemplate

package cn.smallmartial.demo.utils;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
 
import java.io.Serializable;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
 
/**
 * @Author smallmartial
 * @Date 2020/4/16
 * @Email smallmarital@qq.com
 */
@Component
public class RedisUtil {
 
    @Autowired
    private RedisTemplate redisTemplate;
 
    /**
     * 写入缓存
     *
     * @param key
     * @param value
     * @return
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations 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 operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
 
    /**
     * 读取缓存
     *
     * @param key
     * @return
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }
 
    /**
     * 删除对应的value
     *
     * @param key
     */
    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;
 
    }
 
    /**
     * 判断key是否存在
     *
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        boolean result = false;
        ValueOperations operations = redisTemplate.opsForValue();
        if (Objects.nonNull(operations.get(key))) {
            result = true;
        }
        return result;
    }
 
 
}

自定义注解

自定义一个注解,定义此注解的目的是把它添加到需要实现幂等的方法上,只要某个方法注解了其,都会自动实现幂等操作。其代码如下

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

token 的创建和实现

token 服务接口,我们新建一个接口,创建token服务,里面主要是有两个方法,一个用来创建 token,一个用来验证token

public interface TokenService {
 
    /**
     * 创建token
     * @return
     */
    public  String createToken();
 
    /**
     * 检验token
     * @param request
     * @return
     */
    public boolean checkToken(HttpServletRequest request) throws Exception;
 
}

token 的实现类,token中引用了服务的实现类,token引用了 redis 服务,创建token采用随机算法工具类生成随机 uuid 字符串,然后放入 redis 中,如果放入成功,返回token,校验方法就是从 header 中获取 token 的值,如果不存在,直接跑出异常,这个异常信息可以被直接拦截到,返回给前端。

package cn.smallmartial.demo.service.impl;
 
import cn.smallmartial.demo.bean.RedisKeyPrefix;
import cn.smallmartial.demo.bean.ResponseCode;
import cn.smallmartial.demo.exception.ApiResult;
import cn.smallmartial.demo.exception.BusinessException;
import cn.smallmartial.demo.service.TokenService;
import cn.smallmartial.demo.utils.RedisUtil;
import io.netty.util.internal.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
 
import javax.servlet.http.HttpServletRequest;
import java.util.Random;
import java.util.UUID;
 
/**
 * @Author smallmartial
 * @Date 2020/4/16
 * @Email smallmarital@qq.com
 */
@Service
public class TokenServiceImpl implements TokenService {
    @Autowired
    private RedisUtil redisService;
 
    /**
     * 创建token
     *
     * @return
     */
    @Override
    public String createToken() {
        String str = UUID.randomUUID().toString().replace("-", "");
        StringBuilder token = new StringBuilder();
        try {
            token.append(RedisKeyPrefix.TOKEN_PREFIX).append(str);
            redisService.setEx(token.toString(), token.toString(), 10000L);
            boolean empty = StringUtils.isEmpty(token.toString());
            if (!empty) {
                return token.toString();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
 
    /**
     * 检验token
     *
     * @param request
     * @return
     */
    @Override
    public boolean checkToken(HttpServletRequest request) throws Exception {
 
        String token = request.getHeader(RedisKeyPrefix.TOKEN_NAME);
        if (StringUtils.isEmpty(token)) {// header中不存在token
            token = request.getParameter(RedisKeyPrefix.TOKEN_NAME);
            if (StringUtils.isEmpty(token)) {// parameter中也不存在token
                throw new BusinessException(ApiResult.BADARGUMENT);
            }
        }
 
        if (!redisService.exists(token)) {
            throw new BusinessException(ApiResult.REPETITIVE_OPERATION);
        }
 
        boolean remove = redisService.remove(token);
        if (!remove) {
            throw new BusinessException(ApiResult.REPETITIVE_OPERATION);
        }
        return true;
    }
}

拦截器的配置

用于拦截前端的 token,判断前端的 token 是否有效

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
 
    @Bean
    public AuthInterceptor authInterceptor() {
        return new AuthInterceptor();
    }
 
    /**
     * 拦截器配置
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor());
//                .addPathPatterns("/ksb/**")
//                .excludePathPatterns("/ksb/auth/**", "/api/common/**", "/error", "/api/*");
        super.addInterceptors(registry);
    }
 
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations(
                "classpath:/static/");
        registry.addResourceHandler("swagger-ui.html").addResourceLocations(
                "classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars/");
        super.addResourceHandlers(registry);
    }
 
 
}

拦截处理器:主要用于拦截扫描到 Autoldempotent 到注解方法,然后调用 tokenService 的 checkToken 方法校验 token 是否正确,如果捕捉到异常就把异常信息渲染成 json 返回给前端。这部分代码主要和自定义注解部分挂钩。其主要代码如下所示

@Slf4j
public class AuthInterceptor extends HandlerInterceptorAdapter {
 
    @Autowired
    private TokenService tokenService;
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //被ApiIdempotment标记的扫描
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {
            try {
                return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
            } catch (Exception ex) {
                throw new BusinessException(ApiResult.REPETITIVE_OPERATION);
            }
        }
        return true;
    }
 
}

测试用例

这里进行相关的测试用例 模拟业务请求类,通过相关的路径获得相关的token,然后调用 testidempotence 方法,这个方法注解了 @Autoldempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用 TokenService 中的 checkToken() 方法,如果有异常会跑出,代码如下所示

/**
 * @Author smallmartial
 * @Date 2020/4/16
 * @Email smallmarital@qq.com
 */
@RestController
public class BusinessController {
 
 
    @Autowired
    private TokenService tokenService;
 
    @GetMapping("/get/token")
    public Object  getToken(){
        String token = tokenService.createToken();
        return ResponseUtil.ok(token) ;
    }
 
 
    @AutoIdempotent
    @GetMapping("/test/Idempotence")
    public Object testIdempotence() {
        String token = "接口幂等性测试";
        return ResponseUtil.ok(token) ;
    }
}

用浏览器进行访问

用获取到的token第一次访问

用获取到的token再次访问

可以看到,第二次访问失败,即,幂等性验证通过。

作者:___mySoul

原文链接:
https://blog.csdn.net/melovemingming/article/details/109665270

相关推荐

oracle数据导入导出_oracle数据导入导出工具

关于oracle的数据导入导出,这个功能的使用场景,一般是换服务环境,把原先的oracle数据导入到另外一台oracle数据库,或者导出备份使用。只不过oracle的导入导出命令不好记忆,稍稍有点复杂...

继续学习Python中的while true/break语句

上次讲到if语句的用法,大家在微信公众号问了小编很多问题,那么小编在这几种解决一下,1.else和elif是子模块,不能单独使用2.一个if语句中可以包括很多个elif语句,但结尾只能有一个...

python continue和break的区别_python中break语句和continue语句的区别

python中循环语句经常会使用continue和break,那么这2者的区别是?continue是跳出本次循环,进行下一次循环;break是跳出整个循环;例如:...

简单学Python——关键字6——break和continue

Python退出循环,有break语句和continue语句两种实现方式。break语句和continue语句的区别:break语句作用是终止循环。continue语句作用是跳出本轮循环,继续下一次循...

2-1,0基础学Python之 break退出循环、 continue继续循环 多重循

用for循环或者while循环时,如果要在循环体内直接退出循环,可以使用break语句。比如计算1至100的整数和,我们用while来实现:sum=0x=1whileTrue...

Python 中 break 和 continue 傻傻分不清

大家好啊,我是大田。...

python中的流程控制语句:continue、break 和 return使用方法

Python中,continue、break和return是控制流程的关键语句,用于在循环或函数中提前退出或跳过某些操作。它们的用途和区别如下:1.continue(跳过当前循环的剩余部分,进...

L017:continue和break - 教程文案

continue和break在Python中,continue和break是用于控制循环(如for和while)执行流程的关键字,它们的作用如下:1.continue:跳过当前迭代,...

作为前端开发者,你都经历过怎样的面试?

已经裸辞1个月了,最近开始投简历找工作,遇到各种各样的面试,今天分享一下。其实在职的时候也做过面试官,面试官时,感觉自己问的问题很难区分候选人的能力,最好的办法就是看看候选人的github上的代码仓库...

面试被问 const 是否不可变?这样回答才显功底

作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反...

2023金九银十必看前端面试题!2w字精品!

导文2023金九银十必看前端面试题!金九银十黄金期来了想要跳槽的小伙伴快来看啊CSS1.请解释CSS的盒模型是什么,并描述其组成部分。...

前端面试总结_前端面试题整理

记得当时大二的时候,看到实验室的学长学姐忙于各种春招,有些收获了大厂offer,有些还在苦苦面试,其实那时候的心里还蛮忐忑的,不知道自己大三的时候会是什么样的一个水平,所以从19年的寒假放完,大二下学...

由浅入深,66条JavaScript面试知识点(七)

作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061目录...

2024前端面试真题之—VUE篇_前端面试题vue2020及答案

添加图片注释,不超过140字(可选)...

今年最常见的前端面试题,你会做几道?

在面试或招聘前端开发人员时,期望、现实和需求之间总是存在着巨大差距。面试其实是一个交流想法的地方,挑战人们的思考方式,并客观地分析给定的问题。可以通过面试了解人们如何做出决策,了解一个人对技术和解决问...