SpringBoot如何使用自定義注解實現(xiàn)接口限流
使用自定義注解實現(xiàn)接口限流
在高并發(fā)系統(tǒng)中,保護系統(tǒng)的三種方式分別為:緩存,降級和限流。
限流的目的是通過對并發(fā)訪問請求進行限速或者一個時間窗口內(nèi)的的請求數(shù)量進行限速來保護系統(tǒng),一旦達到限制速率則可以拒絕服務(wù)、排隊或等待。
1、自定義限流注解
import com.asurplus.common.enums.LimitType; import java.lang.annotation.*; /** * 限流注解 * * @author Lizhou */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Limit { /** * 限流key前綴 */ String prefix() default "limit:"; /** * 限流時間,單位秒 */ int time() default 60; /** * 限流次數(shù) */ int count() default 10; /** * 限流類型 */ LimitType type() default LimitType.DEFAULT; }
2、限流類型枚舉類
/** ?* 限流類型 ?* ?* @author Lizhou ?*/ public enum LimitType { ? ? /** ? ? ?* 默認策略全局限流 ? ? ?*/ ? ? DEFAULT, ? ? /** ? ? ?* 根據(jù)請求者IP進行限流 ? ? ?*/ ? ? IP }
我們定義了兩種限流類型,分別為全局限流和 IP 限流,全局限流對訪問接口的所有用戶進行限流保護,IP 限流對每個 IP 請求用戶進行單獨限流保護。
3、限流 Lua 腳本
1、由于我們使用 Redis 進行限流,我們需要引入 Redis 的 maven 依賴,同時需要引入 aop 的依賴
<!-- aop依賴 --> <dependency> ? ? <groupId>org.springframework.boot</groupId> ? ? <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- redis依賴 --> <dependency> ? ? <groupId>org.springframework.boot</groupId> ? ? <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
在配置文件中配置 Redis 的連接信息,具體參考:SpringBoot中整合Redis
2、限流腳本
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; /** ?* 接口限流 ?*/ @Slf4j @Component public class RedisLimitUtil { ? ? @Autowired ? ? private StringRedisTemplate redisTemplate; ? ? /** ? ? ?* 限流 ? ? ?* ? ? ?* @param key ? 鍵 ? ? ?* @param count 限流次數(shù) ? ? ?* @param times 限流時間 ? ? ?* @return ? ? ?*/ ? ? public boolean limit(String key, int count, int times) { ? ? ? ? try { ? ? ? ? ? ? String script = "local lockKey = KEYS[1]\n" + ? ? ? ? ? ? ? ? ? ? "local lockCount = KEYS[2]\n" + ? ? ? ? ? ? ? ? ? ? "local lockExpire = KEYS[3]\n" + ? ? ? ? ? ? ? ? ? ? "local currentCount = tonumber(redis.call('get', lockKey) or \"0\")\n" + ? ? ? ? ? ? ? ? ? ? "if currentCount < tonumber(lockCount)\n" + ? ? ? ? ? ? ? ? ? ? "then\n" + ? ? ? ? ? ? ? ? ? ? " ? ?redis.call(\"INCRBY\", lockKey, \"1\")\n" + ? ? ? ? ? ? ? ? ? ? " ? ?redis.call(\"expire\", lockKey, lockExpire)\n" + ? ? ? ? ? ? ? ? ? ? " ? ?return true\n" + ? ? ? ? ? ? ? ? ? ? "else\n" + ? ? ? ? ? ? ? ? ? ? " ? ?return false\n" + ? ? ? ? ? ? ? ? ? ? "end"; ? ? ? ? ? ? RedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class); ? ? ? ? ? ? List<String> keys = Arrays.asList(key, String.valueOf(count), String.valueOf(times)); ? ? ? ? ? ? return redisTemplate.execute(redisScript, keys); ? ? ? ? } catch (Exception e) { ? ? ? ? ? ? log.error("限流腳本執(zhí)行失?。簕}", e.getMessage()); ? ? ? ? } ? ? ? ? return false; ? ? } }
通過 Lua 腳本,根據(jù) Redis 中緩存的鍵值判斷限流時間(也是 key 的過期時間)內(nèi),訪問次數(shù)是否超出了限流次數(shù),沒超出則訪問次數(shù) +1,返回 true,超出了則返回 false。
4、限流切面處理類
import com.asurplus.common.annotation.Limit; import com.asurplus.common.enums.LimitType; import com.asurplus.common.exception.CustomException; import com.asurplus.common.ip.IpUtil; import com.asurplus.common.redis.RedisLimitUtil; import com.asurplus.common.utils.HttpRequestUtil; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** ?* 限流處理 ?* ?* @author Lizhou ?*/ @Slf4j @Aspect @Component public class LimitAspect { ? ? @Autowired ? ? private RedisLimitUtil redisLimitUtil; ? ? /** ? ? ?* 前置通知,判斷是否超出限流次數(shù) ? ? ?* ? ? ?* @param point ? ? ?*/ ? ? @Before("@annotation(limit)") ? ? public void doBefore(JoinPoint point, Limit limit) { ? ? ? ? try { ? ? ? ? ? ? // 拼接key ? ? ? ? ? ? String key = getCombineKey(limit, point); ? ? ? ? ? ? // 判斷是否超出限流次數(shù) ? ? ? ? ? ? if (!redisLimitUtil.limit(key, limit.count(), limit.time())) { ? ? ? ? ? ? ? ? throw new CustomException("訪問過于頻繁,請稍候再試"); ? ? ? ? ? ? } ? ? ? ? } catch (CustomException e) { ? ? ? ? ? ? throw e; ? ? ? ? } catch (Exception e) { ? ? ? ? ? ? throw new RuntimeException("接口限流異常,請稍候再試"); ? ? ? ? } ? ? } ? ? /** ? ? ?* 根據(jù)限流類型拼接key ? ? ?*/ ? ? public String getCombineKey(Limit limit, JoinPoint point) { ? ? ? ? StringBuilder sb = new StringBuilder(limit.prefix()); ? ? ? ? // 按照IP限流 ? ? ? ? if (limit.type() == LimitType.IP) { ? ? ? ? ? ? sb.append(IpUtil.getIpAddr(HttpRequestUtil.getRequest())).append("-"); ? ? ? ? } ? ? ? ? // 拼接類名和方法名 ? ? ? ? MethodSignature signature = (MethodSignature) point.getSignature(); ? ? ? ? Method method = signature.getMethod(); ? ? ? ? Class<?> targetClass = method.getDeclaringClass(); ? ? ? ? sb.append(targetClass.getName()).append("-").append(method.getName()); ? ? ? ? return sb.toString(); ? ? } }
1、使用我們剛剛的 Lua 腳本判斷是否超出了限流次數(shù),超出了限流次數(shù)后返回一個自定義異常,然后在全局異常中去捕捉異常,返回 JSON 數(shù)據(jù)。
2、根據(jù)注解參數(shù),判斷限流類型,拼接緩存 key 值
5、使用與測試
1、測試方法
@Limit(type = LimitType.DEFAULT, time = 10, count = 2) @GetMapping("test") public String test() { ? ? return "請求成功:" + System.currentTimeMillis(); }
使用自定義注解 @Limit,限制為 10 秒內(nèi),允許訪問 2 次
2、測試結(jié)果
第一次
第二次
第三次
可以看出,前面兩次都成功返回了請求結(jié)果,第三次超出了接口限流次數(shù),返回了自定義異常信息。
SpringBoot工程中限流方式
限流,是防止用戶惡意刷新接口。常見的限流方式有阿里開源的sentinel、redis等。
1、google的guava,令牌桶算法實現(xiàn)限流
Guava的 RateLimiter提供了令牌桶算法實現(xiàn):平滑突發(fā)限流(SmoothBursty)和平滑預(yù)熱限流(SmoothWarmingUp)實現(xiàn)。
// RateLimiter提供了兩個工廠方法,最終會調(diào)用下面兩個函數(shù),生成RateLimiter的兩個子類。 static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) { ??? ?RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */); ? ? rateLimiter.setRate(permitsPerSecond); ??? ?return rateLimiter; } static RateLimiter create( ??? ?SleepingStopwatch stopwatch, double permitsPerSecond, long warmupPeriod, TimeUnit unit, ?double coldFactor) { ??? ?RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor); ? ? rateLimiter.setRate(permitsPerSecond); ??? ?return rateLimiter; }
- 平滑突發(fā)限流:使用 RateLimiter的靜態(tài)方法創(chuàng)建一個限流器,設(shè)置每秒放置的令牌數(shù)為10個。返回的RateLimiter對象可以保證1秒內(nèi)不會給超過10個令牌,并且以固定速率進行放置,達到平滑輸出的效果。
- 平滑預(yù)熱限流:RateLimiter的 SmoothWarmingUp是帶有預(yù)熱期的平滑限流,它啟動后會有一段預(yù)熱期,逐步將分發(fā)頻率提升到配置的速率。
@RestController public class HomeController { ? ? // 這里的10表示每秒允許處理的量為10個 ? ? private RateLimiter limiter = RateLimiter.create(10); ? ? private RateLimiter limiter2 = RateLimiter.create(2, 1000, TimeUnit.SECONDS); ? ? //permitsPerSecond: 表示 每秒新增 的令牌數(shù);warmupPeriod: 表示在從 冷啟動速率 過渡到 平均速率 的時間間隔 ? ? @GetMapping("/test/{name}") ? ? public String test(@PathVariable("name") String name) { ? ? ? ? // 請求RateLimiter, 超過permits會被阻塞 ? ? ? ? final double acquire = limiter.acquire(); ? ? ? ? System.out.println("acquire=" + acquire); ? ? ? ? //判斷double是否為空或者為0 ? ? ? ? if (acquire == 0) { ? ? ? ? ? ? return name; ? ? ? ? } else { ? ? ? ? ? ? return "操作太頻繁"; ? ? ? ? } ? ? } ? ? @AccessLimit(limit = 2, sec = 10) ? ? @GetMapping("/test2/{name}") ? ? public String test2(@PathVariable("name") String name) { ? ? ? ? return name; ? ? } }
2、interceptor+redis根據(jù)注解限流
public class AccessLimitInterceptor implements HandlerInterceptor { ? ? @Resource ? ? private RedisTemplate<String, Integer> redisTemplate; ? ? @Override ? ? public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ? ? ? ? if (handler instanceof HandlerMethod) { ? ? ? ? ? ? HandlerMethod handlerMethod = (HandlerMethod) handler; ? ? ? ? ? ? Method method = handlerMethod.getMethod(); ? ? ? ? ? ? if (!method.isAnnotationPresent(AccessLimit.class)) { ? ? ? ? ? ? ? ? return true; ? ? ? ? ? ? } ? ? ? ? ? ? AccessLimit accessLimit = method.getAnnotation(AccessLimit.class); ? ? ? ? ? ? if (accessLimit == null) { ? ? ? ? ? ? ? ? return true; ? ? ? ? ? ? } ? ? ? ? ? ? int limit = accessLimit.limit(); ? ? ? ? ? ? int sec = accessLimit.sec(); ? ? ? ? ? ? String key = IPUtil.getIpAddress(request) + request.getRequestURI(); ? ? ? ? ? ? //資源唯一標(biāo)識 ? ? ? ? ? ? Integer maxLimit = redisTemplate.opsForValue().get(key); ? ? ? ? ? ? if (maxLimit == null) { ? ? ? ? ? ? ? ? //set時一定要加過期時間 ? ? ? ? ? ? ? ? redisTemplate.opsForValue().set(key, 1, sec, TimeUnit.SECONDS); ? ? ? ? ? ? } else if (maxLimit < limit) { ? ? ? ? ? ? ? ? redisTemplate.opsForValue().set(key, maxLimit + 1, sec, TimeUnit.SECONDS); ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? output(response, "請求太頻繁!"); ? ? ? ? ? ? ? ? return false; ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? return true; ? ? } ? ? public void output(HttpServletResponse response, String msg) throws IOException { ? ? ? ? response.setContentType("application/json;charset=UTF-8"); ? ? ? ? ServletOutputStream outputStream = null; ? ? ? ? try { ? ? ? ? ? ? outputStream = response.getOutputStream(); ? ? ? ? ? ? outputStream.write(msg.getBytes("UTF-8")); ? ? ? ? } catch (IOException e) { ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? } finally { ? ? ? ? ? ? outputStream.flush(); ? ? ? ? ? ? outputStream.close(); ? ? ? ? } ? ? } ? ? @Override ? ? public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { ? ? } ? ? @Override ? ? public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ? ? } }
@Configuration public class InterceptorConfig extends WebMvcConfigurationSupport { ? ? @Bean ? ? public AccessLimitInterceptor accessLimitInterceptor() { ? ? ? ? return new AccessLimitInterceptor(); ? ? } ? ? @Override ? ? public void addInterceptors(InterceptorRegistry registry) { ? ? ? ? //addPathPatterns 添加攔截規(guī)則 ? ? ? ? registry.addInterceptor(accessLimitInterceptor()).addPathPatterns("/**"); ? ? } ? ? @Override ? ? public void addViewControllers(ViewControllerRegistry registry) { ? ? ? ? registry.addViewController("/"); ? ? } }
@Configuration public class RedisConfig { ? ? @Bean ? ? public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { ? ? ? ? RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>(); ? ? ? ? template.setConnectionFactory(redisConnectionFactory); ? ? ? ? template.setKeySerializer(new StringRedisSerializer()); ? ? ? ? template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); ? ? ? ? template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer()); ? ? ? ? template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); ? ? ? ? template.afterPropertiesSet(); ? ? ? ? return template; ? ? } }
限流方式還有很多,后續(xù)繼續(xù)嘗試。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring中的@RestControllerAdvice注解使用解析
這篇文章主要介紹了Spring中的@RestControllerAdvice注解使用解析,@RestControllerAdvice?是?Spring?框架中一個用于統(tǒng)一處理控制器異常和返回結(jié)果的注解,它可以被用來定義全局異常處理程序和全局響應(yīng)結(jié)果處理程序,需要的朋友可以參考下2024-01-01使用JAXBContext輕松實現(xiàn)Java和xml的互相轉(zhuǎn)換方式
這篇文章主要介紹了依靠JAXBContext輕松實現(xiàn)Java和xml的互相轉(zhuǎn)換方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08解決@RequestMapping和@FeignClient放在同一個接口上遇到的坑
這篇文章主要介紹了解決@RequestMapping和@FeignClient放在同一個接口上遇到的坑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07Java基礎(chǔ)之JDBC的數(shù)據(jù)庫連接與基本操作
這篇文章主要介紹了Java基礎(chǔ)之JDBC的數(shù)據(jù)庫連接與基本操作,文中有非常詳細的代碼示例,對正在學(xué)習(xí)java基礎(chǔ)的小伙伴們也有很好的幫助,需要的朋友可以參考下2021-05-05解決springboot項目啟動失敗Could not initialize class&
這篇文章主要介紹了解決springboot項目啟動失敗Could not initialize class com.fasterxml.jackson.databind.ObjectMapper問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06SpringBoot常用注解@RestControllerAdvice詳解
這篇文章主要介紹了SpringBoot常用注解@RestControllerAdvice詳解,@RestControllerAdvice是一個組合注解,由@ControllerAdvice、@ResponseBody組成,而@ControllerAdvice繼承了@Component,因此@RestControllerAdvice本質(zhì)上是個Component,需要的朋友可以參考下2024-01-01Java?Hibernate中一對多和多對多關(guān)系的映射方式
Hibernate是一種Java對象關(guān)系映射框架,支持一對多和多對多關(guān)系的映射。一對多關(guān)系可以使用集合屬性和單向/雙向關(guān)聯(lián)來映射,多對多關(guān)系可以使用集合屬性和中間表來映射。在映射過程中,需要注意級聯(lián)操作、延遲加載、中間表的處理等問題2023-04-04