1.引言
接口防刷这个事情,其实就是限流。在我们探讨服务容错的方案中,通常有以下可选择的方案
- 超时:我调用你,你迟迟不给响应,超过一定时间我就不等你了,免得受到你的影响
- 流控(限流):你们怎么都一起来找我,我一次只能服务3个人,超过3个的我管不了
- 熔断降级:资源不够用不可用,为了保障核心链路可用,其它的都让一让
那么针对以上服务容错方案,业界可选择的产品比较丰富的,比如说:
- 服务之间调用,不管是ribbon+restTemplate,还是feign都支持超时设置
- 流控、熔断降级可选择的开源组件有: hystrix、resilience4j、sentinel
这些方案组件从功能、特性都比较丰富强大,要用好,需要团队中有专门的小伙伴去研究吃透,简单来说就是使用成本相对比较高,适合在平台级产品线中去使用。
那么,如果我们是一个创业型的小团队,产品线还没有那么丰富,一些临时性活动支撑。
举个例子,中秋节了,领导决定做一个客户、用户、员工线上答谢活动,即临时组织一个线上秒杀活动,那么这个任务,自然就交给了技术组的小伙伴。
如何实现接口防刷呢?前面我们提到方案肯定是来不及!有没有合适的方案?答案是有:spring拦截器+redis方案。下面来看具体实现。
2.环境准备
2.1.pom.xml
springboot整合redis使用,需要导入spring-boot-starter-data-redis依赖
xml
<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
yaml
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
java
/**
* 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
整个工具类,代码量比较大,需要完整看的小伙伴请到代码仓库看,我已经把整个案例代码上传到代码仓库
java
/**
* 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
java
/**
* 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
java
/**
* 自定义注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
// 过期时间 单位:秒
int seconds();
// 最大请求次数
int maxCount();
}
LimitInterceptor
java
/**
* 限流 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
java
/**
* 限流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,观察计数器
本文源码:gitee.com/yanghouhua/...,子模块: follow-me-springboot-interceptor