Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。
协同是非常强大的功能,但是用起来也很复杂。
线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。
在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。
协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。
方法 | 描述 |
---|---|
coroutine.create() | 创建 coroutine,返回 coroutine, 参数是一个函数,当和 resume 配合使用的时候就唤醒函数调用 |
coroutine.resume() | 重启 coroutine,和 create 配合使用 |
coroutine.yield() | 挂起 coroutine,将 coroutine 设置为挂起状态 也就是停止了运行 等候再次resume触发事件。唤醒线程继续执行 |
coroutine.status() | 查看 coroutine 的状态 注:coroutine 的状态有三种:dead,suspended,running,具体什么时候有这样的状态请参考下面的程序 |
coroutine.wrap() | 创建 coroutine,返回一个函数,一旦你调用这个函数,就进入 coroutine,和 create 功能重复 |
coroutine.running() | 返回正在跑的 coroutine,一个 coroutine 就是一个线程,当使用running的时候,就是返回一个 corouting 的线程号 |
以下实例演示了以上各个方法的用法:
------------创建线程 方法1
co = coroutine.create(
function(i)
print(i);
end
)
coroutine.resume(co, 1) --启动create创建的线程 结果: 1
print(coroutine.status(co)) -- dead(死亡状态)
------------创建线程 方法2
co = coroutine.wrap(
function(i)
print(i);
end
)
co(1) --调用wrap内函数启动线程 结果 1
co2 = coroutine.create(
function()
for i=1,10 do
print(i)
if i == 3 then
print(coroutine.status(co2)) --running(运行状态)
print(coroutine.running()) --thread:XXXXXX
end
coroutine.yield() --挂起当前线程 等待使用resume唤醒继续执行
end
end
)
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
-- ............. 一直执行10次线程结束
print(coroutine.status(co2)) -- suspended(挂起状态)
resume可以理解为函数调用,并且可以传入参数,激活协同时,参数是传给程序的,唤醒yield时,参数是传递给yield的;
yield就相当于是一个特殊的return语句,只是它只是暂时性的返回(挂起),并且yield可以像return一样带有返回参数,这些参数是传递给resume的。
function foo (a)
print("foo 函数输出", a)
return coroutine.yield(2 * a) -- 返回 2*a 的值
end
co = coroutine.create(function (a , b)
print("第一次协同程序执行输出", a, b) -- 1 10
r=foo(a + 1) --占时挂起
print(r) --第二次协同程序执行
r,s=coroutine.yield(a + b, a - b) -- a,b的值为第一次调用协同程序时传入
print(r, s)
return b, "结束协同程序" -- b的值为第1次调用协同程序时传入
end)
print("---分割线---")
print("co返回", coroutine.resume(co, 1, 10)) -- true, 4
print("--分割线----")
print("co返回", coroutine.resume(co, "第二次协同程序执行")) -- true 11 -9
print("---分割线---")
print("co返回", coroutine.resume(co,"第三次协同程序执行", "xxx")) -- true 10 结束协同程序
print(coroutine.status(co)) --dead(死亡状态)
详细解析:
当执行第一次 coroutine.resume 执行co线程 传入 1 和10
1.
执行到foo 的时候返进入到函数里 执行 coroutine.yield(2/*4) 然后线程被挂起 返回结果 4
1.
当使用coroutine.resume第二次唤醒线程的时候 将 第二次协同程序执行
传入
第一次挂起的coroutine.yield(“第二次协同程序执行”) 里 然后返回给 r 继续往下执行
1.
此刻执行到第二个coroutine.yield(1+10,1-10) 然后线程被挂起 返回结果 11,-9
1.
当使用coroutine.resume第三次唤醒线程的时候 将 第三次协同程序执行
,xxx
传入
第二次挂起的coroutine.yield(“第三次协同程序执行”,"xxx) 里 然后返回给 r ,s 继续往下执行
后面没有coroutine.yield了 线程执行完毕 返回第一次 传入b的值(10) 然后线程进入死亡状态
生产者-消费者问题
newProductor --线程对象
num=10 ---生产10个物品 和消费者接收10个物品
function productor()
local i = 0
while i<num do
i = i + 1
send(i) -- 将生产的物品发送给消费者
end
end
function consumer()
local i = 0
while i<num do
i = i + 1
local i = receive() -- 从生产者那里得到物品
print(i)
end
end
function receive()
local status, value = coroutine.resume(newProductor)
return value
end
function send(x)
coroutine.yield(x) -- x表示需要发送的值,值返回以后,就挂起该协同程序
end
-- 启动程序
newProductor = coroutine.create(productor)
consumer()
print(coroutine.status(newProductor))
结果 :
1
2
3
4
5
6
7
8
9
10
suspended
生产者 生产 10个 消费者消费10个 然后线程结束
Lua I/O 库用于读取和处理文件。分为简单模式(和C一样)、完全模式。
简单模式在做一些简单的文件操作时较为合适。但是在进行一些高级的文件操作的时候,简单模式就显得力不从心。例如同时读取多个文件这样的操作,使用完全模式则较为合适。
在 lua i/o中 错误都是 nil
打开文件操作语句如下:
file = io.open (filename [, mode])
mode 的值有:
模式 | 描述 |
---|---|
r | 以只读方式打开文件,该文件必须存在。 |
w | 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。 |
a | 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留) |
r+ | 以可读写方式打开文件,该文件必须存在。 |
w+ | 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。 |
a+ | 与a类似,但此文件可读可写 |
b | 二进制模式,如果文件是二进制文件,可以加上b |
+ | 号表示对文件既可以读也可以写 |
简单模式
简单模式使用标准的 I/O 或 使用一个当前输入文件 和 一个当前输出文件。
输出文件 使用r(只读的方式) 如果文件不存在 读取失败
-- 以只读方式打开文件
file = io.open("test.lua", "r")
print(file) -- nil 因为文件不存在
创建一个test.txt文件
vi test.txt
内容
1.测试
2.测试xx
3.测试xxxxx
只读 r
-- 以只读方式打开文件
file = io.open("test.txt", "r")
-- 设置默认 输入文件为 test.txt
io.input(file)
-- 输出文件第一行
print(io.read()) --1.测试
-- 关闭打开的文件
io.close(file)
附加 a 若文件不存在则建立该文件。
-- 以附加的方式打开只写文件
file = io.open("test.txt", "a")
-- 设置默认输出文件为 test.txt
io.output(file)
-- 在文件最后一行添加 Lua 注释
io.write("-- test.txt 文件末尾注释")
-- 关闭打开的文件
io.close(file)
然后你看看 test.txt是否在末尾追加内容了
在以上实例中我们使用了 io.xxx 方法,
其中 io.read() 中我们没有带参数(默认 i
模式 )
参数可以是下表中的一个:
模式 | 描述 |
---|---|
“/*n” | 读取一个数字并返回它。例:file.read("/*n") |
“/*a” | 从当前位置读取整个文件。例:file.read("/*a") |
“/*l”(默认) | 从第一行开始 每次读取下一行,在文件尾 处返回 nil。例:file.read("/*l") |
number | 返回一个指定字符个数的字符串,或在 EOF 时返回 nil。例:file.read(5) |
完全模式
通常我们 同一时间读写 处理 一个文件。我们需要使用 file.xxx 来代替 io.xx 方法。以下实例演示了如何同时处理同一个文件:
-- 以读写追加方式打开文件
file = io.open("test.txt", "a+")
-- 输出文件第一行
print(file:read())
-- 在文件最后一行添加 Lua 注释
file:write("--test")
-- 关闭打开的文件
file:close()
然后你看看 test.txt是否在末尾追加内容了
read 的参数与简单模式一致。
快捷读取
开指定的文件filename为读模式并返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回nil,并自动关闭文件。
for line in io.lines("test.txt") do
print(line)
end
改变读写的位置
不想读的时候是从头开始的 不想写的时候是 在末尾追加的
我们可以控制 光标在文件位置
file:seek(optional whence, optional offset): 设置和获取当前文件位置,成功则返回最终的文件位置(按字节),失败则返回nil加错误信息。参数 whence 值可以是:
-- 以只读方式打开文件
file = io.open("test.txt", "r")
file:seek("set",5) --从第5个字节 开始
print(file:read("*a")) --读取全部内容
-- 关闭打开的文件
file:close()
程序运行中错误处理是必要的,在我们进行文件操作,数据转移及web service 调用过程中都会出现不可预期的错误。如果不注重错误信息的处理,就会造成信息泄露,程序无法运行等情况。
任何程序语言中,都需要错误处理。错误类型有:
使用assert 断言的方式调试Debug
assert首先检查第一个参数,若没问题,assert不做任何事情;否则,assert以第二个参数作为错误信息抛出。
function test(a)
assert(type(a) == "number", "a 不是数字")
print("hello world")
end
test("abc")
结果:
stdin:2: a 不是数字
stack traceback:
[C]: in function ‘assert’
stdin:2: in function ‘test’
(…tail calls…)
[C]: in ?
1.
使用 error
终止正在执行的函数,并返回message的内容作为错误信息
function test(a)
error("错误xxxxx")
print("hello world")
end
test("abc")
stdin:2: 错误xxxxx
stack traceback:
[C]: in function ‘error’
stdin:2: in function ‘test’
(…tail calls…)
[C]: in ?
1.
pcall 和 xpcall
pcall
pcall 指的是 protected call
类似其它语言里的 try-catch
, 使用pcall 调用函数,如果函数 f 中发生了错误, 它并不会抛出一个错误,而是返回错误的状态, 为被执行函数提供一个保护模式,保证程序不会意外终止
语法
pcall( f , arg1,···)
返回值
true
false
使用pcall 处理错误
function square(a)
return a * "a"
end
status, retval = pcall(square,10);
if (status) then
print("正确")
else
print("错误:",retval)
end
错误: stdin:2: attempt to mul a ‘number’ with a ‘string’
*
xpcall
xpcall
类似 pcall
但是他的错误处理是交给函数的
返回值
true
false
语法
xpcall (f, msgh [, arg1, ···])
function square(a)
return a * "a"
end
-- 错误处理函数
function error( err )
print( "出错误了ERROR:", err ) --打印错误堆栈
end
status= xpcall(square, error, 10)
print( status) --false
出错误了ERROR: stdin:2: attempt to mul a ‘number’ with a ‘string’
false
模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。
Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。
以下为创建自定义模块 module.lua,文件代码格式如下:
vi module.lua
内容如下
-- 定义一个名为 module 的模块
module = {}
-- 定义一个常量
module.constant = "这是一个常量"
-- 定义一个函数
function module.func1()
io.write("这是一个公有函数!\n")
end
local function func2()
print("这是一个私有函数!")
end
function module.func3()
func2()
end
return module
由上可知,模块的结构就是一个 table 的结构,因此可以像操作调用 table 里的元素那样来操作调用模块里的常量或函数。
上面的 func2 声明为程序块的局部变量,即表示一个私有函数,因此是不能从外部访问模块里的这个私有函数,必须通过模块里的公有函数来调用.
require 函数
Lua提供了一个名为require的函数用来加载模块。要加载一个模块,只需要简单地调用就可以了。例如:
require("<模块名>")
或者
require "<模块名>"
执行 require 后会返回一个由模块常量或函数组成的 table,并且还会定义一个包含该 table 的全局变量。
创建 test.lua 必须和module.lua 在同一个文件夹里
vi test.lua
内容如下:
-- module 模块为上文提到到 module.lua
require("module")
print(module.constant)
module.func3()
lua test.lua
以上代码执行结果为:
这是一个常量
这是一个私有函数!
至于其他的这里就不说了 我们也不怎么使用lua
OpenResty(又称:ngx_openresty) 是一个基于 nginx的可伸缩的 Web 平台,由中国人章亦春发起,提供了很多高质量的第三方模块。
OpenResty 是一个强大的 Web 应用服务器,Web 开发人员可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,更主要的是在性能方面,OpenResty可以 快速构造出足以胜任 10K 以上并发连接响应的超高性能 Web 应用系统。
360,UPYUN,阿里云,新浪,腾讯网,去哪儿网,酷狗音乐等都是 OpenResty 的深度用户。
OpenResty 封装了nginx,并且集成了LUA脚本,开发人员只需要简单的其提供了模块就可以实现相关的逻辑,而不再像之前,还需要在nginx中自己编写lua的脚本,再进行调用了。
而且OpenResty 里还集成了 访问mysql redis 的模块 在OpenResty安装目录下lualib下的resty里
1.添加仓库执行命令
yum install yum-utils
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
2.执行安装
yum install openresty
3.安装成功后 会在默认的目录如下:
ls /usr/local/openresty
openresty 里默认 安装了Nginx
安装目录默认在
/usr/local/openresty
启动nginx
/usr/local/openresty/nginx/sbin/nginx
重启nginx和加载配置文件
/usr/local/openresty/nginx/sbin/nginx -s reload -c /usr/local/openresty/nginx/conf/nginx.conf
/usr/local/openresty/nginx/sbin/nginx -s reload
nginx配置文件
vi /usr/local/openresty/nginx/conf/nginx.conf
vi /usr/local/openresty/nginx/conf/nginx.conf
进入配置文件里将头部 权限改为 user root root;
目的就是将来要使用lua脚本的时候 ,直接可以加载在root下的lua脚本。
在http部分添加如下配置
lua_package_path "/usr/local/openresty/lualib/?.lua;;"; #lua 模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; #c 模块
以上配置如果不配置 那么就会找不到 对应的模块 (路径就是你的 openresty安装路径/lualib 后面是固定的)
然后我们 实现在页面显示 Hello world
vi /usr/local/openresty/nginx/conf/nginx.conf
在nginx.conf中server部分添加如下配置
location /lua {
default_type 'text/html';
content_by_lua 'ngx.say("hello world")';
}
网络访问
curl http://192.168.66.71/lua
结果 hello world
我们使用 lua 脚本文件试试
在/root/lua 下面创建 test.lua文件
mkdir /root/lua
vi /root/lua/test.lua
文件内容
ngx.say("hello worldxxxxxx")
在nginx.conf中server部分添加如下配置
location /luafile {
default_type text/html;
charset utf-8;
content_by_lua_file /root/lua/test.lua;
}
然后重启nginx
/usr/local/openresty/nginx/sbin/nginx -s reload
网络访问
curl http://192.168.66.71/luafile
结果: hello worlxxxxxxd
在docker 中拉取redis镜像 然后 创建容器 配置好端口映射 建议在windows 客户端先测试下看看 能连接成功吗?
不会的话在我博客 docker入门教学
这篇文件里有教学
在/root/lua/下创建redis_test.lua文件
vi /root/lua/redis_test.lua
redis_test.lua 文件内容
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(2000)
local ok, err = red:connect("192.168.66.66", 6379)
if not ok then
ngx.say("failed to connect: ", err)
return
end
ngx.say("set result: ", ok)
ok, err = red:set("dog", "hello world-你好")
if not ok then
ngx.say("set dog error : ", err)
return close_redis(red)
end
local res, err = red:get("dog")
if not res then
ngx.say("failed to get doy: ", err)
return
end
if res == ngx.null then
ngx.say("dog not found.")
return
end
ngx.say("dog: ", res)
--只有数据传输完毕了,才能放到池子里,系统无法帮你自动做这个事情
local ok, err = red:set_keepalive(1000, 100) --设置连接池 超时时间,池的大小(线程的数量)
if not ok then
ngx.say("设置redis线程池失败: ", err)
return
end
然后修改 nginx.conf
vi /usr/local/openresty/nginx/conf/nginx.conf
在nginx.conf中server部分添加如下配置
location /luaredis {
default_type text/html;
charset utf-8;
content_by_lua_file /root/lua/redis_test.lua;
}
然后重启nginx
/usr/local/openresty/nginx/sbin/nginx -s reload
网络访问
curl http://192.168.66.71/luaredis
结果: dog: hello world-你好
在docker 中拉取mysql镜像 然后 创建容器 配置好端口映射 建议在windows 客户端(Navicat)先测试下看看 能连接成功吗?
不会的话在我博客 docker入门教学
这篇文件里有教学
在/root/lua/下创建mysql_test.lua文件
vi /root/lua/mysql_test.lua
mysql_test.lua文件内容
local mysql = require "resty.mysql"
local db, err = mysql:new()
if not db then
ngx.say("实例化mysql失败: ", err)
return
end
--数据库连接超时时间
db:set_timeout(2000)
-- 连接数据库
local ok, err, errno, sqlstate = db:connect{
host = "192.168.66.66", -- ip
port = 3306, --端口
database = "changgou_content", --数据库
user = "root", --账户
password="123456", --密码
max_packet_size = 1024 * 1024
}
if not ok then
ngx.say("连接mysql失败 : ", err, ": ", errno, " ", sqlstate)
return
end
-- 设置查询出来的字符集 否则中文乱码
db:query("SET NAMES utf8")
-- 以上代码 是 连接 mysql 的
--查询数据 返回的是table
local res, err, errno, sqlstate = db:query("select * FROM tb_content")
if not res then
ngx.say("没有查询到: ", err, ": ", errno, ": ", sqlstate, ".")
return
end
-- 使用cjson 用于将 table 转json
local cjson = require "cjson"
--table转json 输出到 网页上
ngx.say("result: ", cjson.encode(res))
-- 只有数据传输完毕了,才能放到池子里,系统无法帮你自动做这个事情
local ok, err = db:set_keepalive(1000, 100) --设置连接池 超时时间,池的大小(线程的数量)
if not ok then
ngx.say("设置mysql线程池失败: ", err)
return
end
然后修改 nginx.conf
vi /usr/local/openresty/nginx/conf/nginx.conf
在nginx.conf中server部分添加如下配置
location /luamysql {
default_type application/json;
charset utf-8;
content_by_lua_file /root/lua/mysql_test.lua;
}
然后重启nginx
/usr/local/openresty/nginx/sbin/nginx -s reload
网络访问
curl http://192.168.66.71/luamysql
结果: dog: hello world
缓存是一个大型系统中非常重要的一个组成部分。在硬件层面,大部分的计算机硬件都会用缓存来提高速度,比如CPU会有多级缓存、RAID卡也有读写缓存。在软件层面,我们用的数据库就是一个缓存设计非常好的例子,在SQL语句的优化、索引设计、磁盘读写的各个地方,都有缓存
个生产环境的缓存系统,需要根据自己的业务场景和系统瓶颈,来找出最好的方案,这是一门平衡的艺术。
一般来说,缓存有两个原则。一是越靠近用户的请求越好,比如能用本地缓存的就不要发送HTTP请求,能用CDN缓存的就不要打到Web服务器,能用nginx缓存的就不要用数据库的缓存;二是尽量使用本进程和本机的缓存解决,因为跨了进程和机器甚至机房,缓存的网络开销就会非常大,在高并发的时候会非常明显。
而openResty 的缓存 是离用户很最近的地方了
修改 nginx.conf
vi /usr/local/openresty/nginx/conf/nginx.conf
在nginx.conf中http部分添加如下配置 开启 openResty 的缓存 并设置缓存大小
lua_shared_dict cache_ngx 128m; #lua 缓存
注意 这个 dis_cache
就是缓存的对象
操作openResty缓存的方法
--读取缓存方法 传入key 获取 值
function get_from_cache(key)
local cache_ngx = ngx.shared.cache_ngx --读取缓存
local value = cache_ngx:get(key) --通过key缓存中指定的值
return value
end
--设置缓存方法 参数1 是键 参数2是值 参数3是缓存失效时间 (秒)
function set_to_cache(key, value, exptime)
if not exptime then
exptime = 0
end
local cache_ngx = ngx.shared.cache_ngx
local succ, err, forcible = cache_ngx:set(key, value, exptime)
return succ --返回 是否设置成功 true 或者false
end
小技巧 等会下面我们要使用
判断是否查询到缓存
local cache=get_from_cache(key)
if not cache then
ngx.say("缓存中没有查询到")
return
end
获取 url的参数
local uri_args = ngx.req.get_uri_args(); -- 获取url路径所有参数 返回table
local id = uri_args["id"]; --获取 table 里的id值
if not id then
ngx.say("url参数中没有id这个参数-查询失败")
return
end
三层高效数据缓存
实现思路: 所有数据 都使用json格式存储
使用docker
准备 mysql 并且映射 3306 准备 Redis 并且 映射6379 都在ip 192.168.66.66 上
我的本地ip 是192.169.66.71
获取数据的思路
修改数据的思路 和上面一样 只是反过来了
以下只演示高效缓存 获取数据
在/root/lua/下创建 luacontent.lua文件
vi /root/lua/luacontent.lua
read_content.lua 文件内容
local uri_args = ngx.req.get_uri_args(); -- 获取url路径所有参数 返回table
local id = uri_args["id"]; --获取 url 参数里的id值
--获取本地OpenResty缓存
local cache_ngx = ngx.shared.cache_ngx;
local contentCache = cache_ngx:get('content_cache_'..id);
-- 判断缓存是否存在 如果不存在 则 查询redis
if contentCache == "" or contentCache == nil then
-- 创建redis 连接
local redis = require("resty.redis");
local red = redis:new()
red:set_timeout(2000) --设置redis连接超时时间
red:connect("192.168.66.66", 6379)
local rescontent=red:get("content_"..id);
-- 判断redis内指定数据是否存在 不存在 返回null
-- 使用 ngx.null 判断 建是否为空
if ngx.null == rescontent then
local mysql = require "resty.mysql"
local db, err = mysql:new()
if not db then
ngx.say("实例化mysql失败: ", err)
return
end
--数据库连接超时时间
db:set_timeout(2000)
-- 连接数据库
local ok, err, errno, sqlstate = db:connect{
host = "192.168.66.66", -- ip
port = 3306, --端口
database = "changgou_content", --数据库
user = "root", --账户
password="123456", --密码
max_packet_size = 1024 * 1024
}
if not ok then
ngx.say("连接mysql失败 : ", err, ": ", errno, " ", sqlstate)
return
end
-- 设置查询出来的字符集 否则中文乱码
db:query("SET NAMES utf8")
-- 以上代码 是 连接 mysql 的
--查询数据 返回的是table
local res, err, errno, sqlstate = db:query("select url,pic from tb_content where status ='1' and category_id="..id.." order by sort_order")
if not res then
ngx.say("没有查询到: ", err, ": ", errno, ": ", sqlstate, ".")
return
end
-- 使用cjson 用于将 table 转json
local cjson = require "cjson"
local responsejson = cjson.encode(res);
--存入redis缓存中
red:set("content_"..id,responsejson);
--设置本地 OpenResty缓存
cache_ngx:set('content_cache_'..id,responsejson, 10*60);
ngx.say(responsejson) --将数据返回到页面
--只有数据传输完毕了,才能放到池子里,系统无法帮你自动做这个事情
local ok, err = red:set_keepalive(1000, 100) --设置连接池 超时时间,池的大小(线程的数量)
if not ok then
ngx.say("设置redis线程池失败: ", err)
return
end
-- 只有数据传输完毕了,才能放到池子里,系统无法帮你自动做这个事情
local ok, err = db:set_keepalive(1000, 100) --设置连接池 超时时间,池的大小(线程的数量)
if not ok then
ngx.say("设置mysql线程池失败: ", err)
return
end
return
else
--设置本地 OpenResty缓存
cache_ngx:set('content_cache_'..id, rescontent, 10*60);
ngx.say(rescontent) --将数据返回到页面
--只有数据传输完毕了,才能放到池子里,系统无法帮你自动做这个事情
local ok, err = red:set_keepalive(1000, 100) --设置连接池 超时时间,池的大小(线程的数量)
if not ok then
ngx.say("设置redis线程池失败: ", err)
return
end
return
end
else
ngx.say(contentCache) --将数据返回到页面
end
修改nginx.conf
vi /usr/local/openresty/nginx/conf/nginx.conf
在nginx.conf中http部分添加如下配置
lua_shared_dict cache_ngx 128m; #lua 缓存
在nginx.conf中server部分添加如下配置
location /luacon {
default_type application/json;
charset utf-8;
content_by_lua_file /root/lua/luacontent.lua;
}
然后重启nginx
/usr/local/openresty/nginx/sbin/nginx -s reload
网络访问
curl http://192.168.66.71/luacon?id=1
结果: dog: hello world
在上面我们进行了3层缓存处理 但是面临一个新的问题就是 无论是服务器也好还是现实中的物品也好 都会有一个数量的 比如某某家的售卖的衣服这个月一共就200件 在多也就没有了 而不是无限制的 同样我们服务器的 访问量和流量 以及处理能力也是有限制的
最简单的来说 当你电脑内存满了 会触发2种情况 第一种死机 第二种就是宕机 最严重的还会影响到驱动导致驱动损坏
而我们常访问某某家的网站 原理就是在访问你服务器 而你服务器需要利用内存进行处理请求 比如你服务器是4g内存
一个用户请求访问占100mb 那么如果同时40个用户进行访问(注意:是同时) 这时候你服务器就爆了 CPU满负荷或内存不足),让正常的业务请求连接不进来。 这也是网上常说的洪水攻击
如何解决这样的问题呢 我们可以使用 限流技术 而限流就是保护措施之一。
生活中限流对比
水坝泄洪,通过闸口限制洪水流量(控制流量速度)。
*
办理银行业务:所有人先领号,各窗口叫号处理。每个窗口处理速度根据客户具体业务而定,所有人排队等待叫号即可。若快下班时,告知客户明日再来(拒绝流量)
*
火车站排队买票安检,通过排队 的方式依次放入。(缓存带处理任务)
nginx的限流
nginx提供两种限流的方式:
一是控制速率
*
二是控制并发连接数
这个需要你自己测试你服务器 在1秒内或者几秒内 最大能允许多少并发 然后进行限流 这样就不会出现 CPU满负荷或内存不足情况了
控制速率的方式之一就是采用漏桶算法。
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出
(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:
nginx的配置
修改/usr/local/openresty/nginx/conf/nginx.conf:
vi /usr/local/openresty/nginx/conf/nginx.conf
在http节点内添加
#限流设置
limit_req_zone $binary_remote_addr zone=one:10m rate=2r/s;
调用限流
在server节点的 对应 location 中 添加
limit_req zone=one burst=5 nodelay;
第一个参数:zone=one 设置使用哪个配置区域来做限制,与上面limit_req_zone 里的 zone=one对应。
*
第二个参数:burst=5,重点说明一下这个配置,burst爆发的意思,这个配置的意思是设置一个大小为5的缓冲区当有大量请求(爆发)过来时,超过了访问频次限制的请求可以先放到这个缓冲区内 不过,单独使用 burst 参数并不实用。假设 burst=50 ,rate为10r/s,排队中的50个请求 虽然每秒处理10个,但第50个请求却需要等待 5s,这么长的处理时间自然难以接受。
因此,burst 往往结合 nodelay 一起使用。
*
第三个参数:nodelay,如果设置,超过访问频次而且缓冲区也满了的时候就会直接返回503,如果没有设置,则所有请求会等待排队
我们在上面三层缓存案例 的 location /luacon 引入限流 注意不要忘了在http添加限流的配置
然后重启nginx
/usr/local/openresty/nginx/sbin/nginx -s reload
然后我们使用浏览器来测试
http://192.168.66.71/luacon?id=1
在1秒钟之内可以刷新1~6次,正常处理
但是超过之后,1秒连续刷新6次以上,抛出异常。
如果你的手速不够快 那么你 将 rate 和 burst 都调整小点 比如 rate=1r/s burst=2 这样是在1秒内刷新3次就ok了
什么是并发量 , 就是在同一时间的请求的数量 学过多线程应该明白
我们可以 利用连接数限制 某一个用户的ip连接的数量来控制流量。 (防止恶意攻击)
注意:并非所有连接都被计算在内 只有当服务器正在处理请求并且已经读取了整个请求头时,才会计算有效连接。
nginx的配置
修改/usr/local/openresty/nginx/conf/nginx.conf:
vi /usr/local/openresty/nginx/conf/nginx.conf
在http节点内添加
#根据IP地址来限制,存储内存大小10M
limit_conn_zone $binary_remote_addr zone=addr:1m;
注意 和限流不同 limit_conn_zone
在server节点的 对应 location 中 添加
limit_conn addr 2;
测试
我们先使用Springboot写一个接口
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String Hello() throws InterruptedException {
Thread.sleep(1000); //阻塞线程 1秒 (也可以当成执行此请求的时间)
return "Hello! World";
}
}
运行Springboot
使用游览器访问 http://localhost:8080/hello
Hello! World
使用 ipconfig 查询windows 的ip
然后我们配置 控制并发限制
nginx的配置
修改/usr/local/openresty/nginx/conf/nginx.conf:
vi /usr/local/openresty/nginx/conf/nginx.conf
别忘了在 http里配置 加上 limit_conn_zone …
在server节点的 对应 location / 路径拦截中 添加转发请求 到 http://localhost:8080/hello
location / {
limit_conn addr 2;
proxy_pass http://192.168.1.1:8080/hello; #windows ip
}
然后重启nginx
/usr/local/openresty/nginx/sbin/nginx -s reload
然后使用 http://192.168.66.71:80/hello
Hello! World
可以看到能访问成功的 那么我们就开始测试 是否控制并发
我们需要利用apache-jmeter工具来进行并发测试 这个可以到网上自己下载
可以看到我发了3个请求 2个成功 1个失败 代表成功拦截(有效防止 恶意攻击)
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/weixin_45203607/article/details/120248031
内容来源于网络,如有侵权,请联系作者删除!