使用Java自定義注解實(shí)現(xiàn)一個(gè)簡單的令牌桶限流器
什么是令牌桶限流?
令牌桶限流是一種常用的限流算法,它基于令牌桶的概念。在令牌桶中,令牌以固定的速率被生成并放置其中。當(dāng)一個(gè)請求到達(dá)時(shí),它必須獲取一個(gè)令牌才能繼續(xù)執(zhí)行,否則將被阻塞或丟棄。
開始我們的實(shí)現(xiàn)
第一步:創(chuàng)建一個(gè)自定義注解
我們首先需要?jiǎng)?chuàng)建一個(gè)自定義注解,用于標(biāo)識(shí)需要進(jìn)行限流的方法。這個(gè)注解可以命名為@RateLimit
,它可以帶有以下幾個(gè)參數(shù)
rate
: 表示該方法的限流速率,單位可以是每秒請求數(shù)(QPS)。prefixKey
: 針對不同方法上對同一個(gè)資源做限流的情況。target
: 限流的對象,默認(rèn)使用spEl表達(dá)式對入?yún)⑦M(jìn)行獲取capacity
: 令牌桶容量,滿了之后令牌不再增加
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { /** * key的前綴,默認(rèn)取方法全限定名,除非我們在不同方法上對同一個(gè)資源做頻控,就自己指定 */ String prefixKey() default ""; /** * 選擇目標(biāo)類型: 1.EL,需要在spEl()中指定限流資源。2.USER,針對用戶進(jìn)行限流 */ Target target() default Target.EL; /** * springEl表達(dá)式 指定頻控對象 */ String spEl() default ""; /** * 令牌桶容量 */ double capacity() default 10; /** * 令牌生成速率 n/秒 */ double rate() default 1; enum Target { EL, USER } }
第二步:實(shí)現(xiàn)限流邏輯
接下來,我們需要編寫一個(gè)類來處理限流邏輯。這個(gè)類可以命名為RateLimitAspect
,它將會(huì)掃描所有被@RateLimit
注解標(biāo)記的方法,并在必要時(shí)進(jìn)行限流。
/** * 令牌桶限流 * * @date 2023/07/07 */ @Aspect @Component @Slf4j @RequiredArgsConstructor public class RateLimitAspect { @Resource private RedisTemplate<String, BucketLog> redisTemplate; @Resource private RbacUserService rbacUserService; private final SpELUtil spELUtil; @Around("@annotation(com.netease.fuxi.config.annotation.RateLimit)") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); RateLimit[] annotations = method.getAnnotationsByType(RateLimit.class); var var1 = new HashMap<String, RateLimit>(); for (int i = 0; i < annotations.length; i++) { RateLimit annotation = annotations[i]; final String prefix = StringUtils.isBlank(annotation.prefixKey()) ? method.getDeclaringClass() + "#" + method.getName() + ":index:" + i : annotation.prefixKey(); String key = ""; switch (annotation.target()) { case EL: key = spELUtil.getArgValue(annotation.spEl(), joinPoint); break; case USER: key = rbacUserService.getCurrentUser(); } var1.put(prefix + ":" + key, annotation); } var1.forEach((k, v) -> { var var2 = Boolean.TRUE.equals(redisTemplate.hasKey(k)) ? redisTemplate.opsForValue().get(k) : new BucketLog(v.capacity(), Instant.now().getEpochSecond()); long nowTime = Instant.now().getEpochSecond(); double addTokens = (nowTime - var2.getLastRefillTime()) * v.rate(); // 如果生成的令牌超過的桶最大容量,那么令牌數(shù)取桶最大容量 var2.setTokens(Math.min(var2.getTokens() + addTokens, v.capacity())); var2.setLastRefillTime(nowTime); double remain = var2.getTokens() - 1; if (remain < 0) { throw new BusinessException("操作太頻繁,請稍后重試", 42901); } var2.setTokens(remain); long timeout = (long) Math.ceil(v.capacity() / v.rate());// redis過期時(shí)間設(shè)置大于 容量/速率 redisTemplate.opsForValue().set(k, var2, timeout, TimeUnit.SECONDS); }); return joinPoint.proceed(); } }
SpEL解析工具類
@Component public class SpELUtil { /** * 獲取表達(dá)式中的參數(shù)值 * * @param expr 表達(dá)式 * @param joinPoint 切點(diǎn) * @return 參數(shù)值 */ public String getArgValue(String expr, JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); String[] parameterNames = getParameterNames(joinPoint); EvaluationContext context = new StandardEvaluationContext(); for (int i = 0; i < parameterNames.length; i++) { context.setVariable(parameterNames[i], args[i]); } ExpressionParser parser = new SpelExpressionParser(); return parser.parseExpression(expr).getValue(context, String.class); } /** * 獲取方法的參數(shù)名稱 * * @param joinPoint 切點(diǎn) * @return 參數(shù)名稱 */ private String[] getParameterNames(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String[] parameterNames = signature.getParameterNames(); if (parameterNames == null || parameterNames.length == 0) { ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); parameterNames = parameterNameDiscoverer.getParameterNames(signature.getMethod()); } return parameterNames; } }
第三步:在方法上使用@RateLimit注解
現(xiàn)在,我們可以在需要進(jìn)行限流的方法上使用@RateLimited
注解,指定相應(yīng)的限流速率。
示例1: 限制了令牌桶容量10,每10秒生成一個(gè)令牌,限制對象為當(dāng)前用戶。
@Api(tags = "項(xiàng)目服務(wù)") @Validated @Slf4j @RestController @RequestMapping("/api/v1") @RequiredArgsConstructor public class ProjectController { @Resource private IProjectService projectService; @ApiOperation("創(chuàng)建項(xiàng)目") @PostMapping("/project") @RateLimit(capacity = 10, rate = 0.1, target = RateLimit.Target.USER) public Result<ProjectVO> createProject() { ProjectVO projectVO = projectService.createProject(); return Result.ok(projectVO); } }
示例2: 限制了令牌桶容量1,每2秒生成一個(gè)令牌,限制對象為該項(xiàng)目。
@Slf4j @RestController @RequestMapping("/api/v1") public class ProjectController { @Resource private IProjectService projectService; @ApiOperation("數(shù)據(jù)導(dǎo)出") @PostMapping("/project/{projectId}/export") @RateLimit(capacity = 1, rate = 0.5, spEl = "#projectId") public Result<Void> export(@PathVariable String projectId, @RequestBody @Valid ExportDTO exportDTO) { projectService.export(projectId, exportDTO); return Result.ok(); } }
總結(jié)
通過使用Java自定義注解,我們成功地實(shí)現(xiàn)了一個(gè)簡單的令牌桶限流器。這個(gè)限流器可以方便地應(yīng)用于需要對訪問速率進(jìn)行控制的方法中,保證系統(tǒng)的穩(wěn)定性和可靠性。
在實(shí)際項(xiàng)目中,我們可以根據(jù)需求對限流器進(jìn)行進(jìn)一步地?cái)U(kuò)展和優(yōu)化,以滿足不同場景下的限流需求。希望本文對你理解和實(shí)現(xiàn)限流算法有所幫助!
到此這篇關(guān)于基于Java自定義注解實(shí)現(xiàn)一個(gè)令牌桶限流的文章就介紹到這了,更多相關(guān)Java實(shí)現(xiàn)令牌桶限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一文搞懂SpringMVC中@InitBinder注解的使用
@InitBinder方法可以注冊控制器特定的java.bean.PropertyEditor或Spring Converter和 Formatter組件。本文通過示例為大家詳細(xì)講講@InitBinder注解的使用,需要的可以參考一下2022-06-06SpringBoot MongoDB 索引沖突分析及解決方法
這篇文章主要介紹了SpringBoot MongoDB 索引沖突分析及解決方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-11-11mybatis查詢實(shí)現(xiàn)返回List<Map>類型數(shù)據(jù)操作
這篇文章主要介紹了mybatis查詢實(shí)現(xiàn)返回List<Map>類型數(shù)據(jù)操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11jar包運(yùn)行一段時(shí)間后莫名其妙掛掉線上問題及處理方案
這篇文章主要介紹了jar包運(yùn)行一段時(shí)間后莫名其妙掛掉線上問題及處理方案,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09Java Scaner類詳解_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
Java.util.Scanner是Java5.0的新特征,主要功能是簡化文本掃描。下面通過本文給大家分享java scaner類相關(guān)知識(shí),需要的朋友下吧2017-04-04Java使用ByteBuffer進(jìn)行多文件合并和拆分的代碼實(shí)現(xiàn)
因?yàn)轵?yàn)證證書的需要,需要把證書文件和公鑰給到客戶,考慮到多個(gè)文件交互的不便性,所以決定將2個(gè)文件合并成一個(gè)文件交互給客戶,但是由于是加密文件,采用字符串形式合并后,拆分后文件不可用,本文給大家介紹了Java使用ByteBuffer進(jìn)行多文件合并和拆分,需要的朋友可以參考下2024-09-09Java動(dòng)態(tài)編譯執(zhí)行代碼示例
這篇文章主要介紹了Java動(dòng)態(tài)編譯執(zhí)行代碼示例,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12解決JDBC連接Mysql長時(shí)間無動(dòng)作連接失效的問題
這篇文章主要介紹了解決JDBC連接Mysql長時(shí)間無動(dòng)作連接失效的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-03-03