前言
在開發分布式高并發系統時有三把利器用來保護系統:緩存、降級、限流。
緩存
緩存的目的是提升系統通路速度和增大系統處理容量
降級
降級是當服務出現問題或者影響到核心流程時,需要暫時屏蔽掉,待高峰或者問題解決後再打開
限流
限流的目的是通過對并發通路/請求進行限速,或者對一個時間視窗内的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務、排隊或等待、降級等處理
本文主要講的是api接口限流相關内容,雖然不是論述高并發概念中的限流, 不過道理都差不多。通過限流可以讓系統維持在一個相對穩定的狀态,為更多的客戶提供服務。
api接口的限流主要應用場景有:
- 電商系統(特别是6.18、雙11等)中的秒殺活動,使用限流防止使用軟體惡意刷單;
- 各種基礎api接口限流:例如天氣資訊擷取,IP對應城市接口,百度、騰訊等對外提供的基礎接口,都是通過限流來實作免費與付費直接的轉換。
- 被各種系統廣泛調用的api接口,嚴重消耗網絡、記憶體等資源,需要合理限流。
api限流實戰
一、SpringBoot中內建Redis
SpringBoot中內建Redis相對比較簡單,步驟如下:
1.1 引入Redis依賴
<!--springboot redis依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1.2 在application.yml中配置Redis
spring:
redis:
database: 3 # Redis資料庫索引(預設為0)
host: 127.0.0.1 # Redis伺服器位址
port: 6379 # Redis伺服器連接配接端口
password: 123456 # Redis伺服器連接配接密碼(預設為空)
timeout: 2000 # 連接配接逾時時間(毫秒)
jedis:
pool:
max-active: 200 # 連接配接池最大連接配接數(使用負值表示沒有限制)
max-idle: 20 # 連接配接池中的最大空閑連接配接
min-idle: 0 # 連接配接池中的最小空閑連接配接
max-wait: -1 # 連接配接池最大阻塞等待時間(使用負值表示沒有限制)
1.3 配置RedisTemplate
/**
* @Description: redis配置類
* @Author oyc
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* RedisTemplate相關配置
* 使redis支援插入對象
*
* @param factory
* @return 方法緩存 Methods the cache
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置連接配接工廠
template.setConnectionFactory(factory);
// 設定key的序列化器
template.setKeySerializer(new StringRedisSerializer());
// 設定value的序列化器
//使用Jackson 2,将對象序列化為JSON
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;
}
}
以上,已經完成Redis的內建,後續使用可以直接注入RedisTemplate,如下所示:
@Autowired
private RedisTemplate<String, Object> redisTemplate;
二、實作限流
2.1 添加自定義AccessLimit注解
使用注解方式實作接口的限流操作,友善而優雅。
/**
* @Description:
* @Author oyc
*/
@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;
}
2.2 編寫攔截器
限流的思路
- 通過路徑:ip的作為key,通路次數為value的方式對某一使用者的某一請求進行唯一辨別
- 每次通路的時候判斷key是否存在,是否count超過了限制的通路次數
- 若通路超出限制,則應response傳回msg:請求過于頻繁給前端予以展示
/**
* @Description: 通路攔截器
* @Author oyc
*/
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {// Handler 是否為 HandlerMethod 執行個體
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 (accessLimit == null) {
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 (null == count || -1 == count) {
redisTemplate.opsForValue().set(key, 1, seconds, TimeUnit.SECONDS);
return true;
}
if (count < maxCount) {
redisTemplate.opsForValue().increment(key);
return true;
}
if (count >= maxCount) {
logger.warn("請求過于頻繁請稍後再試");
return false;
}
}
return true;
} catch (Exception e) {
logger.warn("請求過于頻繁請稍後再試");
e.printStackTrace();
}
return true;
}
}
2.3 注冊攔截器并配置攔截路徑和不攔截路徑
/**
* @Description: 通路攔截器配置
* @Author oyc
* @Date 2020/10/22 11:34 下午
*/
@Configuration
public class IntercepterConfig implements WebMvcConfigurer {
@Autowired
private AccessLimitInterceptor accessLimitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessLimitInterceptor)
.addPathPatterns("/**").excludePathPatterns("/static/**","/login.html","/user/login");
}
}
2.4 使用AccessLimit
/**
* @Description:
* @Author oyc
*/
@RestController
@RequestMapping("access")
public class AccessLimitController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 限流測試
*/
@GetMapping
@AccessLimit(maxCount = 3,second = 60)
public String limit(HttpServletRequest request) {
logger.error("Access Limit Test");
return "限流測試";
}
}
2.5 測試
源碼傳送門:
https://github.com/oycyqr/springboot-learning-demo/tree/master/springboot-validated