赞
踩
SpringBoot+Redis+自定义注解实现接口防刷(限制不同接口单位时间内最大请求次数):
SpringBoot+Redis+自定义注解实现接口防刷(限制不同接口单位时间内最大请求次数)_redis防刷_霸道流氓气质的博客-CSDN博客
以下接口幂等性的实现方式与上面博客类似,可参考。
什么是幂等性?
幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后
会和其作用一次的结果相同。在计算机中编程中,一个幂等操作的特点是其任意多次执行所
产生的影响均与一次执行的影响相同。幂等函数或幂等方法是指可以使用相同参数重复执行,
并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
什么是接口幂等性?
在HTTP/1.1中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有
同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求
都不会再对资源产生副作用。这里的副作用是不会对结果产生破坏或者产生不可预料的结果。
也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
为什么需要实现幂等性?
在接口调用时一般情况下都能正常返回信息不会重复提交,不过在遇见以下情况时可以就会出现问题,
如:
前端重复提交表单:
在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,
致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。
用户恶意进行刷单:
例如在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到
用户重复提交的投票信息,这样会使投票结果与事实严重不符。
接口超时重复提交:
很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络
波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
消息进行重复消费:
当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。
使用幂等性最大的优势在于使接口保证任何幂等性操作,免去因重试等造成系统产生的未知的问题。
实现接口幂等性的方案有很多,下面记录一种自定义注解加拦截器和redis的实现方式模拟防止订单重复提交。
注:
博客:
霸道流氓气质_C#,架构之路,SpringBoot-CSDN博客
1、新建SpringBoot项目并添加必要的依赖
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.mysql</groupId>
- <artifactId>mysql-connector-j</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- <!--MySQL驱动-->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
- <!--MyBatis整合SpringBoot框架的起步依赖-->
- <dependency>
- <groupId>org.mybatis.spring.boot</groupId>
- <artifactId>mybatis-spring-boot-starter</artifactId>
- <version>2.2.1</version>
- </dependency>
- <!-- redis 缓存操作 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
- <!-- 阿里JSON解析器 -->
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.75</version>
- </dependency>
- </dependencies>
2、修改配置文件yml添加相关配置
- # 开发环境配置
- server:
- # 服务器的HTTP端口,默认为8080
- port: 996
- servlet:
- # 应用的访问路径
- context-path: /
- tomcat:
- # tomcat的URI编码
- uri-encoding: UTF-8
- # tomcat最大线程数,默认为200
- max-threads: 800
- # Tomcat启动初始化的线程数,默认值25
- min-spare-threads: 30
-
- # 数据源
- spring:
- application:
- name: badao-tcp-demo
- datasource:
- url:jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
- username: root
- password: 123456
- driver-class-name: com.mysql.cj.jdbc.Driver
- dbcp2:
- min-idle: 5 # 数据库连接池的最小维持连接数
- initial-size: 5 # 初始化连接数
- max-total: 5 # 最大连接数
- max-wait-millis: 150 # 等待连接获取的最大超时时间
-
- # redis 配置
- redis:
- # 地址
- #本地测试用
- host: 127.0.0.1
- port: 6379
- password: 123456
- # 连接超时时间
- timeout: 10s
- lettuce:
- pool:
- # 连接池中的最小空闲连接
- min-idle: 0
- # 连接池中的最大空闲连接
- max-idle: 8
- # 连接池的最大数据库连接数
- max-active: 8
- # #连接池最大阻塞等待时间(使用负值表示没有限制)
- max-wait: -1ms
-
- # mybatis配置
- mybatis:
- mapper-locations: classpath:mapper/*.xml # mapper映射文件位置
- type-aliases-package: com.badao.demo.entity # 实体类所在的位置
- configuration:
- log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #用于控制台打印sql语句
3、新建Redis的两个配置类
RedisConfig:
- import org.springframework.cache.annotation.CachingConfigurerSupport;
- import org.springframework.cache.annotation.EnableCaching;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.context.annotation.Primary;
- import org.springframework.data.redis.connection.RedisConnectionFactory;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.data.redis.serializer.StringRedisSerializer;
- import com.fasterxml.jackson.annotation.JsonAutoDetect;
- import com.fasterxml.jackson.annotation.JsonTypeInfo;
- import com.fasterxml.jackson.annotation.PropertyAccessor;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
-
- /**
- * redis配置
- *
- */
- @Configuration
- @EnableCaching
- public class RedisConfig extends CachingConfigurerSupport
- {
- @Bean
- @SuppressWarnings(value = { "unchecked", "rawtypes" })
- @Primary
- public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
- {
- RedisTemplate<Object, Object> template = new RedisTemplate<>();
- template.setConnectionFactory(connectionFactory);
-
- FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
-
- ObjectMapper mapper = new ObjectMapper();
- mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
- mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
- serializer.setObjectMapper(mapper);
-
- template.setValueSerializer(serializer);
- // 使用StringRedisSerializer来序列化和反序列化redis的key值
- template.setKeySerializer(new StringRedisSerializer());
- template.afterPropertiesSet();
- return template;
- }
- }
FastJson2JsonRedisSerializer:
- import com.alibaba.fastjson.JSON;
- import com.alibaba.fastjson.parser.ParserConfig;
- import com.alibaba.fastjson.serializer.SerializerFeature;
- import com.fasterxml.jackson.databind.JavaType;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import com.fasterxml.jackson.databind.type.TypeFactory;
- import org.springframework.data.redis.serializer.RedisSerializer;
- import org.springframework.data.redis.serializer.SerializationException;
- import org.springframework.util.Assert;
-
- import java.nio.charset.Charset;
-
- /**
- * Redis使用FastJson序列化
- *
- */
- public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
- {
- @SuppressWarnings("unused")
- private ObjectMapper objectMapper = new ObjectMapper();
-
- public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
- private Class<T> clazz;
-
- static
- {
- ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
- }
-
- public FastJson2JsonRedisSerializer(Class<T> clazz)
- {
- super();
- this.clazz = clazz;
- }
-
- @Override
- public byte[] serialize(T t) throws SerializationException
- {
- if (t == null)
- {
- return new byte[0];
- }
- return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
- }
-
- @Override
- public T deserialize(byte[] bytes) throws SerializationException
- {
- if (bytes == null || bytes.length <= 0)
- {
- return null;
- }
- String str = new String(bytes, DEFAULT_CHARSET);
-
- return JSON.parseObject(str, clazz);
- }
-
- public void setObjectMapper(ObjectMapper objectMapper)
- {
- Assert.notNull(objectMapper, "'objectMapper' must not be null");
- this.objectMapper = objectMapper;
- }
-
- protected JavaType getJavaType(Class<?> clazz)
- {
- return TypeFactory.defaultInstance().constructType(clazz);
- }
- }
4、新建Redis工具类
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.*;
- import org.springframework.stereotype.Component;
- import java.util.*;
- import java.util.concurrent.TimeUnit;
-
- /**
- * spring redis 工具类
- *
- **/
- @SuppressWarnings(value = { "unchecked", "rawtypes" })
- @Component
- public class RedisCache
- {
- @Autowired
- public RedisTemplate redisTemplate;
-
- @Autowired
- public StringRedisTemplate stringRedisTemplate;
-
- /**
- * 缓存基本的对象,Integer、String、实体类等
- *
- * @param key 缓存的键值
- * @param value 缓存的值
- * @return 缓存的对象
- */
- public <T> ValueOperations<String, T> setCacheObject(String key, T value)
- {
- ValueOperations<String, T> operation = redisTemplate.opsForValue();
- operation.set(key, value);
- return operation;
- }
-
- /**
- * 缓存基本的对象,Integer、String、实体类等
- *
- * @param key 缓存的键值
- * @param value 缓存的值
- * @param timeout 时间
- * @param timeUnit 时间颗粒度
- * @return 缓存的对象
- */
- public <T> ValueOperations<String, T> setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit)
- {
- ValueOperations<String, T> operation = redisTemplate.opsForValue();
- operation.set(key, value, timeout, timeUnit);
- return operation;
- }
-
- /**
- * 获得缓存的基本对象。
- *
- * @param key 缓存键值
- * @return 缓存键值对应的数据
- */
- public <T> T getCacheObject(String key)
- {
- ValueOperations<String, T> operation = redisTemplate.opsForValue();
- return operation.get(key);
- }
-
- /**
- * 删除单个对象
- *
- * @param key
- */
- public void deleteObject(String key)
- {
- redisTemplate.delete(key);
- }
-
- /**
- * 删除集合对象
- *
- * @param collection
- */
- public void deleteObject(Collection collection)
- {
- redisTemplate.delete(collection);
- }
-
- /**
- * 缓存List数据
- *
- * @param key 缓存的键值
- * @param dataList 待缓存的List数据
- * @return 缓存的对象
- */
- public <T> ListOperations<String, T> setCacheList(String key, List<T> dataList)
- {
- ListOperations listOperation = redisTemplate.opsForList();
- if (null != dataList)
- {
- int size = dataList.size();
- for (int i = 0; i < size; i++)
- {
- listOperation.leftPush(key, dataList.get(i));
- }
- }
- return listOperation;
- }
-
- /**
- * 获得缓存的list对象
- *
- * @param key 缓存的键值
- * @return 缓存键值对应的数据
- */
- public <T> List<T> getCacheList(String key)
- {
- List<T> dataList = new ArrayList<T>();
- ListOperations<String, T> listOperation = redisTemplate.opsForList();
- Long size = listOperation.size(key);
-
- for (int i = 0; i < size; i++)
- {
- dataList.add(listOperation.index(key, i));
- }
- return dataList;
- }
-
- /**
- * 缓存Set
- *
- * @param key 缓存键值
- * @param dataSet 缓存的数据
- * @return 缓存数据的对象
- */
- public <T> BoundSetOperations<String, T> setCacheSet(String key, Set<T> dataSet)
- {
- BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
- Iterator<T> it = dataSet.iterator();
- while (it.hasNext())
- {
- setOperation.add(it.next());
- }
- return setOperation;
- }
-
- /**
- * 获得缓存的set
- *
- * @param key
- * @return
- */
- public <T> Set<T> getCacheSet(String key)
- {
- Set<T> dataSet = new HashSet<T>();
- BoundSetOperations<String, T> operation = redisTemplate.boundSetOps(key);
- dataSet = operation.members();
- return dataSet;
- }
-
- /**
- * 缓存Map
- *
- * @param key
- * @param dataMap
- * @return
- */
- public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap)
- {
- HashOperations hashOperations = redisTemplate.opsForHash();
- if (null != dataMap)
- {
- for (Map.Entry<String, T> entry : dataMap.entrySet())
- {
- hashOperations.put(key, entry.getKey(), entry.getValue());
- }
- }
- return hashOperations;
- }
-
- /**
- * 获得缓存的Map
- *
- * @param key
- * @return
- */
- public <T> Map<String, T> getCacheMap(String key)
- {
- Map<String, T> map = redisTemplate.opsForHash().entries(key);
- return map;
- }
-
- /**
- * 获得缓存的基本对象列表
- *
- * @param pattern 字符串前缀
- * @return 对象列表
- */
- public Collection<String> keys(String pattern)
- {
- return redisTemplate.keys(pattern);
- }
-
- /**
- * 如果redis中不存在则存储进redis
- * @return
- */
- public Boolean setIfAbsent(Object key,Object value,long timeout){
- return redisTemplate.opsForValue().setIfAbsent(key, value,timeout, TimeUnit.SECONDS);
- }
- }
5、新建自定义注解类Idempotent
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface Idempotent {
-
- /**
- * 用于拼接幂等性判断的key的入参字段
- * @return
- */
- String[] fields();
- /**
- * 用于接口幂等性校验的Redis中Key的过期时间,单位秒
- * @return
- */
- long timeout() default 10l;
- }
6、创建自定义拦截器IdempotentInterceptor
- import com.badao.demo.annotation.Idempotent;
- import com.badao.demo.utils.RedisCache;
- import org.springframework.stereotype.Component;
- import org.springframework.web.method.HandlerMethod;
- import org.springframework.web.servlet.HandlerInterceptor;
- import javax.annotation.Resource;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.io.OutputStream;
-
- @Component
- public class IdempotentInterceptor implements HandlerInterceptor {
-
- @Resource
- private RedisCache redisCache;
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- Idempotent idempotent = ((HandlerMethod)handler).getMethodAnnotation(Idempotent.class);
- if(idempotent == null){
- return true;
- }
- String idempotentKey = this.idempotentKey(idempotent,request);
- Boolean success = redisCache.setIfAbsent(idempotentKey,1, idempotent.timeout());
- if(Boolean.FALSE.equals(success)){
- render(response,"请勿重复请求");
- return false;
- }
- return true;
- }
-
- private String idempotentKey(Idempotent idempotent,HttpServletRequest request){
- String[] fields = idempotent.fields();
- StringBuilder idempotentKey = new StringBuilder();
- for (String field : fields) {
- String parameter = request.getParameter(field);
- idempotentKey.append(parameter);
- }
- return idempotentKey.toString();
- }
-
- /**
- * 接口渲染
- * @param response
- * @throws Exception
- */
- private void render(HttpServletResponse response,String message)throws Exception {
- response.setContentType("application/json;charset=UTF-8");
- OutputStream out = response.getOutputStream();
- out.write(message.getBytes("UTF-8"));
- out.flush();
- out.close();
- }
- }
7、配置拦截器IdempotentInterceptor注册到SpringMVC的拦截器链中
- import org.springframework.context.annotation.Configuration;
- import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
- import javax.annotation.Resource;
-
- /**
- * 配置拦截器IdempotentInterceptor注册到SpringMVC的拦截器链中
- */
- @Configuration
- public class WebConfig extends WebMvcConfigurationSupport {
-
- @Resource
- private IdempotentInterceptor idempotentInterceptor;
-
- @Override
- protected void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(idempotentInterceptor);
- }
- }
8、创建测试接口并添加自定义注解
- import com.badao.demo.annotation.Idempotent;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestParam;
- import org.springframework.web.bind.annotation.RestController;
-
- @RestController
- public class TestController {
-
- @Idempotent(fields = {"orderId"},timeout = 10)
- @GetMapping("/test")
- public String test(@RequestParam("orderId") String orderId, String remark){
- return "success";
- }
- }
这里指定orderId为订单唯一ID参数,并设置在10秒内如果此字段相同的请求会被拦截
9、创建测试方法
- import org.junit.jupiter.api.Test;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.boot.test.context.SpringBootTest;
- import org.springframework.http.MediaType;
- import org.springframework.test.web.servlet.MockMvc;
- import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
- import org.springframework.test.web.servlet.setup.MockMvcBuilders;
- import org.springframework.web.context.WebApplicationContext;
-
- @SpringBootTest
- class IdempotenceTest {
-
- @Autowired
- private WebApplicationContext webApplicationContext;
-
- @Test
- void test1() throws Exception {
- //初始化MockMvc
- MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
- //循环调用5次进行测试
- for (int i = 1; i <= 5; i++) {
- System.out.println("第"+i+"次调用接口");
- //调用接口
- String result = mockMvc.perform(MockMvcRequestBuilders.get("/test")
- .accept(MediaType.TEXT_HTML)
- .param("orderId","001")
- .param("remark","badao"))
- .andReturn()
- .getResponse()
- .getContentAsString();
- System.out.println(result);
- }
- }
- }
测试结果
使用postman等接口测试工具复测验证
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。