使用Java自定義注解實現(xiàn)一個簡單的令牌桶限流器
什么是令牌桶限流?
令牌桶限流是一種常用的限流算法,它基于令牌桶的概念。在令牌桶中,令牌以固定的速率被生成并放置其中。當一個請求到達時,它必須獲取一個令牌才能繼續(xù)執(zhí)行,否則將被阻塞或丟棄。
開始我們的實現(xiàn)
第一步:創(chuàng)建一個自定義注解
我們首先需要創(chuàng)建一個自定義注解,用于標識需要進行限流的方法。這個注解可以命名為@RateLimit,它可以帶有以下幾個參數(shù)
rate: 表示該方法的限流速率,單位可以是每秒請求數(shù)(QPS)。prefixKey: 針對不同方法上對同一個資源做限流的情況。target: 限流的對象,默認使用spEl表達式對入?yún)⑦M行獲取capacity: 令牌桶容量,滿了之后令牌不再增加
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
/**
* key的前綴,默認取方法全限定名,除非我們在不同方法上對同一個資源做頻控,就自己指定
*/
String prefixKey() default "";
/**
* 選擇目標類型: 1.EL,需要在spEl()中指定限流資源。2.USER,針對用戶進行限流
*/
Target target() default Target.EL;
/**
* springEl表達式 指定頻控對象
*/
String spEl() default "";
/**
* 令牌桶容量
*/
double capacity() default 10;
/**
* 令牌生成速率 n/秒
*/
double rate() default 1;
enum Target {
EL, USER
}
}第二步:實現(xiàn)限流邏輯
接下來,我們需要編寫一個類來處理限流邏輯。這個類可以命名為RateLimitAspect,它將會掃描所有被@RateLimit注解標記的方法,并在必要時進行限流。
/**
* 令牌桶限流
*
* @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è)置大于 容量/速率
redisTemplate.opsForValue().set(k, var2, timeout, TimeUnit.SECONDS);
});
return joinPoint.proceed();
}
}SpEL解析工具類
@Component
public class SpELUtil {
/**
* 獲取表達式中的參數(shù)值
*
* @param expr 表達式
* @param joinPoint 切點
* @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 切點
* @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)在,我們可以在需要進行限流的方法上使用@RateLimited注解,指定相應(yīng)的限流速率。
示例1: 限制了令牌桶容量10,每10秒生成一個令牌,限制對象為當前用戶。
@Api(tags = "項目服務(wù)")
@Validated
@Slf4j
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class ProjectController {
@Resource
private IProjectService projectService;
@ApiOperation("創(chuà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秒生成一個令牌,限制對象為該項目。
@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自定義注解,我們成功地實現(xiàn)了一個簡單的令牌桶限流器。這個限流器可以方便地應(yīng)用于需要對訪問速率進行控制的方法中,保證系統(tǒng)的穩(wěn)定性和可靠性。
在實際項目中,我們可以根據(jù)需求對限流器進行進一步地擴展和優(yōu)化,以滿足不同場景下的限流需求。希望本文對你理解和實現(xiàn)限流算法有所幫助!
到此這篇關(guān)于基于Java自定義注解實現(xiàn)一個令牌桶限流的文章就介紹到這了,更多相關(guān)Java實現(xiàn)令牌桶限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一文搞懂SpringMVC中@InitBinder注解的使用
@InitBinder方法可以注冊控制器特定的java.bean.PropertyEditor或Spring Converter和 Formatter組件。本文通過示例為大家詳細講講@InitBinder注解的使用,需要的可以參考一下2022-06-06
SpringBoot MongoDB 索引沖突分析及解決方法
這篇文章主要介紹了SpringBoot MongoDB 索引沖突分析及解決方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-11-11
mybatis查詢實現(xiàn)返回List<Map>類型數(shù)據(jù)操作
這篇文章主要介紹了mybatis查詢實現(xiàn)返回List<Map>類型數(shù)據(jù)操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11
Java Scaner類詳解_動力節(jié)點Java學(xué)院整理
Java.util.Scanner是Java5.0的新特征,主要功能是簡化文本掃描。下面通過本文給大家分享java scaner類相關(guān)知識,需要的朋友下吧2017-04-04
Java使用ByteBuffer進行多文件合并和拆分的代碼實現(xiàn)
因為驗證證書的需要,需要把證書文件和公鑰給到客戶,考慮到多個文件交互的不便性,所以決定將2個文件合并成一個文件交互給客戶,但是由于是加密文件,采用字符串形式合并后,拆分后文件不可用,本文給大家介紹了Java使用ByteBuffer進行多文件合并和拆分,需要的朋友可以參考下2024-09-09

