一、Nginx实现服务器端集群搭建
1.1Nginx与Tomcat部署
我们都知道了 Nginx 在 高并发场景和处理静态资源是非常高性能的,但是在实际项目中除了静
态资源还有就是后台业务代码模块,一般后台业务都会被部署在
Tomcat , weblogic 或者是 websphere 等 web 服务器上。那么如何使用
Nginx 接收用户的请求并把请求转发到后台 web 服务器?
步骤分析 :
1. 准备 Tomcat 环境,并在 Tomcat 上部署一个 web 项目
2. 准备 Nginx 环境,使用 Nginx 接收请求,并把请求分发到 Tomat 上
环境准备 (Tomcat)
浏览器访问 :
http://192.168.200.146:8080/demo/index.html
获取动态资源的链接地址 :
http://192.168.200.146:8080/demo/getAddress
本次将采用 Tomcat 作为后台 web 服务器
(1)在 Centos 上准备一个 Tomcat
1.Tomcat 官网地址 :https://tomcat.apache.org/
2. 下载 tomcat, 本次课程使用的是 apache-tomcat-8.5.59.tar.gz
3. 将 tomcat 进行解压缩
mkdir web_tomcat
tar -zxf apache-tomcat-8.5.59.tar.gz -C /web_tomcat
(2)准备一个 web 项目,将其打包为 war
1. 将资料中的 demo.war 上传到 tomcat8 目录下的 webapps 包下
2. 将 tomcat 进行启动,进入 tomcat8 的 bin 目录下
./startup.sh
(3)启动 tomcat 进行访问测试。
静态资源 : http://192.168.200.146:8080/demo/index.html
动态资源 : http://192.168.200.146:8080/demo/getAddress
环境准备 (Nginx)
(1)使用 Nginx 的反向代理,将请求转给 Tomcat 进行处理。
upstream webservice {
server 192.168.200.146:8080;
}
server{
listen 80;
server_name localhost;
location /demo {
proxy_pass http://webservice;
}
}
学习到这,可能大家会有一个困惑,明明直接通过 tomcat 就能访问,为
什么还需要多加一个 nginx ,这样不是反而是系统的复杂度变高了么 ? 那
接下来我们从两个方便给大家分析下这个问题,
第一个使用 Nginx 实现动静分离
第二个使用 Nginx 搭建 Tomcat 的集群
1.2Nginx实现动静分离
什么是动静分离 ?
动 : 后台应用程序的业务处理
静 : 网站的静态资源 (html,javaScript,css,images 等文件 )
分离 : 将两者进行分开部署访问,提供用户进行访问。举例说明就是以后
所有和静态资源相关的内容都交给 Nginx 来部署访问,非静态内容则交
个类似于 Tomcat 的服务器来部署访问。
为什么要动静分离 ?
前面我们介绍过 Nginx 在处理静态资源的时候,效率是非常高的,而且
Nginx 的并发访问量也是名列前茅,而 Tomcat 则相对比较弱一些,所以
把静态资源交个 Nginx 后,可以减轻 Tomcat 服务器的访问压力并提高静
态资源的访问速度。
动静分离以后,降低了动态资源和静态资源的耦合度。如动态资源宕机
了也不影响静态资源的展示。
如何实现动静分离 ?
实现动静分离的方式很多,比如静态资源可以部署到 CDN 、 Nginx 等服
务器上,动态资源可以部署到 Tomcat,weblogic 或者 websphere 上。本
次课程只要使用 Nginx+Tomcat 来实现动静分离。
需求分析
动静分离实现步骤
1. 将 demo.war 项目中的静态资源都删除掉,重新打包生成一个 war 包,
在资料中有提供。
2. 将 war 包部署到 tomcat 中,把之前部署的内容删除掉
进入到 tomcat 的 webapps 目录下,将之前的内容删除掉
将新的 war 包复制到 webapps 下
将 tomcat 启动
3. 在 Nginx 所在服务器创建如下目录,并将对应的静态资源放入指定的位
置
其中 index.html 页面的内容如下 :
4. 配置 Nginx 的静态资源与动态资源的访问
lang = "en" >
charset = "UTF-8" >
Title
src = "images/logo.png" />
Nginx 如何将请求转发到后端服务器
id = "msg" >
src = "images/mv.png" />
4. 配置 Nginx 的静态资源与动态资源的访问
upstream webservice{
server 192.168.200.146:8080;
}
server {
listen 80;
server_name localhost;
# 动态资源
location /demo {
proxy_pass http://webservice;
}
# 静态资源
location ~/.*\.(png|jpg|gif|js){
root html/web;
gzip on;
}
location / {
root html/web;
index index.html index.htm;
}
}
5. 启动测试,访问 http://192.168.200.133/index.html
假如某个时间点,由于某个原因导致 Tomcat 后的服务器宕机了,我们再
次访问 Nginx, 会得到如下效果,用户还是能看到页面,只是缺失了访问
次数的统计,这就是前后端耦合度降低的效果,并且整个请求只和后的
服务器交互了一次, js 和 images 都直接从 Nginx 返回,提供了效率,降
低了后的服务器的压力。
Nginx 实现 Tomcat 集群搭建
在使用 Nginx 和 Tomcat 部署项目的时候,我们使用的是一台 Nginx 服务
器和一台 Tomcat 服务器,效果图如下 :
那么问题来了,如果 Tomcat 的真的宕机了,整个系统就会不完整,所以
如何解决上述问题,一台服务器容易宕机,那就多搭建几台 Tomcat 服务
器,这样的话就提升了后的服务器的可用性。这也就是我们常说的集
群,搭建 Tomcat 的集群需要用到了 Nginx 的反向代理和赋值均衡的知
识,具体如何来实现 ? 我们先来分析下原理
环境准备:
(1) 准备 3 台 tomcat, 使用端口进行区分 [ 实际环境应该是三台服务器 ] ,修
改 server.ml ,将端口修改分别修改为 8080,8180,8280
(2) 启动 tomcat 并访问测试,
(3) 在 Nginx 对应的配置文件中添加如下内容 :
upstream webservice{
server 192.168.200.146:8080;
server 192.168.200.146:8180;
server 192.168.200.146:8280;
}
好了,完成了上述环境的部署,我们已经解决了 Tomcat 的高可用性,一
台服务器宕机,还有其他两条对外提供服务,同时也可以实现后台服务
器的不间断更新。但是新问题出现了,上述环境中,如果是 Nginx 宕机
了呢,那么整套系统都将服务对外提供服务了,这个如何解决?
1.3Nginx高可用解决方案
针对于上面提到的问题,我们来分析下要想解决上述问题,需要面临哪
些问题 ?
需要两台以上的 Nginx 服务器对外提供服务,这样的话就可以解决其中一
台宕机了,另外一台还能对外提供服务,但是如果是两台 Nginx 服务器的
话,会有两个 IP 地址,用户该访问哪台服务器,用户怎么知道哪台是好
的,哪台是宕机了的 ?
Keepalived
使用 Keepalived 来解决, Keepalived 软件由 C 编写的,最初是专为 LVS
负载均衡软件设计的, Keepalived 软件主要是通过 VRRP 协议实现高可
用功能。
VRRP 介绍
VRRP ( Virtual Route Redundancy Protocol )协议,翻译过来为虚拟路
由冗余协议。 VRRP 协议将两台或多台路由器设备虚拟成一个设备,对外
提供虚拟路由器 IP, 而在路由器组内部,如果实际拥有这个对外 IP 的路由
器如果工作正常的话就是 MASTER,MASTER 实现针对虚拟路由器 IP 的各
种网络功能。其他设备不拥有该虚拟 IP ,状态为 BACKUP, 处了接收
MASTER 的 VRRP 状态通告信息以外,不执行对外的网络功能。当主机失
效时, BACKUP 将接管原先 MASTER 的网络功能。
从上面的介绍信息获取到的内容就是 VRRP 是一种协议,那这个协议是用
来干什么的?
1. 选择协议
VRRP 可以把一个虚拟路由器的责任动态分配到局域网上的 VRRP 路由器
中的一台。其中的虚拟路由即 Virtual 路由是由 VRRP 路由群组创建的一个
不真实存在的路由,这个虚拟路由也是有对应的 IP 地址。而且 VRRP 路由 1
和 VRRP 路由 2 之间会有竞争选择,通过选择会产生一个 Master 路由和一个
Backup 路由。
2. 路由容错协议
Master 路由和 Backup 路由之间会有一个心跳检测, Master 会定时告知
Backup 自己的状态,如果在指定的时间内, Backup 没有接收到这个通知
内容, Backup 就会替代 Master 成为新的 Master 。 Master 路由有一个特
权就是虚拟路由和后端服务器都是通过 Master 进行数据传递交互的,而备
份节点则会直接丢弃这些请求和数据,不做处理,只是去监听 Master 的状
态
环境搭建
环境准备
keepalived 的安装
步骤 1: 从官方网站下载 keepalived, 官网地址
https://keepalived.org/
步骤 2: 将下载的资源上传到服务器
keepalived-2.0.20.tar.gz
步骤 3: 创建 keepalived 目录,方便管理资源
mkdir keepalived
步骤 4: 将压缩文件进行解压缩,解压缩到指定的目录
tar -zxf keepalived-2.0.20.tar.gz -C keepalived/
步骤 5: 对 keepalived 进行配置,编译和安装
cd keepalived/keepalived-2.0.20
./configure --sysconf=/etc --prefix=/usr/local
make && make install
安装完成后,有两个文件需要我们认识下,一个是
/etc/keepalived/keepalived.conf (keepalived 的系统配置文件,我
们主要操作的就是该文件 ) ,一个是 /usr/local/sbin 目录下的
keepalived , 是系统配置脚本,用来启动和关闭 keepalived
Keepalived 配置文件介绍
打开 keepalived.conf 配置文件
这里面会分三部,第一部分是 global 全局配置、第二部分是 vrrp 相关配
置、第三部分是 LVS 相关配置。 本次课程主要是使用 keepalived 实现高
可用部署,没有用到 LVS ,所以我们重点关注的是前两部分
global 全局部分:
global_defs {
# 通知邮件,当 keepalived 发送切换时需要发 email 给具体的邮箱
地址
notification_email {
}
# 设置发件人的邮箱信息
# 指定 smpt 服务地址
smtp_server 192.168.200.1
# 指定 smpt 服务连接超时时间
smtp_connect_timeout 30
# 运行 keepalived 服务器的一个标识,可以用作发送邮件的主题信
息
router_id LVS_DEVEL
# 默认是不跳过检查。检查收到的 VRRP 通告中的所有地址可能会比较
耗时,设置此命令的意思是,如果通告与接收的上一个通告来自相同的
master 路由器,则不执行检查 ( 跳过检查 )
vrrp_skip_check_adv_addr
# 严格遵守 VRRP 协议。
vrrp_strict
# 在一个接口发送的两个免费 ARP 之间的延迟。可以精确到毫秒级。
默认是 0
vrrp_garp_interval 0
# 在一个网卡上每组 na 消息之间的延迟时间,默认为 0
vrrp_gna_interval 0
}
VRRP 部分,该部分可以包含以下四个子模块
1. vrrp_script
2. vrrp_sync_group
3. garp_group
4. vrrp_instance
我们会用到第一个和第四个,
# 设置 keepalived 实例的相关信息, VI_1 为 VRRP 实例名称
vrrp_instance VI_1 {
state MASTER # 有两个值可选 MASTER 主 BACKUP 备
interface ens33 #vrrp 实例绑定的接口,用于发送 VRRP
包 [ 当前服务器使用的网卡名称 ]
virtual_router_id 51# 指定 VRRP 实例 ID ,范围是 0-255
priority 100 # 指定优先级,优先级高的将成为
MASTER
advert_int 1 # 指定发送 VRRP 通告的间隔,单位是秒
authentication { #vrrp 之间通信的认证信息
auth_type PASS # 指定认证方式。 PASS 简单密码认证 ( 推
荐 )
auth_pass 1111 # 指定认证使用的密码,最多 8 位
}
virtual_ipaddress { # 虚拟 IP 地址设置虚拟 IP 地址,供用户
访问使用,可设置多个,一行一个
192.168.200.222
}
}
配置内容如下 :
服务器 1
global_defs {
notification_email {
}
smtp_server 192.168.200.1
smtp_connect_timeout 30
router_id keepalived1
vrrp_skip_check_adv_addr
vrrp_strict
vrrp_garp_interval 0
vrrp_gna_interval 0
}
vrrp_instance VI_1 {
state MASTER
interface ens33
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.200.222
}
}
服务器 2
! Configuration File for keepalived
global_defs {
notification_email {
}
smtp_server 192.168.200.1
smtp_connect_timeout 30
router_id keepalived2
vrrp_skip_check_adv_addr
vrrp_strict
vrrp_garp_interval 0
vrrp_gna_interval 0
}
vrrp_instance VI_1 {
state BACKUP
interface ens33
virtual_router_id 51
priority 90
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.200.222
}
}
访问测试
1. 启动 keepalived 之前,咱们先使用命令 ip a , 查看 192.168.200.133
和 192.168.200.122 这两台服务器的 IP 情况。
2. 分别启动两台服务器的 keepalived
cd /usr/local/sbin
./keepalived
3. 当把 192.168.200.133 服务器上的 keepalived 关闭后,再次查看 ip
通过上述的测试,我们会发现,虚拟 IP(VIP) 会在 MASTER 节点上,当
MASTER 节点上的 keepalived 出问题以后,因为 BACKUP 无法收到
MASTER 发出的 VRRP 状态通过信息,就会直接升为 MASTER 。 VIP 也
会 " 漂移 " 到新的 MASTER 。
上面测试和 Nginx 有什么关系 ?
我们把 192.168.200.133 服务器的 keepalived 再次启动下,由于它的优先
级高于服务器 192.168.200.122 的,所有它会再次成为 MASTER , VIP 也
会 " 漂移 " 过去,然后我们再次通过浏览器访问 :
如果把 192.168.200.133 服务器的 keepalived 关闭掉,再次访问相同的地
址
效果实现了以后, 我们会发现要想让 vip 进行切换,就必须要把服务器上
的 keepalived 进行关闭,而什么时候关闭 keepalived 呢 ? 应该是在
keepalived 所在服务器的 nginx 出现问题后,把 keepalived 关闭掉,就可
以让 VIP 执行另外一台服务器,但是现在这所有的操作都是通过手动来完
成的,我们如何能让系统自动判断当前服务器的 nginx 是否正确启动,如
果没有,要能让 VIP 自动进行 " 漂移 " ,这个问题该如何解决 ?
keepalived 之 vrrp_script
keepalived 只能做到对网络故障和 keepalived 本身的监控,即当出现网
络故障或者 keepalived 本身出现问题时,进行切换。但是这些还不够,
我们还需要监控 keepalived 所在服务器上的其他业务,比如 Nginx, 如果
Nginx 出现异常了,仅仅 keepalived 保持正常,是无法完成系统的正常
工作的,因此需要根据业务进程的运行状态决定是否需要进行主备切
换,这个时候,我们可以通过编写脚本对业务进程进行检测监控。
实现步骤 :
1. 在 keepalived 配置文件中添加对应的配置像
vrrp_script 脚本名称
{
script " 脚本位置 "
interval 3 # 执行时间间隔
weight -20 # 动态调整 vrrp_instance 的优先级
}
2. 编写脚本
ck_nginx.sh
#!/bin/bash
num=`ps -C nginx --no-header | wc -l`
if [ $num -eq 0 ];then
/usr/local/nginx/sbin/nginx
sleep 2
if [ `ps -C nginx --no-header | wc -l` -eq 0 ]; then
killall keepalived
fi
fi
Linux ps 命令用于显示当前进程 (process) 的状态。
-C(command) : 指定命令的所有进程
--no-header 排除标题
3. 为脚本文件设置权限
chmod 755 ck_nginx.sh
4. 将脚本添加到
vrrp_script ck_nginx {
script "/etc/keepalived/ck_nginx.sh" # 执行脚本的位置
interval 2 # 执行脚本的周期,秒为单位
weight -20 # 权重的计算方式
}
vrrp_instance VI_1 {
state MASTER
interface ens33
virtual_router_id 10
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.200.111
}
track_script {
ck_nginx
}
}
5. 如果效果没有出来,可以使用 tail - f /var/log/messages 查看日
志信息,找对应的错误信息。
6. 测试
问题思考 :
通常如果 master 服务死掉后 backup 会变成 master ,但是当 master 服务
又好了的时候 master 此时会抢占 VIP ,这样就会发生两次切换对业务繁
忙的网站来说是不好的。所以我们要在配置文件加入 nopreempt 非抢
占,但是这个参数只能用于 state 为 backup ,故我们在用 HA 的时候最好
master 和 backup 的 state 都设置成 backup 让其通过 priority 来竞争。
二、Nginx制作下载站点
首先我们先要清楚什么是下载站点 ?
我们先来看一个网站 http://nginx.org/download/ 这个我们刚开始学
习 Nginx 的时候给大家看过这样的网站,该网站主要就是用来提供用户
来下载相关资源的网站,就叫做下载网站。
1583825943945
如何制作一个下载站点 :
nginx 使用的是模块 ngx_http_autoindex_module 来实现的,该模块处
理以斜杠 ("/") 结尾的请求,并生成目录列表。
nginx 编译的时候会自动加载该模块,但是该模块默认是关闭的,我们需
要使用下来指令来完成对应的配置
(1) autoindex: 启用或禁用目录列表输出
(2) autoindex_exact_size: 对应 HTLM 格式,指定是否在目录列表展示
文件的详细大小
默认为 on ,显示出文件的确切大小,单位是 bytes 。 改为 offff 后,显示出
文件的大概大小,单位是 kB 或者 MB 或者 GB
(3) autoindex_format :设置目录列表的格式
注意 : 该指令在 1.7.9 及以后版本中出现
(4) autoindex_localtime: 对应 HTML 格式,是否在目录列表上显示时
间。
默认为 off ,显示的文件时间为 GMT 时间。 改为 on 后,显示的文件时间
为文件的服务器时间
配置方式如下 :
location /download{
root /usr/local;
autoindex on;
autoindex_exact_size on;
autoindex_format html;
autoindex_localtime on;
}
XML/JSON 格式 [ 一般不用这两种方式 ]
三、Nginx的用户认证模块
对应系统资源的访问,我们往往需要限制谁能访问,谁不能访问。这块
就是我们通常所说的认证部分,认证需要做的就是根据用户输入的用户
名和密码来判定用户是否为合法用户,如果是则放行访问,如果不是则
拒绝访问。
Nginx 对应用户认证这块是通过 ngx_http_auth_basic_module 模块来实
现的,它允许通过使用 "HTTP 基本身份验证 " 协议验证用户名和密码来限
制对资源的访问。默认情况下 nginx 是已经安装了该模块,如果不需要则
使用 --without-http_auth_basic_module 。
该模块的指令比较简单,
(1) auth_basic: 使用 “ HTTP 基本认证 ” 协议启用用户名和密码的验证
开启后,服务端会返回 401 ,指定的字符串会返回到客户端,给用户以
提示信息,但是不同的浏览器对内容的展示不一致。
(2) auth_basic_user_fifile: 指定用户名和密码所在文件
指定文件路径,该文件中的用户名和密码的设置,密码需要进行加密。
可以采用工具自动生成
实现步骤 :
1.nginx.conf 添加如下内容
location /download{
root /usr/local;
autoindex on;
autoindex_exact_size on;
autoindex_format html;
autoindex_localtime on;
auth_basic 'please input your auth';
auth_basic_user_file htpasswd;
}
2. 我们需要使用 htpasswd 工具生成
yum install -y httpd-tools
htpasswd -c /usr/local/nginx/conf/htpasswd username //
创建一个新文件记录用户名和密码
htpasswd -b /usr/local/nginx/conf/htpasswd username
password // 在指定文件新增一个用户名和密码
htpasswd -D /usr/local/nginx/conf/htpasswd username //
从指定文件删除一个用户信息
htpasswd -v /usr/local/nginx/conf/htpasswd username //
验证用户名和密码是否正确
上述方式虽然能实现用户名和密码的验证,但是大家也看到了,所有的
用户名和密码信息都记录在文件里面,如果用户量过大的话,这种方式
就显得有点麻烦了,这时候我们就得通过后台业务代码来进行用户权限
的校验了。
四、Nginx的扩展模块
Nginx 是可扩展的,可用于处理各种使用场景。本节中,我们将探讨使
用 Lua 扩展 Nginx 的功能。
4.1Lua
概念
Lua 是一种轻量、小巧的脚本语言,用标准 C 语言编写并以源代码形式开
发。设计的目的是为了嵌入到其他应用程序中,从而为应用程序提供灵
活的扩展和定制功能。
特性
跟其他语言进行比较, Lua 有其自身的特点:
(1)轻量级
Lua 用标准 C 语言编写并以源代码形式开发,编译后仅仅一百余千字节,可
以很方便的嵌入到其他程序中。
(2)可扩展
Lua 提供非常丰富易于使用的扩展接口和机制,由宿主语言 ( 通常是 C 或
C++) 提供功能, Lua 可以使用它们,就像内置的功能一样。
(3)支持面向过程编程和函数式编程
应用场景
Lua 在不同的系统中得到大量应用,场景的应用场景如下 :
游戏开发、独立应用脚本、 web 应用脚本、扩展和数据库插件、系统安
全上。
Lua 的安装
在 linux 上安装 Lua 非常简单,只需要下载源码包并在终端解压、编译即
可使用。
Lua 的官网地址为 : https://www.lua.org
1. 点击 download 可以找到对应版本的下载地址,我们本次课程采用的
是 lua-5.3.5, 其对应的资源链接地址为 https://www.lua.org/ftp/lua-5.
4.1.tar.gz, 也可以使用 wget 命令直接下载 :
wget https://www.lua.org/ftp/lua-5.4.1.tar.gz
2. 编译安装
cd lua-5.4.1
make linux test
make install
如果在执行 make linux test 失败,报如下错误 :
说明当前系统缺少 libreadline-dev 依赖包,需要通过命令来进行安装
yum install -y readline-devel
Lua 的语法
Lua 和 C/C++ 语法非常相似,整体上比较清晰,简洁。条件语句、循环语
句、函数调用都与 C/C++ 基本一致。如果对 C/C++ 不太熟悉的同学来说,
也没关系,因为天下语言是一家,基本上理解起来都不会太困难。我们
一点点来讲。
第一个 Lua 程序
大家需要知道的是, Lua 有两种交互方式,分别是 : 交互式和脚本式,这
两者的区别,下面我们分别来讲解下:
交互式之 HELLOWORLD
交互式是指可以在命令行输入程序,然后回车就可以看到运行的效果。
Lua 交互式编程模式可以通过命令 lua -i 或 lua 来启用 :
在命令行中 key 输入如下命令,并按回车 , 会有输出在控制台:
脚本式之 HELLOWORLD
脚本式是将代码保存到一个以 lua 为扩展名的文件中并执行的方式。
方式一 :
我们需要一个文件名为 hello.lua, 在文件中添加要执行的代码,然后通过
命令 lua hello.lua 来执行,会在控制台输出对应的结果。
hello.lua
print("Hello World!!")
方式二 :
将 hello.lua 做如下修改
#!/usr/local/bin/lua
print("Hello World!!!")
第一行用来指定 Lua 解释器所在位置为 /usr/local/bin/lua ,加上 # 号标记
解释器会忽略它。一般情况下 #! 就是用来指定用哪个程序来运行本文
件。但是 hello.lua 并不是一个可执行文件,需要通过 chmod 来设置可执
行权限,最简单的方式为 :
补充一点,如果想在交互式中运行脚本式的 hello.lua 中的内容,我们可
以使用一个 dofile 函数,如:
dofile("lua_demo/hello.lua")
注意 : 在 Lua 语言中,连续语句之间的分隔符并不是必须的,也就是说后
面不需要加分号,当然加上也不会报错,
在 Lua 语言中,表达式之间的换行也起不到任何作用。如以下四个写
法,其实都是等效的
写法一
a=1
b=a+2
写法二
a=1;
b=a+2;
写法三
a=1; b=a+2;
写法四
a=1 b=a+2
不建议使用第四种方式,可读性太差。
Lua 的注释
关于 Lua 的注释要分两种,第一种是单行注释,第二种是多行注释。
单行注释的语法为:
-- 注释内容
多行注释的语法为 :
--[[
注释内容
注释内容
--]]
如果想取消多行注释,只需要在第一个 -- 之前在加一个 - 即可,如:
标识符
换句话说标识符就是我们的变量名, Lua 定义变量名以一个字母 A 到 Z
或 a 到 z 或下划线 _ 开头后加上 0 个或多个字母,下划线,数字(0到 9)。这块建议大家最好不要使用下划线加大写字母的标识符,因为 Lua 的保留字也是这样定义的,容易发生冲突。注意Lua 是区分大小写字母 的。
A0
关键字
下列是 Lua 的关键字,大家在定义常量、变量或其他用户自定义标识符
都要避免使用以下这些关键字:
一般约定,以下划线开头连接一串大写字母的名字(比如 _VERSION )
被保留用于 Lua 内部全局变量。这个也是上面我们不建议这么定义标识
符的原因。
运算符
Lua 中支持的运算符有算术运算符、关系运算符、逻辑运算符、其他运
算符。
算术运算符 :
+ 加法
- 减法
* 乘法
/ 除法
% 取余
^ 乘幂
- 负号
例如 :
10+20 -->30
20-10 -->10
10*20 -->200
20/10 -->2
3%2 -->1
10^2 -->100
-10 -->-10
关系运算符
== 等于
~= 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于
例如 :
10==10 -->true
10~=10 -->false
20>10 -->true
20<10 -->false
20>=10 -->true
20<=10 -->false
逻辑运算符
and 逻辑与 A and B &&
or 逻辑或 A or B ||
not 逻辑非 取反,如果为 true, 则返回 false !
逻辑运算符可以作为 if 的判断条件,返回的结果如下 :
A = true
B = true
A and B -->true
A or B -->true
not A -->false
A = true
B = false
A and B -->false
A or B -->true
not A -->false
A = false
B = true
A and B -->false
A or B -->true
not A -->true
其他运算符
.. 连接两个字符串
# 一元预算法,返回字符串或表的长度
例如 :
> "HELLO ".."WORLD" -->HELLO WORLD
> #"HELLO" -->5
全局变量 & 局部变量
在 Lua 语言中,全局变量无须声明即可使用。在默认情况下,变量总是
认为是全局的,如果未提前赋值,默认为 nil:
要想声明一个局部变量,需要使用 local 来声明
Lua 数据类型
Lua 有 8 个数据类型
nil( 空,无效值 )
boolean( 布尔, true/false)
number( 数值 )
string( 字符串 )
function( 函数 )
table (表)
thread( 线程 )
userdata (用户数据)
可以使用 type 函数测试给定变量或者的类型:
print(type(nil)) -->nil
print(type(true)) --> boolean
print(type(1.1*1.1)) --> number
print(type("Hello world")) --> string
print(type(io.stdin)) -->userdata
print(type(print)) --> function
print(type(type)) -->function
print(type{}) -->table
print(type(type(X))) --> string
nil
nil 是一种只有一个 nil 值的类型,它的作用可以用来与其他所有值进行区
分,也可以当想要移除一个变量时,只需要将该变量名赋值为 nil, 垃圾回
收就会会释放该变量所占用的内存。
boolean
boolean 类型具有两个值, true 和 false 。 boolean 类型一般被用来做条
件判断的真与假。在 Lua 语言中,只会将 false 和 nil 视为假,其他的都视
为真,特别是在条件检测中 0 和空字符串都会认为是真,这个和我们熟悉
的大多数语言不太一样。
number
在 Lua5.3 版本开始, Lua 语言为数值格式提供了两种选择 :integer( 整型 )
和 float( 双精度浮点型 )[ 和其他语言不太一样, float 不代表单精度类型 ] 。
数值常量的表示方式 :
>4 -->4
>0.4 -->0.4
>4.75e-3 -->0.00475
>4.75e3 -->4750
不管是整型还是双精度浮点型,使用 type() 函数来取其类型,都会返回的
是 number
>type(3) -->number
>type(3.3) -->number
所以它们之间是可以相互转换的,同时,具有相同算术值的整型值和浮
点型值在 Lua 语言中是相等的
string
Lua 语言中的字符串即可以表示单个字符,也可以表示一整本书籍。在
Lua 语言中,操作 100K 或者 1M 个字母组成的字符串的程序很常见。
可以使用单引号或双引号来声明字符串
>a = "hello"
>b = 'world'
>print(a) -->hello
>print(b) -->world
如果声明的字符串比较长或者有多行,则可以使用如下方式进行声明
html = [[
Lua-string
]]
table
table 是 Lua 语言中最主要和强大的数据结构。使用表, Lua 语言可以以
一种简单、统一且高效的方式表示数组、集合、记录和其他很多数据结
构。 Lua 语言中的表本质上是一种辅助数组。这种数组比 Java 中的数组
更加灵活,可以使用数值做索引,也可以使用字符串或其他任意类型的
值作索引 ( 除 nil 外 ) 。
创建表的最简单方式 :
> a = {}
创建数组 :
我们都知道数组就是相同数据类型的元素按照一定顺序排列的集合,那
么使用 table 如何创建一个数组呢 ?
>arr = {"TOM","JERRY","ROSE"}
要想获取数组中的值,我们可以通过如下内容来获取 :
print(arr[0]) nil
print(arr[1]) TOM
print(arr[2]) JERRY
print(arr[3]) ROSE
从上面的结果可以看出来,数组的下标默认是从 1 开始的。所以上述创建
数组,也可以通过如下方式来创建
>arr = {}
>arr[1] = "TOM"
>arr[2] = "JERRY"
>arr[3] = "ROSE"
上面我们说过了,表的索引即可以是数字,也可以是字符串等其他的内
容,所以我们也可以将索引更改为字符串来创建
>arr = {}
>arr["X"] = 10
>arr["Y"] = 20
>arr["Z"] = 30
当然,如果想要获取这些数组中的值,可以使用下面的方式
方式一
>print(arr["X"])
>print(arr["Y"])
>print(arr["Z"])
方式二
>print(arr.X)
>print(arr.Y)
>print(arr.Z)
当前 table 的灵活不进于此,还有更灵活的声明方式
>arr = {"TOM",X=10,"JERRY",Y=20,"ROSE",Z=30}
如何获取上面的值 ?
TOM : arr[1]
10 : arr["X"] | arr.X
JERRY: arr[2]
20 : arr["Y"] | arr.Y
ROESE?
function
在 Lua 语言中,函数( Function )是对语句和表达式进行抽象的主要方
式。
定义函数的语法为 :
function functionName(params)
end
函数被调用的时候,传入的参数个数与定义函数时使用的参数个数不一
致的时候, Lua 语言会通过 抛弃多余参数和将不足的参数设为 nil 的方
式来调整参数的个数。
function f(a,b)
print(a,b)
end
f() --> nil nil
f(2) --> 2 nil
f(2,6) --> 2 6
f(2.6.8) --> 2 6 (8 被丢弃 )
可变长参数函数
function add(...)
a,b,c=...
print(a)
print(b)
print(c)
end
add(1,2,3) --> 1 2 3
函数返回值可以有多个,这点和 Java 不太一样
function f(a,b)
return a,b
end
x,y=f(11,22) --> x=11,y=22
thread
thread 翻译过来是线程的意思,在 Lua 中, thread 用来表示执行的独立
线路,用来执行协同程序。
userdata
userdata 是一种用户自定义数据,用于表示一种由应用程序或 C/C++ 语
言库所创建的类型。
Lua 控制结构
Lua 语言提供了一组精简且常用的控制结构,包括用于条件执行的证 以 及用于循环的 while、 repeat 和 for 。 所有的控制结构语法上都有一个 显式的终结符: end 用于终结 if 、 for 及 while 结构, until 用于终结 repeat 结构。
if then elseif else
if 语句先测试其条件,并根据条件是否满足执行相应的 then 部分或 else
部分。 else 部分 是可选的。
function testif(a)
if a>0 then
print("a 是正数 ")
end
end
function testif(a)
if a>0 then
print("a 是正数 ")
else
print("a 是负数 ")
end
end
如果要编写嵌套的 if 语句,可以使用 elseif 。 它类似于在 else 后面紧跟
一个 if 。根据传入的年龄返回不同的结果,如
age<=18 青少年,
age>18 , age <=45 青年
age>45 , age<=60 中年人
age>60 老年人
function show(age)
if age<=18 then
return " 青少年 "
elseif age>18 and age<=45 then
return " 青年 "
elseif age>45 and age<=60 then
return " 中年人 "
elseif age>60 then
return " 老年人 "
end
end
while 循环
顾名思义,当条件为真时 while 循环会重复执行其循环体。 Lua 语言先
测试 while 语句 的条件,若条件为假则循环结束;否则, Lua 会执行循
环体并不断地重复这个过程。
语法:
例子 : 实现数组的循环
function testWhile()
local i = 1
while i<=10 do
print(i)
i=i+1
end
end
repeat 循环
顾名思义, repeat-until 语句会重复执行其循环体直到条件为真时结
束。 由于条件测试在循环体之后执行,所以循环体至少会执行一次。
语法
repeat
循环体
until 条件
function testRepeat()
local i = 10
repeat
print(i)
i=i-1
until i < 1
end
for 循环
数值型 for 循环
语法
for param=exp1,exp2,exp3 do
循环体
end
param 的值从 exp1 变化到 exp2 之前的每次循环会执行 循环体,并在每
次循环结束后将步长 (step)exp3 增加到 param 上。 exp3 可选,如果不设
置默认为 1
for i = 1,100,10 do
print(i)
end
泛型 for 循环
泛型 for 循环通过一个迭代器函数来遍历所有值,类似于 java 中的
foreach 语句。
语法
for i,v in ipairs(x) do
循环体
end
i 是数组索引值, v 是对应索引的数组元素值, ipairs 是 Lua 提供的一个迭
代器函数,用来迭代数组, x 是要遍历的数组。
例如 :
arr = {"TOME","JERRY","ROWS","LUCY"}
for i,v in ipairs(arr) do
print(i,v)
end
上述实例输出的结果为
1 TOM
2 JERRY
3 ROWS
4 LUCY
但是如果将 arr 的值进行修改为
arr = {"TOME","JERRY","ROWS",x="JACK","LUCY"}
同样的代码在执行的时候,就只能看到和之前一样的结果,而其中的 x 为
JACK 就无法遍历出来,缺失了数据,如果解决呢 ?
我们可以将迭代器函数变成 pairs, 如
for i,v in pairs(arr) do
print(i,v)
end
上述实例就输出的结果为
1 TOM
2 JERRY
3 ROWS
4 LUCY
x JACK
4.2ngx_lua模块概念
淘宝开发的 ngx_lua 模块通过将 lua 解释器集成进 Nginx ,可以采用 lua 脚
本实现业务逻辑,由于 lua 的紧凑、快速以及内建协程,所以在保证高并
发服务能力的同时极大地降低了业务逻辑实现成本。
4.3ngx_lua模块环境准备
方式一:lua-nginx-module
1. LuaJIT是采用C语言编写的Lua代表的解释器。
官网地址为 : http://luajit.org/
在官网上找到对应的下载地址 : http://luajit.org/download/LuaJIT-2.0.5.t
ar.gz
方式二 :OpenRestry
概述
前面我们提到过, OpenResty 是由淘宝工程师开发的,所以其官方网站
( http://openresty.org/ ) 我们读起来是非常的方便。 OpenResty 是一个基
于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua
库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并
发、扩展性极高的动态 Web 应用、 Web 服务和动态网关。所以本身
OpenResty 内部就已经集成了 Nginx 和 Lua ,所以我们使用起来会更加方
便。
4.4ngx_lua的使用
使用Lua编写Nginx脚本的基本构建块是指令。指令用于指定何时运行用
户 Lua 代码以及如何使用结果。下图显示了执行指令的顺序。
4.5ngx_lua操作Redis
Redis 在系统中经常作为数据缓存、内存数据库使用,在大型系统中扮演
着非常重要的作用。在 Nginx 核心系统中, Redis 是常备组件。 Nginx 支
持 3 种方法访问 Redis, 分别是 HttpRedis 模块、 HttpRedis2Module 、 lua
resty-redis 库。这三种方式中 HttpRedis 模块提供的指令少,功能单一,
适合做简单缓存, HttpRedis2Module 模块比 HttpRedis 模块操作更灵
活,功能更强大。而 Lua-resty-redis 库是 OpenResty 提供的一个操作
Redis 的接口库,可根据自己的业务情况做一些逻辑处理,适合做复杂的
业务逻辑。所以本次课程将主要以 Lua-resty-redis 来进行讲解。
lua-resty-redis 环境准备
步骤一 : 准备一个 Redis 环境
4.6ngx_lua操作Mysql
MySQL 是一个使用广泛的关系型数据库。在 ngx_lua 中, MySQL 有两种
访问模式 , 分别是使
(1)用 ngx_lua 模块和 lua-resty-mysql 模块:这两个模块是安装
OpenResty 时默认安装的。
(2)使用 drizzle_nginx_module(HttpDrizzleModule) 模块:需要单独
安装,这个库现不在 OpenResty 中。
lua-resty-mysql
lua-resty-mysql 是 OpenResty 开发的模块,使用灵活、功能强大,适合
复杂的业务场景,同时支持存储过程的访问。
使用 lua-resty-mysql 实现数据库的查询
使用 lua-cjson 处理查询结果
通过上述的案例学习, read_result() 得到的结果 res 都是 table 类型,要想
在页面上展示,就必须知道 table 的具体数据结构才能进行遍历获取。处
理起来比较麻烦,接下来我们介绍一种简单方式 cjson ,使用它就可以将
table 类型的数据转换成 json 字符串,把 json 字符串展示在页面上即可。
具体如何使用 ?
步骤一:引入 cjson
lua-resty-mysql 实现数据库的增删改
优化 send_query 和 read_result
本方法是 send_query 和 read_result 组合的快捷方法。
语法 :
有了该 API ,上面的代码我们就可以进行对应的优化,如下 :
综合小案例
使用 ngx_lua 模块完成 Redis 缓存预热。
分析 :
(1)先得有一张表 (users)
(2)浏览器输入如下地址
http://191.168.200.133?username=TOM
(3)从表中查询出符合条件的记录,此时获取的结果为 table 类型
(4)使用 cjson 将 table 数据转换成 json 字符串
(5)将查询的结果数据存入 Redis 中
init_by_lua_block{
redis = require "resty.redis"
mysql = require "resty.mysql"
cjson = require "cjson"
}
location /{
default_type "text/html";
content_by_lua_block{
-- 获取请求的参数 username
local param = ngx.req.get_uri_args()
["username"]
-- 建立 mysql 数据库的连接
local db = mysql:new()
local ok,err = db:connect{
host="192.168.200.111",
port=3306,
user="root",
password="123456",
database="nginx_db"
}
if not ok then
ngx.say("failed connect to
mysql:",err)
return
end
-- 设置连接超时时间
db:set_timeout(1000)
-- 查询数据
local sql = "";
if not param then 31 sql="select * from users"
else
sql="select * from users where
username=".."'"..param.."'"
end
local
res,err,errcode,sqlstate=db:query(sql)
if not res then
ngx.say("failed to query from
mysql:",err)
return
end
-- 连接 redis
local rd = redis:new()
ok,err =
rd:connect("192.168.200.111",6379)
if not ok then
ngx.say("failed to connect to
redis:",err)
return
end
rd:set_timeout(1000)
-- 循环遍历数据
for i,v in ipairs(res) do
rd:set("user_"..v.username,cjson.encode(v))
end
ngx.say("success")
rd:close()
db:close()
}
}