一个高并发系统中不得不面临的一个方面流量,过大的流量可能导致接口不可用,甚至可能拖慢整个服务,最终导致整改服务不可用。因此,当系统流量增大到一定程度时,就需要考虑如何限流了。
一、限流算法
1)计数器
通过限制总并发数来限流。
假如我们需要限制一个接口一分钟内只能请求100次,首先设置一个一分钟重置请求次数的计数器counter,当接口接到一个请求后,counter就自动加1。如果counter的值大于100的话,就拦截该请求。
2)漏桶算法
漏桶算法可以控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。
漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶为空,则不需要流出水滴,如果漏桶溢出,那么水滴会被溢出丢弃。
3)令牌桶算法
有一个固定容量的桶,桶里存放着令牌(token)。桶一开始是空的,token以一个固定的速度往桶里填充,直到达到桶的容量,多余的令牌将会被丢弃。每当一个请求过来时,就会尝试从桶里移除一个令牌,如果没有令牌的话,请求无法通过。
二、限流实现
1)redis+lua
通过redis
@Inherited
@Documented
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
int count() default 100;
int second() default 1;
String key() default "";
}
@Aspect
@Configuration
public class LimitInterceptor {
private final RedisTemplate<String, Serializable> limitRedisTemplate;
@Autowired
public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {
this.limitRedisTemplate = limitRedisTemplate;
}
@Around("execution(public * *(..)) && @annotation(com.test.annotation.AccessLimit)")
public Object interceptor(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
AccessLimit limitAnnotation = method.getAnnotation(AccessLimit.class);
int limitSecond = limitAnnotation.second();
int limitCount = limitAnnotation.count();
String key = limitAnnotation.key();
ImmutableList<String> keys = ImmutableList.of(key);
try {
RedisScript<Number> redisScript = new DefaultRedisScript<>(buildLuaScript(), Number.class);
Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitSecond);
if (count != null && count.intValue() <= limitCount) {
return pjp.proceed();
} else {
throw new RuntimeException("Access limit");
}
} catch (Throwable e) {
if (e instanceof RuntimeException) {
throw new RuntimeException(e.getLocalizedMessage());
}
throw new RuntimeException("server exception");
}
}
public String buildLuaScript() {
StringBuilder sb = new StringBuilder();
//定义c
sb.append("local c");
//获取redis中的值
sb.append("\nc = redis.call('get',KEYS[1])");
//如果调用不超过最大值
sb.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");
//直接返回
sb.append("\n return c;");
//结束
sb.append("\nend");
//访问次数加一
sb.append("\nc = redis.call('incr',KEYS[1])");
//如果是第一次调用
sb.append("\nif tonumber(c) == 1 then");
//设置对应值的过期设置
sb.append("\nredis.call('expire',KEYS[1],ARGV[2])");
//结束
sb.append("\nend");
//返回
sb.append("\nreturn c;");
return sb.toString();
}
}
2)Guava RateLimiter
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.1-jre</version>
</dependency>
// 每秒只发出10个令牌,此处是单进程服务的限流,内部采用令牌捅算法实现
private static RateLimiter rateLimiter = RateLimiter.create(10.0);
public Object around(ProceedingJoinPoint joinPoint) {
Boolean flag = rateLimiter.tryAcquire();
Object obj = null;
try {
if(flag){
obj = joinPoint.proceed();
}
} catch (Throwable e) {
e.printStackTrace();
}
return obj;
}
3)Sentinel
Sentinel是阿里巴巴开源的限流器熔断器,并且带有可视化操作界面。支持流量控制、熔断降级、系统负载保护等多种功能。
引入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>0.2.0.RELEASE</version>
</dependency>
配置(nacos)
spring.cloud.sentinel.transport.port=8765
spring.cloud.sentinel.transport.dashboard=127.0.0.1:8090spring.cloud.sentinel.datasource.ds.nacos.server-addr=127.0.0.1:8848
#nacos中存储规则的dataId
spring.cloud.sentinel.datasource.ds.nacos.dataId=product-flow-rules
#nacos中存储规则的groupId
spring.cloud.sentinel.datasource.ds.nacos.groupId=SENTINEL_GROUP
#定义存储的规则类型
spring.cloud.sentinel.datasource.ds.nacos.rule-type=flow
实现
@SentinelResource("createProduct")
@RequestMapping(value = "/createProduct", method = RequestMethod.POST)
@ResponseBody
public String createProduct(@RequestBody String product) {
return null;
}