系统限流实践 - 分布式限流

本文是根据开涛的博客 聊聊高并发系统之限流特技-1 整理而成,自学笔记第三篇
欢迎访问我的个人博客 http://rayleung.xyz/

目录

1.系统限流实践 - 理论篇
2.系统限流实践 - 应用限流
3.系统限流实践 - 分布式限流
4.系统限流实践 - 接入层限流(上)
5.系统限流实践 - 接入层限流(下*完结)

开篇

上篇学习了应用限流(传送门),接下来学习一下分布式限流的方法

分布式限流

分布式系统也会有限流的需求。分布式服务关键需要把限流实现为原子化,解决方案可以使用Redis+Lua或者Nginx+Lua来实现。

Redis+Lua实现

因为Redis是单线程模型,能确保限流服务是线程安全的。

Lua脚本

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

Java代码

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个,可以看到结果如设想一样

Nginx+Lua实现

安装Openresty Windows

Openresty是一个好东西,它是nginx和lua以及一些第三方模块组成的一个捆绑包,并没有对nginx的源码进行更改。
下面是Openresty的官方介绍,Openresty官网

OpenResty ™ 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

因为是试验环境,我们安装Openresty的Windows版本,GitHub地址在这Openresty Windows Version
安装十分简单,解压后直接运行nginx.exe就搞定

Lua脚本(limit.lua)

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配置文件

添加共享字典配置

在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

欢迎关注个人公众号

你可能感兴趣的:(系统限流实践 - 分布式限流)