当前位置:   article > 正文

SpringBoot中接口幂等性实现方案-自定义注解+Redis+拦截器实现防止订单重复提交_接口幂等性,防止重复提交

接口幂等性,防止重复提交

场景

SpringBoot+Redis+自定义注解实现接口防刷(限制不同接口单位时间内最大请求次数):

SpringBoot+Redis+自定义注解实现接口防刷(限制不同接口单位时间内最大请求次数)_redis防刷_霸道流氓气质的博客-CSDN博客

以下接口幂等性的实现方式与上面博客类似,可参考。

接口幂等性

什么是幂等性?

幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后

会和其作用一次的结果相同。在计算机中编程中,一个幂等操作的特点是其任意多次执行所

产生的影响均与一次执行的影响相同。幂等函数或幂等方法是指可以使用相同参数重复执行,

并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

什么是接口幂等性?

在HTTP/1.1中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有

同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求

都不会再对资源产生副作用。这里的副作用是不会对结果产生破坏或者产生不可预料的结果。

也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。

为什么需要实现幂等性?

在接口调用时一般情况下都能正常返回信息不会重复提交,不过在遇见以下情况时可以就会出现问题,

如:

前端重复提交表单:

在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,

致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。

用户恶意进行刷单:

例如在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到

用户重复提交的投票信息,这样会使投票结果与事实严重不符。

接口超时重复提交:

很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络

波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。

消息进行重复消费:

当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。

使用幂等性最大的优势在于使接口保证任何幂等性操作,免去因重试等造成系统产生的未知的问题。

实现接口幂等性的方案有很多,下面记录一种自定义注解加拦截器和redis的实现方式模拟防止订单重复提交。

注:

博客:
霸道流氓气质_C#,架构之路,SpringBoot-CSDN博客

实现

1、新建SpringBoot项目并添加必要的依赖

  1.     <dependencies>
  2.         <dependency>
  3.             <groupId>org.springframework.boot</groupId>
  4.             <artifactId>spring-boot-starter-web</artifactId>
  5.         </dependency>
  6.         <dependency>
  7.             <groupId>com.mysql</groupId>
  8.             <artifactId>mysql-connector-j</artifactId>
  9.             <scope>runtime</scope>
  10.         </dependency>
  11.         <dependency>
  12.             <groupId>org.projectlombok</groupId>
  13.             <artifactId>lombok</artifactId>
  14.             <optional>true</optional>
  15.         </dependency>
  16.         <dependency>
  17.             <groupId>org.springframework.boot</groupId>
  18.             <artifactId>spring-boot-starter-test</artifactId>
  19.             <scope>test</scope>
  20.         </dependency>
  21.         <!--MySQL驱动-->
  22.         <dependency>
  23.             <groupId>mysql</groupId>
  24.             <artifactId>mysql-connector-java</artifactId>
  25.         </dependency>
  26.         <!--MyBatis整合SpringBoot框架的起步依赖-->
  27.         <dependency>
  28.             <groupId>org.mybatis.spring.boot</groupId>
  29.             <artifactId>mybatis-spring-boot-starter</artifactId>
  30.             <version>2.2.1</version>
  31.         </dependency>
  32.         <!-- redis 缓存操作 -->
  33.         <dependency>
  34.             <groupId>org.springframework.boot</groupId>
  35.             <artifactId>spring-boot-starter-data-redis</artifactId>
  36.         </dependency>
  37.         <!-- 阿里JSON解析器 -->
  38.         <dependency>
  39.             <groupId>com.alibaba</groupId>
  40.             <artifactId>fastjson</artifactId>
  41.             <version>1.2.75</version>
  42.         </dependency>
  43.     </dependencies>

2、修改配置文件yml添加相关配置

  1. # 开发环境配置
  2. server:
  3.   # 服务器的HTTP端口,默认为8080
  4.   port: 996
  5.   servlet:
  6.     # 应用的访问路径
  7.     context-path: /
  8.   tomcat:
  9.     # tomcat的URI编码
  10.     uri-encoding: UTF-8
  11.     # tomcat最大线程数,默认为200
  12.     max-threads: 800
  13.     # Tomcat启动初始化的线程数,默认值25
  14.     min-spare-threads: 30
  15. # 数据源
  16. spring:
  17.   application:
  18.     name: badao-tcp-demo
  19.   datasource:
  20.    url:jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
  21.     username: root
  22.     password: 123456
  23.     driver-class-name: com.mysql.cj.jdbc.Driver
  24.     dbcp2:
  25.       min-idle: 5                                # 数据库连接池的最小维持连接数
  26.       initial-size: 5                            # 初始化连接数
  27.       max-total: 5                               # 最大连接数
  28.       max-wait-millis: 150                       # 等待连接获取的最大超时时间
  29.   # redis 配置
  30.   redis:
  31.     # 地址
  32.     #本地测试用
  33.     host: 127.0.0.1
  34.     port: 6379
  35.     password: 123456
  36.     # 连接超时时间
  37.     timeout: 10s
  38.     lettuce:
  39.       pool:
  40.         # 连接池中的最小空闲连接
  41.         min-idle: 0
  42.         # 连接池中的最大空闲连接
  43.         max-idle: 8
  44.         # 连接池的最大数据库连接数
  45.         max-active: 8
  46.         # #连接池最大阻塞等待时间(使用负值表示没有限制)
  47.         max-wait: -1ms
  48. # mybatis配置
  49. mybatis:
  50.   mapper-locations: classpath:mapper/*.xml    # mapper映射文件位置
  51.   type-aliases-package: com.badao.demo.entity    # 实体类所在的位置
  52.   configuration:
  53.     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl   #用于控制台打印sql语句


3、新建Redis的两个配置类

RedisConfig:

  1. import org.springframework.cache.annotation.CachingConfigurerSupport;
  2. import org.springframework.cache.annotation.EnableCaching;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.context.annotation.Primary;
  6. import org.springframework.data.redis.connection.RedisConnectionFactory;
  7. import org.springframework.data.redis.core.RedisTemplate;
  8. import org.springframework.data.redis.serializer.StringRedisSerializer;
  9. import com.fasterxml.jackson.annotation.JsonAutoDetect;
  10. import com.fasterxml.jackson.annotation.JsonTypeInfo;
  11. import com.fasterxml.jackson.annotation.PropertyAccessor;
  12. import com.fasterxml.jackson.databind.ObjectMapper;
  13. import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
  14. /**
  15.  * redis配置
  16.  *
  17.  */
  18. @Configuration
  19. @EnableCaching
  20. public class RedisConfig extends CachingConfigurerSupport
  21. {
  22.     @Bean
  23.     @SuppressWarnings(value = { "unchecked", "rawtypes" })
  24.     @Primary
  25.     public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
  26.     {
  27.         RedisTemplate<Object, Object> template = new RedisTemplate<>();
  28.         template.setConnectionFactory(connectionFactory);
  29.         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
  30.         ObjectMapper mapper = new ObjectMapper();
  31.         mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  32.         mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
  33.         serializer.setObjectMapper(mapper);
  34.         template.setValueSerializer(serializer);
  35.         // 使用StringRedisSerializer来序列化和反序列化redis的key
  36.         template.setKeySerializer(new StringRedisSerializer());
  37.         template.afterPropertiesSet();
  38.         return template;
  39.     }
  40. }

FastJson2JsonRedisSerializer:

  1. import com.alibaba.fastjson.JSON;
  2. import com.alibaba.fastjson.parser.ParserConfig;
  3. import com.alibaba.fastjson.serializer.SerializerFeature;
  4. import com.fasterxml.jackson.databind.JavaType;
  5. import com.fasterxml.jackson.databind.ObjectMapper;
  6. import com.fasterxml.jackson.databind.type.TypeFactory;
  7. import org.springframework.data.redis.serializer.RedisSerializer;
  8. import org.springframework.data.redis.serializer.SerializationException;
  9. import org.springframework.util.Assert;
  10. import java.nio.charset.Charset;
  11. /**
  12.  * Redis使用FastJson序列化
  13.  *
  14.  */
  15. public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
  16. {
  17.     @SuppressWarnings("unused")
  18.     private ObjectMapper objectMapper = new ObjectMapper();
  19.     public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
  20.     private Class<T> clazz;
  21.     static
  22.     {
  23.         ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
  24.     }
  25.     public FastJson2JsonRedisSerializer(Class<T> clazz)
  26.     {
  27.         super();
  28.         this.clazz = clazz;
  29.     }
  30.     @Override
  31.     public byte[] serialize(T t) throws SerializationException
  32.     {
  33.         if (t == null)
  34.         {
  35.             return new byte[0];
  36.         }
  37.         return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
  38.     }
  39.     @Override
  40.     public T deserialize(byte[] bytes) throws SerializationException
  41.     {
  42.         if (bytes == null || bytes.length <= 0)
  43.         {
  44.             return null;
  45.         }
  46.         String str = new String(bytes, DEFAULT_CHARSET);
  47.         return JSON.parseObject(str, clazz);
  48.     }
  49.     public void setObjectMapper(ObjectMapper objectMapper)
  50.     {
  51.         Assert.notNull(objectMapper, "'objectMapper' must not be null");
  52.         this.objectMapper = objectMapper;
  53.     }
  54.     protected JavaType getJavaType(Class<?> clazz)
  55.     {
  56.         return TypeFactory.defaultInstance().constructType(clazz);
  57.     }
  58. }

4、新建Redis工具类

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.data.redis.core.*;
  3. import org.springframework.stereotype.Component;
  4. import java.util.*;
  5. import java.util.concurrent.TimeUnit;
  6. /**
  7.  * spring redis 工具类
  8.  *
  9.  **/
  10. @SuppressWarnings(value = { "unchecked", "rawtypes" })
  11. @Component
  12. public class RedisCache
  13. {
  14.     @Autowired
  15.     public RedisTemplate redisTemplate;
  16.     @Autowired
  17.     public StringRedisTemplate stringRedisTemplate;
  18.     /**
  19.      * 缓存基本的对象,Integer、String、实体类等
  20.      *
  21.      * @param key 缓存的键值
  22.      * @param value 缓存的值
  23.      * @return 缓存的对象
  24.      */
  25.     public <T> ValueOperations<String, T> setCacheObject(String key, T value)
  26.     {
  27.         ValueOperations<String, T> operation = redisTemplate.opsForValue();
  28.         operation.set(key, value);
  29.         return operation;
  30.     }
  31.     /**
  32.      * 缓存基本的对象,Integer、String、实体类等
  33.      *
  34.      * @param key 缓存的键值
  35.      * @param value 缓存的值
  36.      * @param timeout 时间
  37.      * @param timeUnit 时间颗粒度
  38.      * @return 缓存的对象
  39.      */
  40.     public <T> ValueOperations<String, T> setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit)
  41.     {
  42.         ValueOperations<String, T> operation = redisTemplate.opsForValue();
  43.         operation.set(key, value, timeout, timeUnit);
  44.         return operation;
  45.     }
  46.     /**
  47.      * 获得缓存的基本对象。
  48.      *
  49.      * @param key 缓存键值
  50.      * @return 缓存键值对应的数据
  51.      */
  52.     public <T> T getCacheObject(String key)
  53.     {
  54.         ValueOperations<String, T> operation = redisTemplate.opsForValue();
  55.         return operation.get(key);
  56.     }
  57.     /**
  58.      * 删除单个对象
  59.      *
  60.      * @param key
  61.      */
  62.     public void deleteObject(String key)
  63.     {
  64.         redisTemplate.delete(key);
  65.     }
  66.     /**
  67.      * 删除集合对象
  68.      *
  69.      * @param collection
  70.      */
  71.     public void deleteObject(Collection collection)
  72.     {
  73.         redisTemplate.delete(collection);
  74.     }
  75.     /**
  76.      * 缓存List数据
  77.      *
  78.      * @param key 缓存的键值
  79.      * @param dataList 待缓存的List数据
  80.      * @return 缓存的对象
  81.      */
  82.     public <T> ListOperations<String, T> setCacheList(String key, List<T> dataList)
  83.     {
  84.         ListOperations listOperation = redisTemplate.opsForList();
  85.         if (null != dataList)
  86.         {
  87.             int size = dataList.size();
  88.             for (int i = 0; i < size; i++)
  89.             {
  90.                 listOperation.leftPush(key, dataList.get(i));
  91.             }
  92.         }
  93.         return listOperation;
  94.     }
  95.     /**
  96.      * 获得缓存的list对象
  97.      *
  98.      * @param key 缓存的键值
  99.      * @return 缓存键值对应的数据
  100.      */
  101.     public <T> List<T> getCacheList(String key)
  102.     {
  103.         List<T> dataList = new ArrayList<T>();
  104.         ListOperations<String, T> listOperation = redisTemplate.opsForList();
  105.         Long size = listOperation.size(key);
  106.         for (int i = 0; i < size; i++)
  107.         {
  108.             dataList.add(listOperation.index(key, i));
  109.         }
  110.         return dataList;
  111.     }
  112.     /**
  113.      * 缓存Set
  114.      *
  115.      * @param key 缓存键值
  116.      * @param dataSet 缓存的数据
  117.      * @return 缓存数据的对象
  118.      */
  119.     public <T> BoundSetOperations<String, T> setCacheSet(String key, Set<T> dataSet)
  120.     {
  121.         BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
  122.         Iterator<T> it = dataSet.iterator();
  123.         while (it.hasNext())
  124.         {
  125.             setOperation.add(it.next());
  126.         }
  127.         return setOperation;
  128.     }
  129.     /**
  130.      * 获得缓存的set
  131.      *
  132.      * @param key
  133.      * @return
  134.      */
  135.     public <T> Set<T> getCacheSet(String key)
  136.     {
  137.         Set<T> dataSet = new HashSet<T>();
  138.         BoundSetOperations<String, T> operation = redisTemplate.boundSetOps(key);
  139.         dataSet = operation.members();
  140.         return dataSet;
  141.     }
  142.     /**
  143.      * 缓存Map
  144.      *
  145.      * @param key
  146.      * @param dataMap
  147.      * @return
  148.      */
  149.     public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap)
  150.     {
  151.         HashOperations hashOperations = redisTemplate.opsForHash();
  152.         if (null != dataMap)
  153.         {
  154.             for (Map.Entry<String, T> entry : dataMap.entrySet())
  155.             {
  156.                 hashOperations.put(key, entry.getKey(), entry.getValue());
  157.             }
  158.         }
  159.         return hashOperations;
  160.     }
  161.     /**
  162.      * 获得缓存的Map
  163.      *
  164.      * @param key
  165.      * @return
  166.      */
  167.     public <T> Map<String, T> getCacheMap(String key)
  168.     {
  169.         Map<String, T> map = redisTemplate.opsForHash().entries(key);
  170.         return map;
  171.     }
  172.     /**
  173.      * 获得缓存的基本对象列表
  174.      *
  175.      * @param pattern 字符串前缀
  176.      * @return 对象列表
  177.      */
  178.     public Collection<String> keys(String pattern)
  179.     {
  180.         return redisTemplate.keys(pattern);
  181.     }
  182.     /**
  183.      * 如果redis中不存在则存储进redis
  184.      * @return
  185.      */
  186.     public Boolean setIfAbsent(Object key,Object value,long timeout){
  187.         return redisTemplate.opsForValue().setIfAbsent(key, value,timeout, TimeUnit.SECONDS);
  188.     }
  189. }

5、新建自定义注解类Idempotent

  1. import java.lang.annotation.ElementType;
  2. import java.lang.annotation.Retention;
  3. import java.lang.annotation.RetentionPolicy;
  4. import java.lang.annotation.Target;
  5. @Target(ElementType.METHOD)
  6. @Retention(RetentionPolicy.RUNTIME)
  7. public @interface Idempotent {
  8.     /**
  9.      * 用于拼接幂等性判断的key的入参字段
  10.      * @return
  11.      */
  12.     String[] fields();
  13.     /**
  14.      * 用于接口幂等性校验的Redis中Key的过期时间,单位秒
  15.      * @return
  16.      */
  17.     long timeout() default 10l;
  18. }

6、创建自定义拦截器IdempotentInterceptor

  1. import com.badao.demo.annotation.Idempotent;
  2. import com.badao.demo.utils.RedisCache;
  3. import org.springframework.stereotype.Component;
  4. import org.springframework.web.method.HandlerMethod;
  5. import org.springframework.web.servlet.HandlerInterceptor;
  6. import javax.annotation.Resource;
  7. import javax.servlet.http.HttpServletRequest;
  8. import javax.servlet.http.HttpServletResponse;
  9. import java.io.OutputStream;
  10. @Component
  11. public class IdempotentInterceptor implements HandlerInterceptor {
  12.     @Resource
  13.     private RedisCache redisCache;
  14.     @Override
  15.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  16.         Idempotent idempotent = ((HandlerMethod)handler).getMethodAnnotation(Idempotent.class);
  17.         if(idempotent == null){
  18.             return true;
  19.         }
  20.         String idempotentKey = this.idempotentKey(idempotent,request);
  21.         Boolean success = redisCache.setIfAbsent(idempotentKey,1, idempotent.timeout());
  22.         if(Boolean.FALSE.equals(success)){
  23.             render(response,"请勿重复请求");
  24.             return false;
  25.         }
  26.         return true;
  27.     }
  28.     private String idempotentKey(Idempotent idempotent,HttpServletRequest request){
  29.         String[] fields = idempotent.fields();
  30.         StringBuilder idempotentKey = new StringBuilder();
  31.         for (String field : fields) {
  32.             String parameter = request.getParameter(field);
  33.             idempotentKey.append(parameter);
  34.         }
  35.         return idempotentKey.toString();
  36.     }
  37.     /**
  38.      * 接口渲染
  39.      * @param response
  40.      * @throws Exception
  41.      */
  42.     private void render(HttpServletResponse response,String message)throws Exception {
  43.         response.setContentType("application/json;charset=UTF-8");
  44.         OutputStream out = response.getOutputStream();
  45.         out.write(message.getBytes("UTF-8"));
  46.         out.flush();
  47.         out.close();
  48.     }
  49. }

7、配置拦截器IdempotentInterceptor注册到SpringMVC的拦截器链中

  1. import org.springframework.context.annotation.Configuration;
  2. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  3. import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
  4. import javax.annotation.Resource;
  5. /**
  6.  * 配置拦截器IdempotentInterceptor注册到SpringMVC的拦截器链中
  7.  */
  8. @Configuration
  9. public class WebConfig extends WebMvcConfigurationSupport {
  10.     @Resource
  11.     private IdempotentInterceptor idempotentInterceptor;
  12.     @Override
  13.     protected void addInterceptors(InterceptorRegistry registry) {
  14.         registry.addInterceptor(idempotentInterceptor);
  15.     }
  16. }

8、创建测试接口并添加自定义注解

  1. import com.badao.demo.annotation.Idempotent;
  2. import org.springframework.web.bind.annotation.GetMapping;
  3. import org.springframework.web.bind.annotation.RequestParam;
  4. import org.springframework.web.bind.annotation.RestController;
  5. @RestController
  6. public class TestController {
  7.     @Idempotent(fields = {"orderId"},timeout = 10)
  8.     @GetMapping("/test")
  9.     public String test(@RequestParam("orderId") String orderId, String remark){
  10.         return "success";
  11.     }
  12. }

这里指定orderId为订单唯一ID参数,并设置在10秒内如果此字段相同的请求会被拦截

9、创建测试方法

  1. import org.junit.jupiter.api.Test;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.boot.test.context.SpringBootTest;
  4. import org.springframework.http.MediaType;
  5. import org.springframework.test.web.servlet.MockMvc;
  6. import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
  7. import org.springframework.test.web.servlet.setup.MockMvcBuilders;
  8. import org.springframework.web.context.WebApplicationContext;
  9. @SpringBootTest
  10. class IdempotenceTest {
  11.     @Autowired
  12.     private WebApplicationContext webApplicationContext;
  13.     @Test
  14.     void test1() throws Exception {
  15.         //初始化MockMvc
  16.         MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
  17.         //循环调用5次进行测试
  18.         for (int i = 1; i <= 5; i++) {
  19.             System.out.println("第"+i+"次调用接口");
  20.             //调用接口
  21.             String result = mockMvc.perform(MockMvcRequestBuilders.get("/test")
  22.                     .accept(MediaType.TEXT_HTML)
  23.                     .param("orderId","001")
  24.                     .param("remark","badao"))
  25.                     .andReturn()
  26.                     .getResponse()
  27.                     .getContentAsString();
  28.             System.out.println(result);
  29.         }
  30.     }
  31. }

测试结果

使用postman等接口测试工具复测验证

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/504690
推荐阅读
相关标签
  

闽ICP备14008679号