.markdown-body{position:relative;word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#282d36}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin-bottom:10px;padding-bottom:5px}.markdown-body h1{font-size:30px;margin-bottom:5px;color:#2f845e}.markdown-body h2{font-size:24px;display:inline-block;font-weight:700;background:#2f845e;color:#fff;padding:6px 8px 0 0;border-top-right-radius:6px;margin-right:2px;box-shadow:6px 3px 0 0 rgba(47,132,194,.2)}.markdown-body h2:before{content:" ";display:inline-block;width:8px}.markdown-body h2:after{content:" ";position:absolute;display:block;width:calc(100% - 40px);border-bottom:3px solid #2f845e}.markdown-body h3{font-size:18px;padding-bottom:0}.markdown-body h4{font-size:16px}.markdown-body h5{font-size:15px}.markdown-body h6{margin-top:5px}.markdown-body p{line-height:inherit;margin-top:22px;margin-bottom:22px}.markdown-body img{max-width:100%;box-shadow:6px 6px 6px #888}.markdown-body hr{border:none;border-top:1px solid #ddd;margin-top:32px;margin-bottom:32px}.markdown-body code{word-break:break-word;border-radius:2px;overflow-x:auto;background-color:#fff5f5;color:#ff502c;font-size:.87em;padding:.065em .4em}.markdown-body code,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative;line-height:1.75;border-top:6px solid #2f845e}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#262626;background:linear-gradient(180deg,rgba(66,185,131,.1),transparent)!important}.markdown-body strong{background-color:inherit;color:#2f845e}.markdown-body em{background-color:inherit;color:#949415}.markdown-body a{text-decoration:none;color:#2f8e54;border-bottom:1px solid #3f9e64}.markdown-body a:active,.markdown-body a:hover{color:#3f9e64}.markdown-body a[class^=footnote]{margin-left:4px}.markdown-body table{display:inline-block!important;font-size:12px;width:100%;max-width:100%;overflow:auto;border:2px solid #2f8e54}.markdown-body thead{background:#2f8e54;color:#fff;text-align:left;font-weight:700}.markdown-body tr:nth-child(2n){background-color:rgba(153,255,188,.1)}.markdown-body td,.markdown-body th{width:100%;padding:12px 7px;line-height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{padding:1px 22px;margin:22px 0;border-left:6px solid #2f845e;background-color:rgba(66,185,131,.1);border-radius:4px}.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body ol,.markdown-body ul{padding-left:28px}.markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item{list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li .task-list-item ol,.markdown-body ul li .task-list-item ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:3px}.markdown-body ol li{padding-left:6px}.markdown-body del{color:#2f845e}@media (max-width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-size:18px}}
这是我参与更文挑战的第20天,活动详情查看:更文挑战
一、前言
对应的限流算法可以看下这篇:链接
限流可以应对:
- 热点业务带来的突发请求;
- 调用方
bug
导致的突发请求; - 恶意攻击请求。
之前默认的限流方式都是单机下,例如使用 Guava
的 RateLimiter
。
如果部署了多个实例,则需要使用分布式限流。
例如,部署多个网关实例,实现分布式限流。
基于 Redis
和 lua
脚本实现限流算法:
- 计数模式(固定窗口模式)
- 令牌桶算法
Tips
: 为减少多次请求(I/O
),使用 lua
脚本,实现多次操作。
二、方案实现
以下方案均在网关gateway
(Spring Cloud Alibaba
) 基础上实现。
(1)计数算法
主要步骤:
- 创建
lua
脚本 - 在网关添加
filter
- 测试
创建 resource/limit.lua
:
local key = KEYS[1] --限流KEY(一秒一个)
local limit = tonumber(ARGV[1]) --限流大小
local current = tonumber(redis.call("get", key)) or 0
if current + 1 > limit then --如果超出限流大小
return 0
else --请求数+1,并设置n秒过期
redis.call("INCRBY", key, "1")
redis.call("expire", key, ARGV[2])
return 1
end
复制代码
/**
* 获取持续时间,1秒内限制请求,则duration设置2
*
* @return 返回缓存保存的值。
*/
public int fetchDuration() {
// 默认值为 1
Integer durationSeconds = this.durationSeconds;
if (durationSeconds == null || durationSeconds < 1) {
durationSeconds = 1;
}
// 1秒内限制请求,则duration设置2
return durationSeconds + 1;
}
复制代码
public boolean acquire() {
String key = "lmt:" + "123";
int limitCount = 10;
int duration = 2;
Object result = redisTemplate.execute(
new RedisScript() {
@Override
public String getSha1() {
return limitScriptSha1;
}
@Override
public Class getResultType() {
return Long.class;
}
@Override
public String getScriptAsString() {
return limitScript;
}
},
// KEYS[1] key
Collections.singletonList(key),
// ARGV[1] limit
String.valueOf(limitCount),
// ARGV[2] expire
String.valueOf(duration)
);
return REDIS_SUCCESS.equals(result);
}
复制代码
(2)令牌桶算法
令牌桶算法需要在
Redis
中存储桶的大小、当前令牌数量,并且实现每隔一段时间添加新的令牌。最简单的办法当然是每隔一段时间请求一次Redis
,将存储的令牌数量递增。
但实际上我们可以通过对限流两次请求之间的时间和令牌添加速度来计算得出上次请求之后到本次请求时,令牌桶应添加的令牌数量。因此我们在 Redis
中只需要存储上次请求的时间和令牌桶中的令牌数量,而桶的大小和令牌的添加速度可以通过参数传入实现动态修改。
由于第一次运行脚本时默认令牌桶是满的,因此可以将数据的过期时间设置为令牌桶恢复到满所需的时间,及时释放资源。
这里使用 gateway
默认配置来测试下:
- 在配置文件中做以下的配置:
server:
port: 8081
spring:
cloud:
gateway:
routes:
- id: limit_route
uri: http://httpbin.org:80/get
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@hostAddrKeyResolver}'
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3
application:
name: gateway-limiter
redis:
host: localhost
port: 6379
database: 0
复制代码