这篇写的太好了!Spring Boot + Redis 实现接口幂等性

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

[[351749]]

本文转载自微信公众号「小明菜市场」,可以通过以下二维码关注。转载本文请联系小明菜市场公众号。

介绍

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

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

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

实现过程

引入 maven 依赖

  1. <dependency> 
  2.            <groupId>org.springframework.boot</groupId> 
  3.            <artifactId>spring-boot-starter-data-redis</artifactId> 
  4.        </dependency> 

spring 配置文件写入

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

引入 Redis

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

  1. package cn.smallmartial.demo.utils; 
  2.  
  3. import org.springframework.beans.factory.annotation.Autowired; 
  4. import org.springframework.data.redis.core.RedisTemplate; 
  5. import org.springframework.data.redis.core.ValueOperations; 
  6. import org.springframework.stereotype.Component; 
  7.  
  8. import java.io.Serializable
  9. import java.util.Objects; 
  10. import java.util.concurrent.TimeUnit; 
  11.  
  12. /** 
  13.  * @Author smallmartial 
  14.  * @Date 2020/4/16 
  15.  * @Email smallmarital@qq.com 
  16.  */ 
  17. @Component 
  18. public class RedisUtil { 
  19.  
  20.     @Autowired 
  21.     private RedisTemplate redisTemplate; 
  22.  
  23.     /** 
  24.      * 写入缓存 
  25.      * 
  26.      * @param key 
  27.      * @param value 
  28.      * @return 
  29.      */ 
  30.     public boolean set(final String key, Object value) { 
  31.         boolean result = false
  32.         try { 
  33.             ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); 
  34.             operations.set(key, value); 
  35.             result = true
  36.         } catch (Exception e) { 
  37.             e.printStackTrace(); 
  38.         } 
  39.         return result; 
  40.     } 
  41.  
  42.     /** 
  43.      * 写入缓存设置时间 
  44.      * 
  45.      * @param key 
  46.      * @param value 
  47.      * @param expireTime 
  48.      * @return 
  49.      */ 
  50.     public boolean setEx(final String key, Object value, long expireTime) { 
  51.         boolean result = false
  52.         try { 
  53.             ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); 
  54.             operations.set(key, value); 
  55.             redisTemplate.expire(key, expireTime, TimeUnit.SECONDS); 
  56.             result = true
  57.         } catch (Exception e) { 
  58.             e.printStackTrace(); 
  59.         } 
  60.         return result; 
  61.     } 
  62.  
  63.     /** 
  64.      * 读取缓存 
  65.      * 
  66.      * @param key 
  67.      * @return 
  68.      */ 
  69.     public Object get(final String key) { 
  70.         Object result = null
  71.         ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); 
  72.         result = operations.get(key); 
  73.         return result; 
  74.     } 
  75.  
  76.     /** 
  77.      * 删除对应的value 
  78.      * 
  79.      * @param key 
  80.      */ 
  81.     public boolean remove(final String key) { 
  82.         if (exists(key)) { 
  83.             Boolean delete = redisTemplate.delete(key); 
  84.             return delete
  85.         } 
  86.         return false
  87.  
  88.     } 
  89.  
  90.     /** 
  91.      * 判断key是否存在 
  92.      * 
  93.      * @param key 
  94.      * @return 
  95.      */ 
  96.     public boolean exists(final String key) { 
  97.         boolean result = false
  98.         ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); 
  99.         if (Objects.nonNull(operations.get(key))) { 
  100.             result = true
  101.         } 
  102.         return result; 
  103.     } 
  104.  
  105.  

自定义注解

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

  1. @Target({ElementType.METHOD}) 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. public @interface AutoIdempotent { 
  4.    

token 的创建和实现

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

  1. public interface TokenService { 
  2.  
  3.     /** 
  4.      * 创建token 
  5.      * @return 
  6.      */ 
  7.     public  String createToken(); 
  8.  
  9.     /** 
  10.      * 检验token 
  11.      * @param request 
  12.      * @return 
  13.      */ 
  14.     public boolean checkToken(HttpServletRequest request) throws Exception; 
  15.  

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

  1. package cn.smallmartial.demo.service.impl; 
  2.  
  3. import cn.smallmartial.demo.bean.RedisKeyPrefix; 
  4. import cn.smallmartial.demo.bean.ResponseCode; 
  5. import cn.smallmartial.demo.exception.ApiResult; 
  6. import cn.smallmartial.demo.exception.BusinessException; 
  7. import cn.smallmartial.demo.service.TokenService; 
  8. import cn.smallmartial.demo.utils.RedisUtil; 
  9. import io.netty.util.internal.StringUtil; 
  10. import org.springframework.beans.factory.annotation.Autowired; 
  11. import org.springframework.stereotype.Service; 
  12. import org.springframework.util.StringUtils; 
  13.  
  14. import javax.servlet.http.HttpServletRequest; 
  15. import java.util.Random; 
  16. import java.util.UUID; 
  17.  
  18. /** 
  19.  * @Author smallmartial 
  20.  * @Date 2020/4/16 
  21.  * @Email smallmarital@qq.com 
  22.  */ 
  23. @Service 
  24. public class TokenServiceImpl implements TokenService { 
  25.     @Autowired 
  26.     private RedisUtil redisService; 
  27.  
  28.     /** 
  29.      * 创建token 
  30.      * 
  31.      * @return 
  32.      */ 
  33.     @Override 
  34.     public String createToken() { 
  35.         String str = UUID.randomUUID().toString().replace("-"""); 
  36.         StringBuilder token = new StringBuilder(); 
  37.         try { 
  38.             token.append(RedisKeyPrefix.TOKEN_PREFIX).append(str); 
  39.             redisService.setEx(token.toString(), token.toString(), 10000L); 
  40.             boolean empty = StringUtils.isEmpty(token.toString()); 
  41.             if (!empty) { 
  42.                 return token.toString(); 
  43.             } 
  44.         } catch (Exception ex) { 
  45.             ex.printStackTrace(); 
  46.         } 
  47.         return null
  48.     } 
  49.  
  50.     /** 
  51.      * 检验token 
  52.      * 
  53.      * @param request 
  54.      * @return 
  55.      */ 
  56.     @Override 
  57.     public boolean checkToken(HttpServletRequest request) throws Exception { 
  58.  
  59.         String token = request.getHeader(RedisKeyPrefix.TOKEN_NAME); 
  60.         if (StringUtils.isEmpty(token)) {// header中不存在token 
  61.             token = request.getParameter(RedisKeyPrefix.TOKEN_NAME); 
  62.             if (StringUtils.isEmpty(token)) {// parameter中也不存在token 
  63.                 throw new BusinessException(ApiResult.BADARGUMENT); 
  64.             } 
  65.         } 
  66.  
  67.         if (!redisService.exists(token)) { 
  68.             throw new BusinessException(ApiResult.REPETITIVE_OPERATION); 
  69.         } 
  70.  
  71.         boolean remove = redisService.remove(token); 
  72.         if (!remove) { 
  73.             throw new BusinessException(ApiResult.REPETITIVE_OPERATION); 
  74.         } 
  75.         return true
  76.     } 

拦截器的配置

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

  1. @Configuration 
  2. public class WebMvcConfiguration extends WebMvcConfigurationSupport { 
  3.  
  4.     @Bean 
  5.     public AuthInterceptor authInterceptor() { 
  6.         return new AuthInterceptor(); 
  7.     } 
  8.  
  9.     /** 
  10.      * 拦截器配置 
  11.      * 
  12.      * @param registry 
  13.      */ 
  14.     @Override 
  15.     public void addInterceptors(InterceptorRegistry registry) { 
  16.         registry.addInterceptor(authInterceptor()); 
  17. //                .addPathPatterns("/ksb/**"
  18. //                .excludePathPatterns("/ksb/auth/**""/api/common/**""/error""/api/*"); 
  19.         super.addInterceptors(registry); 
  20.     } 
  21.  
  22.     @Override 
  23.     public void addResourceHandlers(ResourceHandlerRegistry registry) { 
  24.         registry.addResourceHandler("/**").addResourceLocations( 
  25.                 "classpath:/static/"); 
  26.         registry.addResourceHandler("swagger-ui.html").addResourceLocations( 
  27.                 "classpath:/META-INF/resources/"); 
  28.         registry.addResourceHandler("/webjars/**").addResourceLocations( 
  29.                 "classpath:/META-INF/resources/webjars/"); 
  30.         super.addResourceHandlers(registry); 
  31.     } 
  32.  
  33.  

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

  1. @Slf4j 
  2. public class AuthInterceptor extends HandlerInterceptorAdapter { 
  3.  
  4.     @Autowired 
  5.     private TokenService tokenService; 
  6.  
  7.     @Override 
  8.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 
  9.  
  10.         if (!(handler instanceof HandlerMethod)) { 
  11.             return true
  12.         } 
  13.         HandlerMethod handlerMethod = (HandlerMethod) handler; 
  14.         Method method = handlerMethod.getMethod(); 
  15.         //被ApiIdempotment标记的扫描 
  16.         AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class); 
  17.         if (methodAnnotation != null) { 
  18.             try { 
  19.                 return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示 
  20.             } catch (Exception ex) { 
  21.                 throw new BusinessException(ApiResult.REPETITIVE_OPERATION); 
  22.             } 
  23.         } 
  24.         return true
  25.     } 
  26.  

测试用例

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

  1. /** 
  2.  * @Author smallmartial 
  3.  * @Date 2020/4/16 
  4.  * @Email smallmarital@qq.com 
  5.  */ 
  6. @RestController 
  7. public class BusinessController { 
  8.  
  9.  
  10.     @Autowired 
  11.     private TokenService tokenService; 
  12.  
  13.     @GetMapping("/get/token"
  14.     public Object  getToken(){ 
  15.         String token = tokenService.createToken(); 
  16.         return ResponseUtil.ok(token) ; 
  17.     } 
  18.  
  19.  
  20.     @AutoIdempotent 
  21.     @GetMapping("/test/Idempotence"
  22.     public Object testIdempotence() { 
  23.         String token = "接口幂等性测试"
  24.         return ResponseUtil.ok(token) ; 
  25.     } 

用浏览器进行访问

用获取到的token第一次访问

用获取到的token再次访问可以看到,第二次访问失败,即,幂等性验证通过。

关于作者

我是小小,双鱼座的程序猿,活在一线城市,我们下期再见。

 

责任编辑:武晓燕 来源: 小明菜市场
相关推荐

2021-10-22 14:50:23

Spring BootJava

2024-08-29 09:01:39

2021-06-25 09:47:59

Spring循环依赖Java

2020-05-07 10:05:58

Spring循环依赖Java

2020-07-29 10:40:21

Spring循环依赖Java

2020-09-09 09:55:36

JavaNIOBIO

2018-09-21 15:50:58

数据库MySQL分库分表

2020-06-18 11:58:13

苹果MacOS保密

2019-09-29 10:04:26

技术编程开发

2019-10-17 09:26:05

MySQL数据库InnoDB

2020-10-18 07:24:16

数字证书签名网络协议

2024-03-13 15:18:00

接口幂等性高并发

2021-10-27 09:55:55

Sharding-Jd分库分表Java

2021-01-18 14:34:59

幂等性接口客户端

2024-06-24 01:00:00

2024-11-01 09:28:02

2021-06-21 15:57:08

微服务架构数据

2020-07-15 08:14:12

高并发

2021-01-13 11:23:59

分布式幂等性支付

2023-03-07 08:19:16

接口幂等性SpringBoot
点赞
收藏

51CTO技术栈公众号