一个高并发系统中不得不面临的一个方面流量,过大的流量可能导致接口不可用,甚至可能拖慢整个服务,最终导致整改服务不可用。因此,当系统流量增大到一定程度时,就需要考虑如何限流了。
一、限流算法
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:8090
spring.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;
}