SpringBoot如何使用自定義注解實(shí)現(xiàn)接口限流
使用自定義注解實(shí)現(xiàn)接口限流
在高并發(fā)系統(tǒng)中,保護(hù)系統(tǒng)的三種方式分別為:緩存,降級(jí)和限流。
限流的目的是通過(guò)對(duì)并發(fā)訪(fǎng)問(wèn)請(qǐng)求進(jìn)行限速或者一個(gè)時(shí)間窗口內(nèi)的的請(qǐng)求數(shù)量進(jìn)行限速來(lái)保護(hù)系統(tǒng),一旦達(dá)到限制速率則可以拒絕服務(wù)、排隊(duì)或等待。
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:";
/**
* 限流時(shí)間,單位秒
*/
int time() default 60;
/**
* 限流次數(shù)
*/
int count() default 10;
/**
* 限流類(lèi)型
*/
LimitType type() default LimitType.DEFAULT;
}
2、限流類(lèi)型枚舉類(lèi)
/**
?* 限流類(lèi)型
?*
?* @author Lizhou
?*/
public enum LimitType {
? ? /**
? ? ?* 默認(rèn)策略全局限流
? ? ?*/
? ? DEFAULT,
? ? /**
? ? ?* 根據(jù)請(qǐng)求者IP進(jìn)行限流
? ? ?*/
? ? IP
}我們定義了兩種限流類(lèi)型,分別為全局限流和 IP 限流,全局限流對(duì)訪(fǎng)問(wèn)接口的所有用戶(hù)進(jìn)行限流保護(hù),IP 限流對(duì)每個(gè) IP 請(qǐng)求用戶(hù)進(jìn)行單獨(dú)限流保護(hù)。
3、限流 Lua 腳本
1、由于我們使用 Redis 進(jìn)行限流,我們需要引入 Redis 的 maven 依賴(lài),同時(shí)需要引入 aop 的依賴(lài)
<!-- aop依賴(lài) --> <dependency> ? ? <groupId>org.springframework.boot</groupId> ? ? <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- redis依賴(lài) --> <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 限流時(shí)間
? ? ?* @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;
? ? }
}通過(guò) Lua 腳本,根據(jù) Redis 中緩存的鍵值判斷限流時(shí)間(也是 key 的過(guò)期時(shí)間)內(nèi),訪(fǎng)問(wèn)次數(shù)是否超出了限流次數(shù),沒(méi)超出則訪(fǎng)問(wèn)次數(shù) +1,返回 true,超出了則返回 false。
4、限流切面處理類(lèi)
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("訪(fǎng)問(wèn)過(guò)于頻繁,請(qǐng)稍候再試");
? ? ? ? ? ? }
? ? ? ? } catch (CustomException e) {
? ? ? ? ? ? throw e;
? ? ? ? } catch (Exception e) {
? ? ? ? ? ? throw new RuntimeException("接口限流異常,請(qǐng)稍候再試");
? ? ? ? }
? ? }
? ? /**
? ? ?* 根據(jù)限流類(lèi)型拼接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("-");
? ? ? ? }
? ? ? ? // 拼接類(lèi)名和方法名
? ? ? ? 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ù)后返回一個(gè)自定義異常,然后在全局異常中去捕捉異常,返回 JSON 數(shù)據(jù)。
2、根據(jù)注解參數(shù),判斷限流類(lèi)型,拼接緩存 key 值
5、使用與測(cè)試
1、測(cè)試方法
@Limit(type = LimitType.DEFAULT, time = 10, count = 2)
@GetMapping("test")
public String test() {
? ? return "請(qǐng)求成功:" + System.currentTimeMillis();
}使用自定義注解 @Limit,限制為 10 秒內(nèi),允許訪(fǎng)問(wèn) 2 次
2、測(cè)試結(jié)果
第一次

第二次

第三次

可以看出,前面兩次都成功返回了請(qǐng)求結(jié)果,第三次超出了接口限流次數(shù),返回了自定義異常信息。
SpringBoot工程中限流方式
限流,是防止用戶(hù)惡意刷新接口。常見(jiàn)的限流方式有阿里開(kāi)源的sentinel、redis等。
1、google的guava,令牌桶算法實(shí)現(xiàn)限流
Guava的 RateLimiter提供了令牌桶算法實(shí)現(xiàn):平滑突發(fā)限流(SmoothBursty)和平滑預(yù)熱限流(SmoothWarmingUp)實(shí)現(xiàn)。
// RateLimiter提供了兩個(gè)工廠方法,最終會(huì)調(diào)用下面兩個(gè)函數(shù),生成RateLimiter的兩個(gè)子類(lèi)。
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)建一個(gè)限流器,設(shè)置每秒放置的令牌數(shù)為10個(gè)。返回的RateLimiter對(duì)象可以保證1秒內(nèi)不會(huì)給超過(guò)10個(gè)令牌,并且以固定速率進(jìn)行放置,達(dá)到平滑輸出的效果。
- 平滑預(yù)熱限流:RateLimiter的 SmoothWarmingUp是帶有預(yù)熱期的平滑限流,它啟動(dòng)后會(huì)有一段預(yù)熱期,逐步將分發(fā)頻率提升到配置的速率。
@RestController
public class HomeController {
? ? // 這里的10表示每秒允許處理的量為10個(gè)
? ? private RateLimiter limiter = RateLimiter.create(10);
? ? private RateLimiter limiter2 = RateLimiter.create(2, 1000, TimeUnit.SECONDS);
? ? //permitsPerSecond: 表示 每秒新增 的令牌數(shù);warmupPeriod: 表示在從 冷啟動(dòng)速率 過(guò)渡到 平均速率 的時(shí)間間隔
? ? @GetMapping("/test/{name}")
? ? public String test(@PathVariable("name") String name) {
? ? ? ? // 請(qǐng)求RateLimiter, 超過(guò)permits會(huì)被阻塞
? ? ? ? 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)識(shí)
? ? ? ? ? ? Integer maxLimit = redisTemplate.opsForValue().get(key);
? ? ? ? ? ? if (maxLimit == null) {
? ? ? ? ? ? ? ? //set時(shí)一定要加過(guò)期時(shí)間
? ? ? ? ? ? ? ? redisTemplate.opsForValue().set(key, 1, sec, TimeUnit.SECONDS);
? ? ? ? ? ? } else if (maxLimit < limit) {
? ? ? ? ? ? ? ? redisTemplate.opsForValue().set(key, maxLimit + 1, sec, TimeUnit.SECONDS);
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? output(response, "請(qǐng)求太頻繁!");
? ? ? ? ? ? ? ? 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ù)嘗試。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- 關(guān)于springboot忽略接口,參數(shù)注解的使用ApiIgnore
- SpringBoot3使用?自定義注解+Jackson實(shí)現(xiàn)接口數(shù)據(jù)脫敏的步驟
- spring?boot?使用?@Scheduled?注解和?TaskScheduler?接口實(shí)現(xiàn)定時(shí)任務(wù)
- SpringBoot使用自定義注解+AOP+Redis實(shí)現(xiàn)接口限流的實(shí)例代碼
- springboot中shiro使用自定義注解屏蔽接口鑒權(quán)實(shí)現(xiàn)
- Spring排序機(jī)制之接口與注解的使用方法
相關(guān)文章
Spring中的@RestControllerAdvice注解使用解析
這篇文章主要介紹了Spring中的@RestControllerAdvice注解使用解析,@RestControllerAdvice?是?Spring?框架中一個(gè)用于統(tǒng)一處理控制器異常和返回結(jié)果的注解,它可以被用來(lái)定義全局異常處理程序和全局響應(yīng)結(jié)果處理程序,需要的朋友可以參考下2024-01-01
java計(jì)算代碼段執(zhí)行時(shí)間的詳細(xì)代碼
java里計(jì)算代碼段執(zhí)行時(shí)間可以有兩種方法,一種是毫秒級(jí)別的計(jì)算,另一種是更精確的納秒級(jí)別的計(jì)算,這篇文章主要介紹了java計(jì)算代碼段執(zhí)行時(shí)間,需要的朋友可以參考下2022-08-08
SpringBoot實(shí)現(xiàn)分庫(kù)分表
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)分庫(kù)分表,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
使用JAXBContext輕松實(shí)現(xiàn)Java和xml的互相轉(zhuǎn)換方式
這篇文章主要介紹了依靠JAXBContext輕松實(shí)現(xiàn)Java和xml的互相轉(zhuǎn)換方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
解決@RequestMapping和@FeignClient放在同一個(gè)接口上遇到的坑
這篇文章主要介紹了解決@RequestMapping和@FeignClient放在同一個(gè)接口上遇到的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
Java基礎(chǔ)之JDBC的數(shù)據(jù)庫(kù)連接與基本操作
這篇文章主要介紹了Java基礎(chǔ)之JDBC的數(shù)據(jù)庫(kù)連接與基本操作,文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)java基礎(chǔ)的小伙伴們也有很好的幫助,需要的朋友可以參考下2021-05-05
解決springboot項(xiàng)目啟動(dòng)失敗Could not initialize class&
這篇文章主要介紹了解決springboot項(xiàng)目啟動(dòng)失敗Could not initialize class com.fasterxml.jackson.databind.ObjectMapper問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06
SpringBoot常用注解@RestControllerAdvice詳解
這篇文章主要介紹了SpringBoot常用注解@RestControllerAdvice詳解,@RestControllerAdvice是一個(gè)組合注解,由@ControllerAdvice、@ResponseBody組成,而@ControllerAdvice繼承了@Component,因此@RestControllerAdvice本質(zhì)上是個(gè)Component,需要的朋友可以參考下2024-01-01
Java?Hibernate中一對(duì)多和多對(duì)多關(guān)系的映射方式
Hibernate是一種Java對(duì)象關(guān)系映射框架,支持一對(duì)多和多對(duì)多關(guān)系的映射。一對(duì)多關(guān)系可以使用集合屬性和單向/雙向關(guān)聯(lián)來(lái)映射,多對(duì)多關(guān)系可以使用集合屬性和中間表來(lái)映射。在映射過(guò)程中,需要注意級(jí)聯(lián)操作、延遲加載、中間表的處理等問(wèn)題2023-04-04

