在實(shí)際應(yīng)用中,只需要根據(jù)場景需求選擇對應(yīng)的限流機(jī)制,即可非常方便的進(jìn)行限流操作。這種靈活性和便捷性,也是SpringBoot中定義Starter的一般套路。
很早以前,我曾寫過兩篇介紹如何在SpringBoot中使用Guava和Redis實(shí)現(xiàn)接口限流的文章。具體包括:
- 使用Guava實(shí)現(xiàn)單機(jī)令牌桶限流
- 使用Redis實(shí)現(xiàn)分布式限流
現(xiàn)在,一個(gè)問題擺在我們面前:如何將這兩種限流機(jī)制整合到同一個(gè)組件中,以便用戶隨時(shí)切換呢?
顯然,我們需要定義一個(gè)通用的限流組件,將其引入到業(yè)務(wù)中,并支持通過配置文件自由切換不同的限流機(jī)制。舉例而言,當(dāng)使用limit.type=redis時(shí),啟用Redis分布式限流組件,當(dāng)使用limit.type=local時(shí),啟用Guava限流組件。這種自由切換機(jī)制能夠?yàn)橛脩籼峁└蟮撵`活性和可維護(hù)性。
接下來,讓我們開始動(dòng)手實(shí)現(xiàn)吧!
第一步,創(chuàng)建通用模塊cloud-limiter-starter
首先在父項(xiàng)目下創(chuàng)建一個(gè)模塊

然后在pom文件中引入相關(guān)依賴
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!--SpringFramework-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
小提示:通用模塊命名最好遵照規(guī)則以starter命名結(jié)束,同時(shí)通用模塊引入的依賴最好設(shè)置<scope>provided</scope>屬性。
第二步,實(shí)現(xiàn)限流功能
- 創(chuàng)建限流接口
既然有兩種限流機(jī)制,按照套路肯定得先創(chuàng)建一個(gè)限流接口,就叫LimiterManager吧。
public interface LimiterManager {
boolean tryAccess(Limiter limiter);
}
- 分別實(shí)現(xiàn)Redis的限流功能和Guava的限流功能,這里只給出核心代碼。
Guava限流的核心實(shí)現(xiàn)GuavaLimiter
@Slf4j
public class GuavaLimiter implements LimiterManager{
private final Map<String, RateLimiter> limiterMap = Maps.newConcurrentMap();
@Override
public boolean tryAccess(Limiter limiter) {
RateLimiter rateLimiter = getRateLimiter(limiter);
if (rateLimiter == null) {
return false;
}
boolean access = rateLimiter.tryAcquire(1,100, TimeUnit.MILLISECONDS);
log.info("{} access :{}",limiter.getKey() , access);
return access;
}
}
Redis限流的核心實(shí)現(xiàn)RedisLimiter
@Slf4j
public class RedisLimiter implements LimiterManager{
private final StringRedisTemplate stringRedisTemplate;
public RedisLimiter(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryAccess(Limiter limiter) {
String key = limiter.getKey();
if (StringUtils.isEmpty(key)) {
throw new LimiterException( "redis limiter key cannot be null" );
}
List<String> keys = new ArrayList<>();
keys.add( key );
int seconds = limiter.getSeconds();
int limitCount = limiter.getLimitNum();
String luaScript = buildLuaScript();
RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
Long count = stringRedisTemplate.execute( redisScript, keys, "" + limitCount, "" + seconds );
log.info( "Access try count is {} for key={}", count, key );
return count != null && count != 0;
}
}
第三步,創(chuàng)建配置類
編寫配置類根據(jù)配置文件注入限流實(shí)現(xiàn)類,當(dāng)配置文件中屬性 limit.type=local 時(shí)啟用Guava限流機(jī)制,當(dāng)limit.type=redis 時(shí)啟用Redis限流機(jī)制。
@Configuration
public class LimiterConfigure {
@Bean
@ConditionalOnProperty(name = "limit.type",havingValue = "local")
public LimiterManager guavaLimiter(){
return new GuavaLimiter();
}
@Bean
@ConditionalOnProperty(name = "limit.type",havingValue = "redis")
public LimiterManager redisLimiter(StringRedisTemplate stringRedisTemplate){
return new RedisLimiter(stringRedisTemplate);
}
}
第四步,創(chuàng)建AOP
根據(jù)前面的兩篇文章可知,避免限流功能污染業(yè)務(wù)邏輯的最好方式是借助Spring AOP,所以很顯然還得需要?jiǎng)?chuàng)建一個(gè)AOP。
@Aspect
@EnableAspectJAutoProxy(proxyTargetClass = true) //使用CGLIB代理
@Conditional(LimitAspectCondition.class)
public class LimitAspect {
@Setter(onMethod_ = @Autowired)
private LimiterManager limiterManager;
@Pointcut("@annotation(com.jianzh5.limit.aop.Limit)")
private void check() {
}
@Before("check()")
public void before(JoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Limit limit = method.getAnnotation(Limit.class);
if(limit != null){
Limiter limiter = Limiter.builder().limitNum(limit.limitNum())
.seconds(limit.seconds())
.key(limit.key()).build();
if(!limiterManager.tryAccess(limiter)){
throw new LimiterException( "There are currently many people , please try again later!" );
}
}
}
}
注意到類上我加了一行@Conditional(LimitAspectCondition.class),使用了自定義條件選擇器,意思是只有當(dāng)配置類中出現(xiàn)了limit.type屬性時(shí)才會加載這個(gè)AOP。
public class LimitAspectCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
//檢查配置文件是否包含limit.type屬性
return conditionContext.getEnvironment().containsProperty(ConfigConstant.LIMIT_TYPE);
}
}
第四步,創(chuàng)建spring.factories文件,引導(dǎo)SpringBoot加載配置類
## AutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoCnotallow=\
com.jianzh5.limit.config.LimiterConfigure,\
com.jianzh5.limit.aop.LimitAspect
完整目錄結(jié)構(gòu)如下:

第五步,在項(xiàng)目中引用限流組件
- 引入依賴
<dependency>
<groupId>com.jianzh5</groupId>
<artifactId>cloud-limit-starter</artifactId>
</dependency>
- 在application.properties中設(shè)置加載的限流組件
如果不配置此屬性則不加載對應(yīng)限流功能。
- 在需要限流的接口上加上注解
@Limit(key = "Limiter:test",limitNum = 3,seconds = 1)
小結(jié)
通過上述步驟,我們已經(jīng)成功實(shí)現(xiàn)了一個(gè)通用限流組件。在實(shí)際應(yīng)用中,只需要根據(jù)場景需求選擇對應(yīng)的限流機(jī)制,即可非常方便的進(jìn)行限流操作。這種靈活性和便捷性,也是SpringBoot中定義Starter的一般套路。
如果你想詳細(xì)了解這兩種限流機(jī)制的原理,可以參考之前的文章中所介紹的內(nèi)容。