本文是根据开涛的博客 聊聊高并发系统之限流特技-1 整理而成,自学笔记第三篇
欢迎访问我的个人博客 http://rayleung.xyz/
1.系统限流实践 - 理论篇
2.系统限流实践 - 应用限流
3.系统限流实践 - 分布式限流
4.系统限流实践 - 接入层限流(上)
5.系统限流实践 - 接入层限流(下*完结)
上篇学习了应用限流(传送门),接下来学习一下分布式限流的方法
分布式系统也会有限流的需求。分布式服务关键需要把限流实现为原子化,解决方案可以使用Redis+Lua或者Nginx+Lua来实现。
因为Redis是单线程模型,能确保限流服务是线程安全的。
local times = redis.call('incr', KEYS[1]) --设置key(KEY[1])并加1
if times == 1 then
redis.call('expire', KEYS[1], ARGV[1]) --设置超时时间
end
if times > tonumber(ARGV[2]) then --限流大小
return 0
end
return 1
public class DistrubuteLimit public Long aquire() throws IOException {
String luaScript = Files.toString(new File("D:\\work\\src\\limit\\src\\main\\java\\distrubute\\limit.lua"), Charset.defaultCharset());
Jedis jedis = new Jedis("localhost", 6379);
// String key = "ip:" + System.currentTimeMillis() / 1000; //此处将当前时间戳取秒数
String key = "ip:" + 1; //此处硬编码时间,保证请求都是在同一秒内发起
String limit = "6"; //限流大小
return (Long) jedis.eval(luaScript, Lists.newArrayList(key), Lists.newArrayList("2", limit));
}
......
}
测试代码
public static void main(String[] args) throws IOException {
final DistrubuteLimit distrubuteLimit = new DistrubuteLimit();
final CountDownLatch latch = new CountDownLatch(1);//两个工人的协作
final Random random = new Random(10);
for (int i = 0; i < 10; i++) {
final int finalI = i;
Thread t = new Thread(new Runnable() {
public void run() {
try {
latch.await();
int sleepTime = random.nextInt(1000);
Thread.sleep(sleepTime);
Long rev = distrubuteLimit.aquire();
if (rev == 1) {
System.out.println("t:" + finalI + ":" + "请求成功");
} else {
System.out.println("t:" + finalI + ":" + "被限流了");
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
});
t.start();
}
latch.countDown();
System.in.read();
}
结果
t:3:请求成功
t:1:请求成功
t:8:请求成功
t:7:请求成功
t:6:请求成功
t:2:请求成功
t:5:被限流了
t:9:被限流了
t:0:被限流了
t:4:被限流了
模拟10个请求,同一时间返回6个,可以看到结果如设想一样
Openresty是一个好东西,它是nginx和lua以及一些第三方模块组成的一个捆绑包,并没有对nginx的源码进行更改。
下面是Openresty的官方介绍,Openresty官网
OpenResty ™ 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
因为是试验环境,我们安装Openresty的Windows版本,GitHub地址在这Openresty Windows Version
安装十分简单,解压后直接运行nginx.exe
就搞定
local locks = require "resty.lock" --lock模块
local limit = 3 --限流大小
local function acquire()
if limit <= 0 then
return 0
end
local lock = locks:new("locks")
local elapsed, err = lock:lock("limit_key") --互斥锁
local limit_counter = ngx.shared.limit_counter --计数器
local key = "ip" .. os.time()
local current = limit_counter:get(key)
if current ~= nil and current + 1 > limit then --如果超出限流大小
lock:unlock()
return 0
end
if current == nil then
limit_counter:set(key, 1, 1) --第一次需要设置过期时间,设置key的值为1,过期时间为1秒
else
limit_counter:incr(key, 1) --第二次开始递增加1
end
lock:unlock()
return 1
end
local rev = acquire()
ngx.log(ngx.ERR, rev)
if rev ~= 1 then
ngx.say("限流了")
else
ngx.say("访问成功")
end
脚本的原理与上一节的原因基本一样,实现中我们使用lua-resty-lock互斥锁模块来解决原子性问题(在实际工程中使用时请考虑获取锁的超时问题),并使用ngx.shared.DICT共享字典来实现计数器
在nginx.conf的http模块里面添加共享字典配置
http {
......
lua_shared_dict locks 10m;
lua_shared_dict limit_counter 10m;
......
}
在nginx.conf的server模块里添加限流的配置
server {
listen 85;
server_name localhost;
......
location /testapi {
#生成内容阶段
content_by_lua_file /work/limit.lua;
header_filter_by_lua 'ngx.header["content-type"] = "application/json; charset=UTF-8"';
}
......
}
这样没当请求过来的时候都会调用limit.lua的脚本来判断当前请求是否超过了限流数。
public class NginxLimit {
public static void main(String[] args) throws IOException {
final NginxLimit distrubuteLimit = new NginxLimit();
final CountDownLatch latch = new CountDownLatch(1);//两个工人的协作
final Random random = new Random(10);
for (int i = 0; i < 5; i++) {
final int finalI = i;
Thread t = new Thread(new Runnable() {
public void run() {
try {
latch.await();
int sleepTime = random.nextInt(1000);
Thread.sleep(sleepTime);
String rev = distrubuteLimit.sendGet("http://localhost:85/testapi", null);
System.out.println(rev);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
latch.countDown();
System.in.read();
}
public static String sendGet(String url, String param) {
String result = "";
BufferedReader in = null;
try {
String urlNameString = url + "?" + param;
URL realUrl = new URL(urlNameString);
// 打开和URL之间的连接
URLConnection connection = realUrl.openConnection();
// 设置通用的请求属性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 建立实际的连接
connection.connect();
// 定义 BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("发送GET请求出现异常!" + e);
e.printStackTrace();
}
// 使用finally块来关闭输入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
}
模拟1秒并发5个请求
访问成功
访问成功
访问成功
限流了
限流了
可以看到结果符合我们预期
以上是分布式限流的实践,主要针对业务上的限流;下一篇我们讲学习接入层的限流,主要使用Nginx来实现。
lua-resty-lock - https://github.com/openresty/lua-resty-lock
nginx-openresty-windows - https://github.com/LomoX-Offical/nginx-openresty-windows
openresty-http://openresty.org/cn/
跟我学Nginx+Lua-http://jinnianshilongnian.iteye.com/blog/2190344
欢迎关注个人公众号