前言
在开发分布式高并发系统时有三把利器来保护系统:缓存、降级、限流。
- 缓存: 缓存的目的是提升系统访问速度和增大系统处理容量;
- 降级:降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开
- 限流:限流的目的是通过对并发访问/请求进行限流,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
api接口的限流主要应用场景:
- 电商系统(特别是6.18、双11等)中的秒杀活动,使用限流防止使用软件恶意刷单;
- 各种基础api接口限流:例如天气信息获取,IP对应城市接口,百度、腾讯等对外提供的基础接口,都是通过限流来实现免费与付费直接的转换;
- 被各种系统广泛调用的api接口,严重消耗网络、内存等资源,需要合理限流。
限流实战
一、SpringBoot集成Redis
- 引入Redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置
server:
port: 8080
spring:
redis:
database: 3
host: 127.0.0.1
port: 6379
timeout: 2000 # 连接超时时间(毫秒)
jedis:
pool:
max-active: 200 # 连接池最大连接数(使用负值表示没有限制)
max-idle: 20 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
- 配置RedisTemplate
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* RedisTemplate相关配置
* 使redis支持插入对象
* @author Fang Ruichuan
* @date 2023-01-07 20:32
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(factory);
// 设置key的序列化器
template.setKeySerializer(new StringRedisSerializer());
// 设置value的序列化器
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// json转对象类,不设置默认的会将json转为hashmap
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}
二、实现限流
- 添加自定义@AccessLimit注解
@Inherited
@Documented
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
// 指定second时间内API请求次数
int maxCount() default 5;
// 请求次数的指定时间范围 秒数(redis数据过期时间)
int second() default 60;
}
- 编写拦截器
- 通过路径:ip作为key,访问次数作为value的方式对某一用户的某一请求进行唯一标识
- 每次访问的时候判断key是否存在,是否count超过了限流的访问次数
- 若访问超出限制,则应response返回msg:请求过于频繁给前端给予展示
@Component
@Slf4j
@RequiredArgsConstructor
public class AccessLimiterInterceptor implements HandlerInterceptor {
private final RedisTemplate<String, Object> redisTemplate;
@Override
public boolean preHandle(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, Object handler) throws Exception {
try {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取方法
Method method = handlerMethod.getMethod();
// 是否有AccessLimit注解
if (!method.isAnnotationPresent(AccessLimit.class)) {
return true;
}
// 获取注解内容信息
AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
if (Objects.isNull(accessLimit)) {
return true;
}
int seconds = accessLimit.second();
int maxCount = accessLimit.maxCount();
// 存储key
String key = request.getRemoteAddr() + ":" + request.getContextPath() + ":" + request.getServletPath();
// 已经访问的次数
Integer count = (Integer) redisTemplate.opsForValue().get(key);
System.out.println("已经访问的次数:" + count);
if (Objects.isNull(count) || count.equals(-1)) {
redisTemplate.opsForValue().set(key, 1, seconds, TimeUnit.SECONDS);
return true;
}
if (count < maxCount) {
redisTemplate.opsForValue().increment(key);
return true;
}
if (count >= maxCount) {
log.warn("请求过于频繁,请稍后再试");
return false;
}
}
return true;
} catch (Exception e) {
log.warn("请求过于频繁,请稍后再试");
}
return true;
}
}
- 注册拦截器并配置拦截路径和不拦截路径
@Configuration
@RequiredArgsConstructor
public class InterceptorConfig implements WebMvcConfigurer {
private final AccessLimiterInterceptor accessLimiterInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessLimiterInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/static/**", "login.html", "user/login");
}
}
- 使用
@Slf4j
@RestController
@RequestMapping("access")
public class AccessLimiterController {
@GetMapping("test")
@AccessLimit(maxCount = 3, second = 60)
public String limit(HttpServletRequest request) {
log.error("Access Limit Test");
return "限流测试";
}
}
- 访问测试