Lua高级教学

x33g5p2x  于2021-09-24 转载在 其他  
字(19.0k)|赞(0)|评价(0)|浏览(880)

Lua 协同程序

什么是协同?

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

Lua I/O 库用于读取和处理文件。分为简单模式(和C一样)、完全模式。

  • 简单模式(simple model)拥有一个当前输入文件和一个当前输出文件,并且提供针对这些文件相关的操作。
  • 完全模式(complete model) 使用外部的文件句柄来实现。它以一种面对对象的形式,将所有的文件操作定义为文件句柄的方法

简单模式在做一些简单的文件操作时较为合适。但是在进行一些高级的文件操作的时候,简单模式就显得力不从心。例如同时读取多个文件这样的操作,使用完全模式则较为合适。

在 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 值可以是:

  • “set”: 从文件头开始
  • “cur”: 从当前位置开始[默认]
  • “end”: 从文件尾开始
  • offset:默认为0
-- 以只读方式打开文件
file = io.open("test.txt", "r")

file:seek("set",5)  --从第5个字节 开始

print(file:read("*a"))  --读取全部内容

-- 关闭打开的文件
file:close()

lua 错误处理

程序运行中错误处理是必要的,在我们进行文件操作,数据转移及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,···)

返回值

  1. 函数执行状态 (boolean)
  • 没有错误返回 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 但是他的错误处理是交给函数的

返回值

  1. 函数执行状态 (boolean)
  • 没有错误返回 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 模块于包

模块类似于一个封装库,从 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+nginx+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里

安装OpenResty

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

nginx+lua 开发环境配置:

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

访问redis

在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-你好

访问mysql

在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

openResty缓存

缓存是一个大型系统中非常重要的一个组成部分。在硬件层面,大部分的计算机硬件都会用缓存来提高速度,比如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

openResty缓存+redis+mysql

三层高效数据缓存

实现思路: 所有数据 都使用json格式存储

使用docker

准备 mysql 并且映射 3306 准备 Redis 并且 映射6379 都在ip 192.168.66.66 上

我的本地ip 是192.169.66.71

获取数据的思路

  1. 客户端请求数据的时候 先查询openResty缓存里是否有对应的数据 ,如果有 就直接返回给用户
  2. 客户端请求数据的时候 先查询openResty缓存里是否有对应的数据 如果没有 那么查询redis里是否有对应的数据如果有, 将数据保存到openResty缓存里然后 将数据返回给用户
  3. 客户端请求数据的时候 先查询openResty缓存里是否有对应的数据 如果没有那么查询redis里是否有对应的数据, 如果还没有那么最后在从数据库里查询出来,然后依次保存到redis和openResty缓存里 , 最后将数据返回给用户

修改数据的思路 和上面一样 只是反过来了

  1. 先把数据库里的内容改完 然后将修改后的数据 依次传入redis和mysql里 将缓存中原来的数据覆盖

以下只演示高效缓存 获取数据

在/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

nginx限流

在上面我们进行了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;
  • 第一个参数:$binary_remote_addr 表示通过remote_addr这个标识来做限制,“binary_”的目的是缩写内存占用量,是限制同一客户端ip地址。
  • 第二个参数:zone=one:10m表示生成一个大小为10M,名字为one的内存区域,用来存储访问的频次信息。
  • 第三个参数:rate=1r/s表示允许相同标识的客户端的访问频次,这里限制的是每秒2次,还可以有比如30r/m的 (根据你服务器性能来调整)

调用限流

在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

  • 表示限制根据用户的IP地址来显示,设置存储地址为的内存大小10M

在server节点的 对应 location 中 添加

limit_conn addr 2;
  • 表示 同一个地址同时只允许连接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个失败 代表成功拦截(有效防止 恶意攻击)

相关文章