1.引言
接口防刷这个事情,其实就是限流。在我们探讨服务容错的方案中,通常有以下可选择的方案
超时:我调用你,你迟迟不给响应,超过一定时间我就不等你了,免得受到你的影响
流控(限流):你们怎么都一起来找我,我一次只能服务3个人,超过3个的我管不了
熔断降级:资源不够用不可用,为了保障核心链路可用,其它的都让一让
那么针对以上服务容错方案,业界可选择的产品比较丰富的,比如说:
服务之间调用,不管是ribbon+restTemplate,还是feign都支持超时设置
流控、熔断降级可选择的开源组件有: hystrix、resilience4j、sentinel
这些方案组件从功能、特性都比较丰富强大,要用好,需要团队中有专门的小伙伴去研究吃透,简单来说就是使用成本相对比较高,适合在平台级产品线中去使用。
那么,如果我们是一个创业型的小团队,产品线还没有那么丰富,一些临时性活动支撑。
举个例子,中秋节了,领导决定做一个客户、用户、员工线上答谢活动,即临时组织一个线上秒杀活动,那么这个任务,自然就交给了技术组的小伙伴。
如何实现接口防刷呢?前面我们提到方案肯定是来不及!有没有合适的方案?答案是有:spring拦截器+redis方案。下面来看具体实现。
2.环境准备
2.1.pom.xml
springboot整合redis使用,需要导入spring-boot-starter-data-redis依赖
<dependencies> <!--web mvc依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--redis 依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--fast json依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.43</version> </dependency> <!--lombok依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--test 依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
2.2.application.yml
server: port: 8080 spring: application: name: follow-me-springboot-interceptor redis: database: 0 host: 127.0.0.1 port: 6379 jedis: pool: min-idle: 8 max-idle: 8 max-active: 100 max-wait: 100000ms timeout: 5000ms
2.3.redis相关
为了方便使用,编写一个redis配置类,以及一个redis工具类
RedisConfig:用于初始配置RedisTemplate模板工具,指定key/value相关的序列化实现
RedisUtil:redis工具类,在RedisTemplate模板的基础上,封装redis相关操作,更加方便业务使用
RedisConfig
/** * Redis配置类 * * @author ThinkPad * @version 1.0 */ @Configuration @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(RedisProperties.class) public class RedisConfig { /** * redisTemplate * @param redisConnectionFactory * @return */ @Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate( RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); // key的序列化采用StringRedisSerializer template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); // value值的序列化采用fastJsonRedisSerializer FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class); template.setValueSerializer(fastJsonRedisSerializer); template.setHashValueSerializer(fastJsonRedisSerializer); // 设置连接工厂 template.setConnectionFactory(redisConnectionFactory); return template; } /** * stringRedisTemplate * @param redisConnectionFactory * @return */ @Bean @ConditionalOnMissingBean(StringRedisTemplate.class) public StringRedisTemplate stringRedisTemplate( RedisConnectionFactory redisConnectionFactory) { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } }
RedisUtil
整个工具类,代码量比较大,需要完整看的小伙伴请到代码仓库看,我已经把整个案例代码上传到代码仓库
/** * Redis工具类 * * @author ThinkPad * @version 1.0 */ @Component public final class RedisUtil { /** * 注入redisTemplate */ @Resource private RedisTemplate<String, Object> redisTemplate; .........................省略其它代码.................... /** * 获取缓存 * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 存入缓存 * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 * @param key 键 * @param delta 要增加几(大于0) * @return */ public long inCr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } .........................省略其它代码.................... }
2.4.拦截器相关
编写一个拦截器,以及拦截器的配置
WebConfig:该配置类实现WebMvcConfigurer,用于web mvc相关的配置,本案例中用于配置拦截器
AccessLimit:注解,用于标注需要访问限制的接口
LimitInterceptor:访问限制处理拦截器
WebConfig
/** * web 配置 * * @author ThinkPad * @version 1.0 */ @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LimitInterceptor limitInterceptor; /** * 添加拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { // 添加限流拦截器 registry.addInterceptor(limitInterceptor); } }
AccessLimit
/** * 自定义注解 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface AccessLimit { // 过期时间 单位:秒 int seconds(); // 最大请求次数 int maxCount(); }
LimitInterceptor
/** * 限流 interceptor * * @author ThinkPad * @version 1.0 */ @Component @Slf4j public class LimitInterceptor extends HandlerInterceptorAdapter { @Autowired private RedisUtil redisUtil; /** * 前置处理 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 当前请求url String url = request.getRequestURI(); // 判断请求是否属于方法的请求 if(handler instanceof HandlerMethod){ HandlerMethod hm = (HandlerMethod) handler; // 检查注解AccessLimit,若没有注解,直接放行 AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class); if(accessLimit == null){ log.info("当前正在请求接口:{},该接口不需要限制访问.", url); return true; } // 过期时间,最大请求次数 int seconds = accessLimit.seconds(); int maxCount = accessLimit.maxCount(); log.info("当前正在请求接口:{},该接口有访问限制需求,时间:{},最大访问次数:{}", url, seconds, maxCount); // 限流key String key = "rate:limit" + url; key = key.replaceAll("/", ":"); Integer count = 0; // 从redis中获取用户访问的次数 Object keyValue = redisUtil.get(key); if(keyValue != null){ count = (Integer) keyValue; } // 第1次访问 if(count == 0){ redisUtil.set(key, 1, seconds); }else if(count < maxCount){ // 第2到maxCount-1次访问 redisUtil.inCr(key,1); }else{ // 大于等于maxCount次访问 String content = String.format("您正在访问的接口:%s,超出了访问限制阈值:%d",url, maxCount); render(response, content); return false; } } return true; } /** * 封装返回值 * @param response * @param msg * @throws Exception */ private void render(HttpServletResponse response, String msg)throws Exception { response.setContentType("application/json;charset=UTF-8"); OutputStream out = response.getOutputStream(); out.write(msg.getBytes("UTF-8")); out.flush(); out.close(); } }
2.5.应用controller
/** * 限流controller * * @author ThinkPad * @version 1.0 */ @RestController public class RateLimitController { @Autowired private RedisUtil redisUtil; /** * 测试方法 * @return */ @RequestMapping("noLimit") public String noLimit(){ // 测试redis工具 redisUtil.inCr("rate:limit:test", 1L); return "no limit."; } /** * 需要限流方法 * @return */ @RequestMapping("needLimit") @AccessLimit(seconds=60, maxCount=5) public String rateLimit(){ return "need limit."; } }
3.案例效果
启动应用,分别访问端点
没有访问限制接口:http://127.0.0.1:8080/noLimit,响应:no limit.
有访问限制接口:http://127.0.0.1:8080/needLimit
未达到限制阈值,显示:need limit.
达到限制阈值,限制:您正在访问的接口:/needLimit,超出了访问限制阈值:5
客户端连接redis,观察计数器