天天看點

Spring Boot + Redis 實作 API 接口防刷限流

作者:java小悠

前言

在開發分布式高并發系統時有三把利器用來保護系統:緩存、降級、限流。

緩存

緩存的目的是提升系統通路速度和增大系統處理容量

降級

降級是當服務出現問題或者影響到核心流程時,需要暫時屏蔽掉,待高峰或者問題解決後再打開

限流

限流的目的是通過對并發通路/請求進行限速,或者對一個時間視窗内的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務、排隊或等待、降級等處理

本文主要講的是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 測試

Spring Boot + Redis 實作 API 接口防刷限流

源碼傳送門:

https://github.com/oycyqr/springboot-learning-demo/tree/master/springboot-validated

繼續閱讀