一、商城门户数据缓存
一般商城首页门户需要展示商品分类和广告数据,可以把这些内容数据存入redis缓存中。
对于变更频率低,查询频率高的数据,如何提高访问速率?
- 数据做成静态网页
- 做缓存(redis)
对于商城首页高并发实现思路:
- 当用户访问首页时,使用Lua查询Nginx缓存,有缓存则返回缓存中商城首页分类的数据
- 当Nginx中没有分类数据缓存时,统过Lua脚本查询Redis,如果Redis中有数据,把查询到的数据存入Nginx缓存中,并返回查询的分类数据结果
- 当Redis中也没有分类缓存数据时,则通过Lua脚本查询数据库,如果数据库中有数据,就将查询到的数据存入Redis缓存中,并返回结果
二、Lua了解
2.1、lua是什么
Lua是一种轻量小巧的脚本语言,它是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo三人所组成的研究小组于1993年开发的。 其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。所以Lua不适合作为开发独立应用程序的语言。Lua 有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。
2.2、特性
- 支持面向过程编程(procedure oriented Programming)和函数式编程(functional programming)
- 自动内存管理,只提供一种类型的表(table),用它可以实现数组、哈希表、集合、对象
- 语言内置模式匹配,闭包(closure),函数也可以看作一个值,提供多线程支持(协同进程,并非操作系统所支持的线程)
- 通过闭包和table可以很方便的支持面对对象编程所需要的一些关键机制,比如数据抽象、虚函数、继承和重载
2.3、应用场景
- 游戏开发
- 独立脚本应用
- web应用脚本
- 扩展和数据库插件(如Mysql Proxy、Mysql WorkBench)
- 安全系统,如入侵检测系统
- redis中嵌套调用实现类似事务的功能
- web容器中处理一些过滤、缓存等等逻辑,例如nginx
2.4、lua安装
本篇关于Linux下安装lua
安装步骤:
# 下载lua压缩包
curl -R -O http://www.lua.org/ftp/lua-5.4.3.tar.gz
# 解压lua文件
tar -zxf lua-5.4.3.tar.gz
# 进入lua软件目录中
cd lua-5.4.3/
# 安装lua
make linux test
make install
注意: 安装时可能会出现以下错误
此时需要安装lua相关的依赖库支持:
yum -y install gcc gcc-c++ kernel-devel
安装gcc依赖完后再执行lua安装命令
检测lua是否安装成功
Tips:
ctrl+c
退出lua编辑。在Linux或windows中大部分脚本运行的程序都可以用ctrl+c
退出
2.5、编写入门程序
创建并编辑hello.lua文件
# 用vim用vim创建hello.lua文件并进入编辑模式
vim hello.lua
# 输入 i 进入编辑模式
print("Hello Lua!");
# 编辑完后esc 退出编辑模式 :wq保存文件
执行命令
lua hello.lua
输出效果:
2.6、lua的基本语法
lua有交互式编程和脚本式编程。
交互式编程就是直接输入语法,就能执行。
脚本式编程需要编写脚本,然后再执行命令 执行脚本才可以。
一般采用脚本式编程。(例如:编写一个hello.lua的文件,输入文件内容,并执行lua hell.lua即可)
-
交互式编程
Lua 提供了交互式编程模式。我们可以在命令行中输入程序并立即查看效果。
Lua 交互式编程模式可以通过命令lua -i
或lua
来启用:
-
脚本式编程
我们可以将 Lua 程序代码保持到一个以 lua 结尾的文件,并执行,该模式称为脚本式编程,例如上面入门程序中将lua语法写到hello.lua文件中。
2.6.1、注释
单行注释:两个减号表示单行注释
--
多行注释:
--[[
多行注释
多行注释
--]]
Tips:
Shift+Delete
删除Ctrl+Enter
换行输入
2.6.2、定义变量
全局变量,默认的情况下,定义一个变量都是全局变量,
如果要用局部变量 需要声明为 local
例如:
-- 全局变量赋值
a=1
-- 局部变量赋值
local b=2
输出结果:
如果变量没有初始化,则它的值为nil;这和java中的null不同。
因为变量b为局部变量,所以输出的为nil
2.6.3、Lua中数据类型
Lua 是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。
Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和table。
数据类型 | 描述 |
---|---|
nil | 这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。 |
boolean | 包含两个值:false和true。 |
number | 表示双精度类型的实浮点数 |
string | 字符串由一对双引号或单引号来表示 |
function | 由 C 或 Lua 编写的函数 |
userdata | 表示任意存储在变量中的C数据结构 |
thread | 表示执行的独立线路,用于执行协同程序 |
table | Lua 中的表(table)其实是一个"关联数组"(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过"构造表达式"来完成,最简单构造表达式是{},用来创建一个空表。 |
实例:
2.6.4 、流程控制
- if语句
Lua if语句 由一个布尔表达式作为判断条件判断,其后紧跟其他语句组成
语法:
if()
then
--[[
在布尔表达式为 true 时执行的语句
--]]
end
实例:
- if … else语句
Lua if 语句可以与 else 语句搭配使用, 在 if 条件表达式为 false 时执行 else 语句代码块。
语法:
if (布尔表达式)
then
--[[
布尔表达式为 true 时执行该语句块
--]]
else
--[[
布尔表达式为 false 时执行该语句块
--]]
end
实例:
2.6.5、循环
- while循环
Lua 编程语言中 while 循环语句在判断条件为 true 时会重复执行循环体语句。
语法:
while (循环条件)
do
执行体
end
实例:
- for循环
Lua 编程语言中 for 循环语句可以重复执行指定语句,重复次数可在 for 语句中控制。
语法: 1->10 1:exp1 10:exp2 2:exp3:递增的数量
for var=exp1,exp2,exp3
do
执行体
end
var 从 exp1 变化到 exp2,每次变化以 exp3 为步长递增 var,并执行一次 “执行体”。exp3 是可选的,如果不指定,默认为1。
实例:
for i=1,20,3
do
print(i)
end
for i=1,20,3
1表示从1开始,20表示到循环到20结束,3表示每次循环递增3
- repeat…until语句
Lua 编程语言中 repeat…until 循环语句不同于 for 和 while循环,for 和 while 循环的条件语句在当前循环执行开始时判断,而 repeat…until 循环的条件语句在当前循环结束后判断。
语法:
repeat
执行体
until (循环条件)
案例:
2.6.6、函数
lua中也可以定义函数,类似于java中的方法。
语法:
function var ()
if ( 条件 )
then
条件真内容
else
条加假内容
end
例如:
-- 定义一个比较大小的函数
function max(a,b)
if (a>b)
then
result=a;
else
result=b;
end
return result;
end
-- 调用函数示例
print("两数比较最大值:",max(10,15))
print("两数比较最大值:",max(20,10))
2.6.7、表
table 是 Lua 的一种数据结构用来帮助我们创建不同的数据类型,如:数组、字典等。
Lua也是通过table来解决模块(module)、包(package)和对象(Object)的。
案例:
-- 初始化表
tb1 = {}
print("tb1的类型为:",type(tb1))
-- 给tb1指定值
tb1[0] = "hello"
tb1["lua"] = "Hello Lua!"
print("tb1表索引0的值为:",tb1[0])
print("tb1表索引lua的值为:",tb1["lua"])
-- 移除tb1表的引用
tb1 = nil
print("tb1表索引0的值为:",tb1[0])
2.6.8、模块
- 模块的定义
模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放
在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。
创建一个文件叫module.lua,在module.lua中创建一个独立的模块,代码如下:
-- 文件名为 module.lua
-- 定义一个名为 module的模块
module = {}
-- 定义一个常量
module.constant = "这是定义的一个常量"
-- 定义一个函数
function module.a()
print("这是一个公共函数")
end
local function b()
print("这是一个私有函数")
end
function module.c()
b()
end
return module
- require 函数
require 用于 引入其他的模块,类似于java中的类要引用别的类的效果。
用法:
# 两种方法均可引用
require("module")
require "module"
# 引用外部文件(./module :文件路径)
require("./module")
案例:
三、OpenResty介绍
OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。
OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。
360,UPYUN,阿里云,新浪,腾讯网,去哪儿网,酷狗音乐等都是 OpenResty 的深度用户。
OpenResty 简单理解成 就相当于封装了nginx,并且集成了LUA脚本,开发人员只需要简单的使用其提供了模块就可以实现相关的逻辑,而不再像之前,还需要在nginx中自己编写lua的脚本,再进行调用了。
3.1、安装OpenResty
本教程是基于 centos7安装,对于 CentOS 8 或以上版本,应将下面的
yum
都替换成dnf
- 添加OpenResty仓库
wget https://openresty.org/package/centos/openresty.repo
sudo mv openresty.repo /etc/yum.repos.d/
# 更新yum索引目录
sudo yum check-update
- 执行安装
sudo yum install -y openresty
安装成功后默认默认目录为:/usr/local/openresty/
3.2、测试访问
安装OpenResty时默认已经安装好了nginx,在目录:/usr/local/openresty/nginx
下。
修改/usr/local/openresty/nginx/conf/nginx.conf,将配置文件使用的根设置为root,目的就是以后要使用lua脚本的时候 ,可以直接加载在root下的lua脚本。
cd /usr/local/openresty/nginx/conf/
vim nginx.conf
# 修改代码
user root root;
启动nginx服务:
cd /usr/local/openresty/nginx/sbin
# 启动
./nginx
# 重启
./nginx -s reload
# 关闭
./nginx -s stop
查询nginx状态: ps -ef | grep nginx
,带master表示启动成功
四、场景模拟-商城首页缓存处理
4.1、需求分析
在页面显示商品的分类信息
4.2、docker安装redis
# 使用docker安装redis
docker pull redis
docker run --name redis -p 6379:6379 -d redis
4.3、Lua+Nginx配置
使用lua来完成一个操作nginx缓存访问redis、mysql数据库的案例
4.3.1 Lua案例1需求:根据分类ID查询分类数据放入redis中
实现思路:
- 定义请求:用于查询数据库中的数据更新到redis中。
- 连接mysql ,按照分类ID读取商品分类,转换为json字符串。
- 连接redis,将广告列表json字符串存入redis 。
定义请求:
请求: /update_content
参数: id(分类id)
返回值: json
请求地址:http://192.168.181.130/update_content?id=1
创建/root/lua目录,在该目录下创建update_content.lua,连接mysql 查询数据 并存储到redis中。
ngx.header.content_type = "application/json;charset=utf8"
local cjson = require("cjson")
local mysql = require("resty.mysql")
local uri_args = ngx.req.get_uri_args()
local id = uri_args["id"]
local db = mysql:new()
db:set_timeout(1000)
local props = {
host = "192.168.181.130",
port = 3306,
database = "legou",
user = "root",
password = "root"
}
local res = db:connect(props)
local select_sql = "SELECT id_,is_parent_,order_,parent_id_,title_,expand_ FROM category_ WHERE id_= " .. id
res = db:query(select_sql)
db:close()
local redis = require("resty.redis")
local red = redis:new()
red:set_timeout(2000)
local ip = "192.168.181.130"
local port = 6379
red:connect(ip, port)
red:set("content_" .. id, cjson.encode(res))
red:close()
ngx.say("{flag:true}")
tips:
..
表示字符串拼接
修改nginx配置文件:
vim /usr/local/openresty/nginx/conf/nginx.conf
server {
listen 80;
server_name localhost;
# 在server中添加
location /update_content {
content_by_lua_file /root/lua/update_content.lua;
}
}
重启nginx服务 ./ngixn -s reload
请求测试:http://192.168.181.130/update_content?id=1
4.3.2、从redis中获取数据
实现思路:定义请求,用户根据分类的ID 获取分类信息。通过lua脚本直接从redis中获取数据即可。
定义请求:
请求: /read_content
参数: id(分类id)
返回值: json
在/root/lua目录下创建read_content.lua:
--设置响应头类型
ngx.header.content_type = "application/json;charset=utf8"
--获取请求中的参数ID
local uri_args = ngx.req.get_uri_args()
local id = uri_args["id"]
--引入redis库
local redis = require("resty.redis")
--创建redis对象
local red = redis:new()
--设置超时时间
red:set_timeout(2000)
--连接
local ok, err = red:connect("192.168.181.130", 6379)
--获取key的值
local resContent = red:get("content_" .. id)
--输出到返回响应中
ngx.say(resContent)
--关闭连接
red:close()
修改nginx配置文件:
vim /usr/local/openresty/nginx/conf/nginx.conf
server {
listen 80;
server_name localhost;
# 在server中添加
location /read_content {
content_by_lua_file /root/lua/read_content.lua;
}
}
访问测试: http://192.168.181.130/read_content?id=1
4.3.3、加入OpenResty本地缓存
如果请求都到redis,redis压力也很大,所以我们一般采用多级缓存的方式
来减少下游系统的服务压力。
先查询openresty本地缓存,如果没有缓存数据=》再查询redis中的数据,如果没有缓存数据=》再查询mysql中的数据,返回查询结果。
修改优化read_content.lua文件,代码如下:
ngx.header.content_type = "application/json;charset=utf8"
local uri_args = ngx.req.get_uri_args();
local id = uri_args["id"];
local cache_ngx = ngx.shared.dis_cache;
local contentCache = cache_ngx:get('content_cache_' .. id);
ngx.say("获取nginx缓存:", contentCache)
if contentCache == "" or contentCache == nil then
local redis = require("resty.redis");
local red = redis:new()
red:set_timeout(2000)
red:connect("192.168.181.130", 6379)
local resContent = red:get("content_" .. id);
if ngx.null == resContent then
local cjson = require("cjson");
local mysql = require("resty.mysql");
local db = mysql:new();
db:set_timeout(2000)
local props = {
host = "192.168.181.130",
port = 3306,
database = "legou",
user = "root",
password = "root"
}
local res = db:connect(props);
local select_sql = "SELECT id_,is_parent_,order_,parent_id_,title_,expand_ FROM category_ WHERE id_= " .. id
res = db:query(select_sql);
local responseJson = cjson.encode(res);
red:set("content_" .. id, responseJson);
ngx.say("数据库数据:",responseJson);
db:close()
else
cache_ngx:set('content_cache_' .. id, resContent, 10 * 60);
ngx.say("redis缓存不为空:",resContent)
end
red:close()
else
ngx.say("nginx缓存不为空:",contentCache)
end
定义lua缓存命名空间及大小,修改nginx.conf,添加如下代码即可:
#nginx本地缓存
lua_shared_dict dis_cache 128m;
测试地址:http://192.168.181.130/read_content?id=1
首次访问:nginx本地缓存和redis缓存都没有,查询数据库并把查询结果存入redis中
第二次访问:redis缓存不为空,查询redis缓存同时把缓存存入nginx本地缓存
第三次及以后访问:获取nginx本地缓存