降级和限流

降级和限流

薛定谔的汪

前言

高并发三把利器:缓存、降级、限流

缓存可以用来解决访问速度的问题,提高系统的吞吐量,减少数据库压力,分布式系统中,常用的缓存有 Redis、MongoDB 等。

本篇主要总结下降级和限流相关知识点笔记,用以提醒自己看到这些点后知道去哪里翻代码。

降级

人工降级 针对核心服务,配置相应开关

自动降级

​ 通过熔断实现,如SpringCloud中的hystrix,dubbo 的 mock 等。

​ 通过限流实现降级,比如当某个业务访问量过大时,可以通过限流的手段加以控制,给被限流的请求返 回友好提示或者固定页面。

限流

限流的目的是为了保护系统能够稳定地运行

限流算法

滑动窗口

TCP 流量整形手段,通过配置滑动窗口的大小来实现限流的目的。

图形演示地址:

http://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanimations/selective-repeat-protocol/

计数器

计数器限流算法是比较常用一种的限流方案也是最为粗暴直接的,主要用来限制总并发数,比如数据库连接池大小、线程池大小、接口访问并发数等都是使用计数器算法。

漏桶算法 leaky bucket

当桶满后,常见的两种处理方式:

  1. 暂时拦截住上方注水,等待漏桶中的一部分水漏走后,再放行注水。
  2. 溢出的水直接抛弃。
漏桶算法特点

漏水的速率恒定,即使出现注水速率突然增大的情况,漏水的速率依然不变,所以演变出了另一种算法—令牌桶

令牌桶 tokenbucket

能够解决流量突发情况,是限流中最常用的办法。

令牌桶由三部分组成,令牌流、数据流、令牌桶,原理如图所示:

令牌桶算法的好处是,在请求来之前桶里预备好了一些令牌(桶满后不再生成令牌),当流量小时,获取令牌的速度小于或等于令牌生成的速度;当流量突发时,请求速率大于令牌生成速率,桶里预备的令牌也可以应付一阵子,当流量再次变小时,令牌桶继续存储预备令牌。

限流实际应用

  1. 谷歌开源的 Guava 的RateLimiter是单机版的限流实现,有两种方式:WarmUp 和 Bursty

WarmUp:使用leaky bucket 算法

Bursty: 使用令牌桶算法

  1. Redis+Lua脚本实现分布式限流

    Lua脚本如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    local c = redis.call('incr', KEYS[1])
    if tonumber(c) == 1 then
    redis.call('expire', KEYS[1], ARGV[1])
    return 1
    elseif tonumber(c) > tonumber(ARGV[2]) then
    return 0
    else
    return 1
    end

    解释:

    KEYS[1]:被限制的对象key,可以是某个方法,或者ip等

    ARGV[1]:第一个Value,传 超时时间 timeout,单位 s;

    ARGV[2]第二个 Value,传限制次数 limitCount。

    第一次使用incr对KEY加一,如果是第一次访问,使用expire设置一个超时时间,返回 1-成功。如果现在递增到值大于限制次数,返回 0-失败,否则返回 1-成功。

    Lua脚本在 Redis 中执行是原子性的。

我写了个分布式限流的简单 Demo:

application.yml 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
spring:
redis:
host: localhost
port: 7000
password: Yakai2018@
database: 0
jedis:
pool:
#最大连接数(负值表示没有限制)
max-active: 100
#最大空闲链接
max-idle: 10
#最小空闲链接
min-idle: 5
#最大阻塞时间 负值表示不限制
max-wait: -1ms
#限流lua脚本
redis-limit-lua: |
local c = redis.call('INCR', KEYS[1])
if tonumber(c) == 1 then
redis.call('expire', KEYS[1], ARGV[1])
return 1
elseif tonumber(c) > tonumber(ARGV[2]) then
return 0
else
return 1
end

LimitType 限制类型

1
2
3
4
5
6
7
8
9
10
11
public enum  LimitType {
/**
* 对 ip 限流
*/
IP,

/**
* 对方法限流
*/
METHOD
}

RateLimiter 限流注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RateLimiter {

/**
* 时间段
*/
int period();

/**
* 限制的次数
*/
int limitCount();

/**
* 默认对 IP 限流
* @return
*/
LimitType limitType() default LimitType.IP;
}

RedisConfig 配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory factory;

@Bean
public RedisTemplate<String, Serializable> redisTemplate() {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}

}

LimitAspect 限流注解切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@Slf4j
@Aspect
@Component
public class LimitAspect {

private static String redisLimitLua;

/**
* 使用静态
*/
@Value("${redis-limit-lua}")
public void setRedisLimitLua(String limitLua){
redisLimitLua = limitLua;
}

@Autowired
private RedisTemplate<String,Serializable> redisTemplate;


@Around("within(cn.zhenyk.limit.controller.*) && @annotation(limit)")
public Object interceptor(ProceedingJoinPoint pjp, RateLimiter limit) throws Throwable{
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
LimitType limitType = limit.limitType();
String key;
int limitCount = limit.limitCount();
int period = limit.period();
switch (limitType){
case IP:
key = getIpAddress();
break;
case METHOD:
key = method.getName();
break;
default:
key = getIpAddress();
}
List<String> keys = Collections.singletonList(key);
try {
RedisScript<Number> redisScript = new DefaultRedisScript<>(redisLimitLua, Number.class);
Number c = redisTemplate.execute(redisScript, keys, period, limitCount);
if(c.intValue() == 1){
Object proceed = pjp.proceed();
log.info("未被限流,正常执行:{}", proceed);
return proceed;
}else {
log.info("被限流");
return "被限流";
}
}catch (Exception e){
log.error("执行发生异常:",e);
return "系统错误";
}

}

private String getIpAddress() {

HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}

}

TestController测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class TestController {
/**
* @method: limit
* @description: 10秒内访问次数超过3次则限流
* @param
*/
@RequestMapping("limit")
@RateLimiter(period = 10,limitCount = 3)
public String limit(){
return "OK";
}
}

测试结果

1
2
3
4
5
6
7
8
9
//10秒内调用5次
未被限流,正常执行:OK
未被限流,正常执行:OK
未被限流,正常执行:OK
被限流
被限流
//10秒后再调用
未被限流,正常执行:OK
未被限流,正常执行:OK

项目地址:https://github.com/zhengyakai/limit

  • Title: 降级和限流
  • Author: 薛定谔的汪
  • Created at : 2018-09-16 18:01:54
  • Updated at : 2023-11-17 19:37:37
  • Link: https://www.zhengyk.cn/2018/09/16/framework/limit-degrade/
  • License: This work is licensed under CC BY-NC-SA 4.0.