1.准备
1.1 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 等都进行一致的高性能响应。
关于OpenResty的搭建,可以参考官方提供的网址进行搭建。http://openresty.org/cn/installation.html ,我们采用源码安装的方式进行安装。
官方提供了源码安装的方式:http://openresty.org/cn/linux-packages.html
1.2 OpenResty搭建
1)安装依赖库:
$ yum install libtermcap-devel ncurses-devel libevent-devel readline-devel pcre-devel gcc openssl openssl-devel per perl wget
2)下载安装包:
3)解压安装包
$ mkdir -p /app/openresty
$ tar -xf openresty-1.21.4.4.tar.gz
$ tar -xf ngx_cache_purge-2.3.tar.gz
$ ls
ngx_cache_purge-2.3 ngx_cache_purge-2.3.tar.gz openresty-1.21.4.4 openresty-1.21.4.4.tar.gz
4)进入安装包,并安装
#进入安装包
$ cd /app/openresty/openresty-1.21.4.4
#安装
$ ./configure --prefix=/app/openresty --with-luajit --without-http_redis2_module --with-http_stub_status_module --with-http_v2_module --with-http_gzip_static_module --with-http_sub_module --add-module=/app/openresty/ngx_cache_purge-2.3/
#编译并安装
$ make && make install
说明:
--prefix=/app/openresty :安装路径
--with-luajit:安装luajit相关库,luajit是lua的一个高效版,LuaJIT的运行速度比标准Lua快数十倍。
--without-http_redis2_module:现在使用的Redis都是3.x以上版本,这里不推荐使用Redis2,表示不安装redis2支持的lua库
--with-http_stub_status_module:Http对应状态的库
--with-http_v2_module:对Http2的支持
--with-http_gzip_static_module:gzip服务端压缩支持
--with-http_sub_module:过滤器,可以通过将一个指定的字符串替换为另一个字符串来修改响应
--add-module=/app/openresty/ngx_cache_purge-2.3/ :Nginx代理缓存清理工具
如下所示安装完成后,在/app/openrestry/nginx
目录下是安装好的nginx,以后我们将在该目录的nginx下实现网站发布。
make[2]: Leaving directory `/app/openresty/openresty-1.21.4.4/build/nginx-1.21.4'
make[1]: Leaving directory `/app/openresty/openresty-1.21.4.4/build/nginx-1.21.4'
mkdir -p /app/openresty/site/lualib /app/openresty/site/pod /app/openresty/site/manifest
ln -sf /app/openresty/nginx/sbin/nginx /app/openresty/bin/openresty
5)配置环境变量:
$ vim /etc/profile
export PATH=/app/openresty/nginx/sbin:$PATH
$ source /etc/profile
6)开机启动:
linux系统结构/lib/systemd/system/
目录,该目录自动存放启动文件的配置位置,里面一般包含有xxx.service
,例如systemctl enable nginx.service
,就是调用 /lib/systemd/system/nginx.service
文件,使nginx开机启动。
我们可以创建/usr/lib/systemd/system/nginx.service
,在该文件中编写启动nginx脚本:
[Service]
Type=forking
PIDFile=/app/openresty/nginx/logs/nginx.pid
ExecStartPre=/app/openresty/nginx/sbin/nginx -t
ExecStart=/app/openresty/nginx/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target
执行systemctl daemon-reload
: 重新加载某个服务的配置文件
执行systemctl enable nginx.service
: 开机启动
执行systemctl start nginx.service
: 启动nginx
$ vim /usr/lib/systemd/system/nginx.service
$ systemctl daemon-reload
$ systemctl enable nginx.service
Created symlink from /etc/systemd/system/multi-user.target.wants/nginx.service to /usr/lib/systemd/system/nginx.service.
$ systemctl start nginx.service
浏览器访问http://192.168.254.160/
,或者在linux中用 curl
$ curl http://192.168.254.160/
查看当前已经开放的端口
firewall-cmd --list-ports
开放 80 端口
firewall-cmd --zone=public --add-port=80/tcp --permanent
重新加载防火墙配置
firewall-cmd --reload
2.动静分离站点架构
2.1 动静分离架构分析
我们打开京东商城,搜索手机,查看网络可以发现响应页面后,页面又会发起很多请求,还没有查看多少信息就已经有393个请求发出了,而多数都是图片,一个人请求如此,人多了对后端造成的压力是非比寻常的,该如何降低静态资源对服务器的压力呢?
比如我们的商城项目完成后,项目上线后,如果所有请求都经过 Tomcat,并发量很大的时候,对项目而言将是灭顶之灾。
电商项目中一个请求返回的页面往往会再次发起很多请求,而绝大多数都是图片或者是css样式、js等静态资源,如果这些静态资源都去查询Tomcat,Tomcat的压力会增加数十倍甚至更高。
这时候我们需要采用动静分离的策略:
- 1、所有静态资源,经过Nginx,Nginx直接从指定磁盘或者nginx缓存中获取文件,然后IO输出给用户
- 2、如果是需要查询数据库数据的请求,就路由到Tomcat集群中,让Tomcat处理,并将结果响应给用户
2.2 门户静态站点发布
修改本地文件C:\Windows\System32\drivers\etc\HOSTS
文件,添加自定义域名www.zmall.com
解析到192.168.254.150
服务器,在HOSTS文件中添加如下配置即可:
192.168.254.150 www.zmall.com
新建目录 /app/mall/static
$ mkdir -p /app/mall/static
$ cd /app/mall/static
将front
商城静态文件上传到/app/mall/static
目录下,再修改/app/openresty/nginx/conf/nginx.conf
,配置如下:
#门户发布
server {
listen 80;
server_name localhost;
location / {
root /app/mall/static/frant;
index index.html index.htm;
}
}
重启nginx
$ nginx -t
nginx: the configuration file /usr/local/openresty/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/openresty/nginx/conf/nginx.conf test is successful
$ nginx -s reload
浏览器访问http://www.zmall.com/
就会出现我们上传的商城静态页了。
3.Lua脚本
3.1 Lua简介
Lua 是一个小巧的脚本语言。其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。所以Lua不适合作为开发独立应用程序的语言。Lua 有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。
Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替XML,ini等文件格式,并且更容易理解和维护。 Lua由标准C编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。 一个完整的Lua解释器不过200k,在所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择。
3.2 Lua安装
首先我们准备一个linux虚拟机来安装Lua,在linux系统中按照如下步骤进行安装:
$ curl -R -O http://www.lua.org/ftp/lua-5.4.7.tar.gz
$ tar xf lua-5.4.7.tar.gz
$ cd lua-5.4.7
$ make linux test
出现如下,表示安装成功:
make[2]: Leaving directory `/app/lua/lua-5.4.7/src'
make[1]: Leaving directory `/app/lua/lua-5.4.7/src'
make[1]: Entering directory `/app/lua/lua-5.4.7/src'
./lua -v
Lua 5.4.7 Copyright (C) 1994-2024 Lua.org, PUC-Rio
make[1]: Leaving directory `/app/lua/lua-5.4.7/src'
版本查看:lua -v
Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio
我们可以发现,Lua版本还是原来系统自带的版本,我们需要替换原来系统自带的lua,执行如下命令:
$ rm -rf /usr/bin/lua
$ ln -s /app/lua/lua-5.4.7/src/lua /usr/bin/lua
此时版本信息如下:
$ lua -v
Lua 5.4.7 Copyright (C) 1994-2024 Lua.org, PUC-Rio
4.多级缓存架构
项目运行过程中往往为了提升项目对数据加载效率,一般都会增加缓存,但缓存如何加载效率最高?如何加载对后端服务造成的压力最小?我们需要设计一套完善的缓存架构体系。
4.1 多级缓存架构分析
用户请求到达后端服务,先经过代理层nginx,nginx将请求路由到后端tomcat服务,tomcat去数据库中取数据,这是一个非常普通的流程。
比如用户请求非常大,存在10k~1000k的请求量,而tomcat中即使加了缓存,一台tomcat服务器处理请求的能力也就是500左右,这样部署多台仍然压力巨大甚至宕机的可能。
在大并发场景下,就需要做优化,缓存任是最有效的手段之一。当然缓存需要优化,执行过程如下:
1:请求到达Nginx,Nginx抗压能力极强。
2:Tomcat抗压能力很弱,如果直接将所有请求路由给Tomcat,Tomcat压力会非常大,很有可能宕机。我们可以在Nginx这里设置2道缓存,第1道是Redis缓存,第2道是Nginx缓存。
3:先加载Redis缓存,如果Redis没有缓存,则加载Nginx缓存,Nginx如果没有缓存,则将请求路由到Tomcat。
4:Tomcat发布的程序会加载数据,加载完成后需要做缓存的,及时将数据存入Redis缓存,再响应数据给用户。
5:用户下次查询的时候,查询Redis缓存或Nginx缓存。
6:后面用户请求的时候,就可以直接从Nginx缓存拿数据了,这样就可以实现后端Tomcat发布的服务被调用的次数大幅减少,负载大幅下降。
上面这套缓存架构被多个大厂应用,除了可以有效提高加载速度、降低后端服务负载之外,还可以防止缓存雪崩,为服务稳定健康打下了坚实的基础,这也就是鼎鼎有名的多级缓存架构体系。
4.2 推广商品高效加载
首页很多商品优先推荐展示,这些其实都是推广商品,并非真正意义上的热门商品,首页展示这些商品数据需要加载效率极高,并且商城首页访问频率也是极高,我们需要对首页数据做缓存处理,我们首先想到的就是Redis缓存。
4.2.1 表结构分析
推广商品并非只在首页出现,有可能在列表页、分类搜索页多个地方出现,因此可以设计一张表用于存放不同位置展示不同商品的表,推广产品推荐表如下:
CREATE TABLE `ad_items` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
`type` int(3) DEFAULT NULL COMMENT '分类,1首页推广,2列表页推广',
`sku_id` varchar(60) DEFAULT NULL COMMENT '展示的产品(对应Sku)',
`sort` int(11) DEFAULT NULL COMMENT '排序',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
4.2.2 根据推广表实现接口
1.推广商品接口编写: 通过Springboot 与 MybatisPlus 编写CRUD。
@Api(tags = "推广查询和操作列表")
@RestController
@RequestMapping("/ad/items/ops")
@RequiredArgsConstructor
public class AdItemsOpsController {
/**
* Spring构造器注入
* 通过 final 修饰,由 @RequiredArgsConstructor 构造器注解生成的构造器注入必要的属性类
*/
private final AdItemsOpsService adItemsOpsService;
/**
* 指定分类下的推广产品列表
* 这里返回结果不做封装,和 Redis存的数据保持一致
* @param id
* @return
*/
@ApiOperation("根据type类型查询推广产品列表")
@GetMapping(value = "getList")
public Result<List<SkuVO>> typeItems(@RequestParam("id") Integer id) {
//查询
List<SkuVO> adSkuItems = adItemsOpsService.typeSkuItems(id);
return Result.success(adSkuItems);
}
/**
* 根据推广分类删除推广产品列表
* @param id
* @return
*/
@ApiOperation("根据type类型删除推广产品列表")
@GetMapping(value = "/del")
public Result delTypeItems(@RequestParam(value = "id") Integer id) {
adItemsOpsService.delTypeSkuItems(id);
return Result.success();
}
/**
* 根据推广分类修改推广产品列表
* @param id
* @return
*/
@ApiOperation("根据type类型修改推广产品列表")
@GetMapping(value = "/update")
public Result updateTypeItems(@RequestParam(value = "id") Integer id) {
//修改
adItemsOpsService.updateTypeSkuItems(id);
return Result.success();
}
}
@Service
@RequiredArgsConstructor
public class AdItemsOpsServiceImpl implements AdItemsOpsService {
final SkuService skuService;
final AdItemsService adItemsService;
final RedisService redisService;
@Override
public List<SkuVO> typeSkuItems(Integer typeId) {
String key = RedisConstants.AD_ITEMS_PREFIX + typeId;
if (redisService.hasKey(key)) {
List<SkuVO> list = redisService.get(key, List.class);
return list;
}
//查询所有分类下的推广
LambdaQueryWrapper<AdItems> adItemsQueryWrapper = new LambdaQueryWrapper<>();
adItemsQueryWrapper.eq(AdItems::getType, typeId);
List<AdItems> adItems = adItemsService.list(adItemsQueryWrapper);
//获取所有SkuId
List<Long> skuIds = adItems.stream().map(AdItems::getSkuId).collect(Collectors.toList());
//批量查询Sku
List<Sku> skus = skuService.listByIds(skuIds);
List<SkuVO> results = new ArrayList<>(skus.size());
for (Sku sku : skus) {
SkuVO vo = new SkuVO();
BeanUtils.copyProperties(sku, vo);
results.add(vo);
}
redisService.set(key, skus, 60 * 60);
return results;
}
@Override
public void delTypeSkuItems(Integer typeId) {
String key = RedisConstants.AD_ITEMS_PREFIX + typeId;
redisService.delete(key);
}
@Override
public void updateTypeSkuItems(Integer typeId) {
this.delTypeSkuItems(typeId);
this.typeSkuItems(typeId);
}
}
# 访问列表接口地址
http://localhost:8100/ad/items/ops/getList?id=1
2.设置对应的Redis缓存。
# redis 设置的key
redisKey = ad:items:${id}
4.3 多级缓存-Lua+Redis
按照上面分析的架构,可以每次在Nginx的时候使用Lua脚本查询Redis,如果Redis有数据,则将数据存入到Nginx缓存,再将数据响应给用户,此时我们需要实现使用Lua将数据从Redis中加载出来。
$ mkdir -p /app/mall/lua
$ cd /app/mall/lua
我们在/app/mall/lua
中创建文件aditem.lua
,脚本如下:
--数据响应类型JSON
ngx.header.content_type="application/json;charset=utf8"
--Redis库依赖
local redis = require("resty.redis");
local cjson = require("cjson");
--获取id参数(type)
local id = ngx.req.get_uri_args()["id"];
--key组装
local key = "ad:items:"..id
--创建链接对象
local red = redis:new()
--设置超时时间
red:set_timeout(2000)
--设置服务器链接信息
red:connect("192.168.254.160", 6379)
-- 设置密码
red:auth('123456')
--查询指定key的数据
local result=red:get(key);
--关闭Redis链接
red:close()
if result==nil or result==null or result==ngx.null then
return true
else
--输出数据
ngx.say(result)
end
进入 /app/openresty/nginx/conf
目录, 修改nginx.conf
,添加如下配置:(最后记得将content_by_lua_file改成rewrite_by_lua_file)
#推广产品查询
location /ad/items/ops/getList {
content_by_lua_file /app/mall/lua/aditem.lua;
}
重启 nginx,nginx -s reload
。
访问
http://192.168.254.160/ad/items/ops/getList?id=1
就能获取到 redis中存储的json数据。
5. Nginx代理缓存
proxy_cache 是用于 proxy 模式的缓存功能,proxy_cache 在 Nginx 配置的 http 段、server 段中分别写入不同的配置。
http 段中的配置用于定义 proxy_cache 空间,server 段中的配置用于调用 http 段中的定义,启用对server 的缓存功能。
使用:
- 1、定义缓存空间
- 2、在指定地方使用定义的缓存
5.1 Nginx代理缓存介绍
1)开启Proxy_Cache缓存:
我们需要在nginx.conf中配置才能开启缓存:
proxy_cache_path /app/openresty/nginx/cache levels=1:2 keys_zone=proxy_cache:10m max_size=1g inactive=60m use_temp_path=off;
参数说明:
【proxy_cache_path】指定缓存存储的路径,缓存存储在 /app/openresty/nginx/cache目录
【levels=1:2】设置一个两级目录层次结构存储缓存,在单个目录中包含大量文件会降低文件访问速度,因此我们建议对大多数部署使用两级目录层次结构。如果 levels 未包含该参数,Nginx 会将所有文件放在同一目录中。
【keys_zone=proxy_cache:10m】设置共享内存区域,用于存储缓存键和元数据,例如使用计时器。拥有内存中的密钥副本,Nginx 可以快速确定请求是否是一个 HIT 或 MISS 不必转到磁盘,从而大大加快了检查速度。1 MB 区域可以存储大约 8,000 个密钥的数据,因此示例中配置的 10 MB 区域可以存储大约 80,000 个密钥的数据。
【max_size=1g】设置缓存大小的上限。它是可选的; 不指定值允许缓存增长以使用所有可用磁盘空间。当缓存大小达到限制时,一个称为缓存管理器的进程将删除最近最少使用的缓存,将大小恢复到限制之下的文件。
【inactive=60m】指定项目在未被访问的情况下可以保留在缓存中的时间长度。在此示例中,缓存管理器进程会自动从缓存中删除 60 分钟未请求的文件,无论其是否已过期。默认值为 10 分钟(10m)。非活动内容与过期内容不同。Nginx 不会自动删除缓存 header 定义为已过期内容(例如 Cache-Control:max-age=120)。过期(陈旧)内容仅在指定时间内未被访问时被删除。访问过期内容时,Nginx 会从原始服务器刷新它并重置 inactive 计时器。
【use_temp_path=off】表示NGINX会将临时文件保存在缓存数据的同一目录中。这是为了避免在更新缓存时,磁盘之间互相复制响应数据,我们一般关闭该功能。
2)Proxy_Cache属性:
proxy_cache:设置是否开启对后端响应的缓存,如果开启的话,参数值就是zone的名称,比如:proxy_cache。
proxy_cache_valid:针对不同的response code设定不同的缓存时间,如果不设置code,默认为200,301,302,也可以用any指定所有code。
proxy_cache_min_uses:指定在多少次请求之后才缓存响应内容,这里表示将缓存内容写入到磁盘。
proxy_cache_lock:默认不开启,开启的话则每次只能有一个请求更新相同的缓存,其他请求要么等待缓存有数据要么限时等待锁释放;nginx 1.1.12才开始有。配套着proxy_cache_lock_timeout一起使用。
proxy_cache_key:缓存文件的唯一key,可以根据它实现对缓存文件的清理操作。
5.2 Nginx代理缓存热点数据应用
1)定义代理缓存空间
修改nginx.conf
,添加如下配置:
操作步骤:
vim nginx.conf
修改位置如下:
...
keepalive_timeout 65;
#gzip on;
# 缓存空间定义
proxy_cache_path /app/openresty/nginx/cache levels=1:2 keys_zone=proxy_cache:10m max_size=1g inactive=60m use_temp_path=off;
server {
listen 80;
server_name www.zmall.com;
#charset koi8-r;
...
}
2)开启代理缓存
修改nginx.conf
,添加如下配置:
#门户发布
server {
listen 80;
server_name localhost;
#推广产品查询
location /ad/items/getList {
#先找redis缓存,content_by_lua_file表示用文件执行返回的数据,不继续执行下面的指令
#content_by_lua_file /app/mall/lua/aditem.lua;
#先找Nginx缓存,rewrite_by_lua_file表示aditem.lua脚本执行后没查到数据返回true时,会继续执行下面的指令
rewrite_by_lua_file /app/mall/lua/aditem.lua;
#启用缓存openresty_cache
proxy_cache proxy_cache;
#针对指定请求缓存
#proxy_cache_methods GET;
#设置指定请求会缓存
proxy_cache_valid 200 304 60s;
#最少请求1次才会缓存
proxy_cache_min_uses 1;
#如果并发请求,只有第1个请求会去服务器获取数据
#proxy_cache_lock on;
#唯一的key
proxy_cache_key $host$uri$is_args$args;
#动态代理
proxy_pass http://192.168.254.160:8100;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
#其他所有请求
location / {
root /app/mall/static/frant;
index index.html index.htm;
}
}
重启nginx或者重新加载配置文件nginx -s reload
,再次测试,可以发现下面个规律:
- 1: 先查找Redis缓存
- 2: Redis缓存没数据,直接找Nginx缓存
- 3: Nginx缓存没数据,则找真实服务器
我们还可以发现cache目录下多了目录和一个文件,这就是Nginx缓存:
$ cd /app/openresty/nginx/cache
$ ll 2/a8
#d𨀿ÿÿÿÿÿÿ椵fb®´Access-Control-Request-Headers7©§°-¬ӈ§ډ
KEY: 192.168.254.160/ad/items/ops/getList?id=1
HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Date: Thu, 26 Sep 2024 13:38:47 GMT
Connection: close
{"code":200,"msg":"success","data":[{"id":"1318594982227025922","name":"华为Mate40 Pro 32G",...}]}
这里缓存的 192.168.254.160/ad/items/ops/getList?id=1
,value就是下面的json数据
5.3 Cache_Purge代理缓存清理
很多时候我们如果不想等待缓存的过期,想要主动清除缓存,可以采用第三方的缓存清除模块清除缓存 nginx_ngx_cache_purge
。
安装nginx的时候,需要添加purge
模块,purge
模块我们已经下载了,在/app/openresty
目录下,添加该模块--add-module=/app/openresty/ngx_cache_purge-2.3/
,这一个步骤我们在安装OpenRestry
的时候已经实现了。
安装好了后,我们配置一个清理缓存的地址:
#清理缓存
location ~ /purge(/.*) {
#清理缓存
proxy_cache_purge proxy_cache $host$1$is_args$args;
}
此时访问 http://192.168.254.160/purge/ad/items/ops/getList?id=1
,表示清除缓存,如果出现如下效果表示清理成功:
<html>
<head>
<title>Successful purge</title>
</head>
<body bgcolor="white">
<center>
<h1>Successful purge</h1>
<br>Key : www.zmall.com/ad/items/getList?id=1
<br>Path: /usr/local/openresty/nginx/cache/2/a8/3fedd64f94f55e3982ab6cad32a4ca82
</center>
<hr>
<center>openresty/1.21.4.4</center>
</body>
</html>
我们可以查看 nginx下的缓存还有没有
$ cd /app/openresty/nginx/cache
$ ll 9/53/
total 0
可以看到缓存文件已经被清空了。
下面贴出完整 nginx.conf 文件内容:
#user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
proxy_cache_path /app/openresty/nginx/cache levels=1:2 keys_zone=proxy_cache:10m max_size=1g inactive=60m use_temp_path=off;
server {
listen 80;
server_name localhost;
location /ad/items/ops/getList {
#content_by_lua_file /app/mall/lua/aditem.lua;
#先找Nginx缓存,rewrite_by_lua_file表示aditem.lua脚本执行后没查到数据返回true时,会继续执行下面的指令
rewrite_by_lua_file /app/mall/lua/aditem.lua;
#启用缓存openresty_cache
proxy_cache proxy_cache;
#针对指定请求缓存
#proxy_cache_methods GET;
#设置指定请求会缓存
proxy_cache_valid 200 304 60s;
#最少请求1次才会缓存
proxy_cache_min_uses 1;
#如果并发请求,只有第1个请求会去服务器获取数据
proxy_cache_lock on;
#唯一的key
proxy_cache_key $host$uri$is_args$args;
##动态代理
proxy_pass http://192.168.254.160:8100;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location ~ /purge(/.*) {
proxy_cache_purge proxy_cache $host$1$is_args$args;
}
location / {
root /app/mall/static/frant;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
6.缓存一致性
上面我们虽然实现了多级缓存架构,但是问题也出现了,如果数据库中数据发生变更,如何更新Redis缓存呢?如何更新Nginx缓存呢?
我们可以使用阿里巴巴的技术解决方案Canal来实现,通过Canal监听数据库变更,并实时消费变更数据,并更新缓存。
学习地址:https://github.com/alibaba/canal
基于日志增量订阅和消费的业务包括
- 数据库镜像
- 数据库实时备份
- 索引构建和实时维护(拆分异构索引、倒排索引等)
- 业务 cache 刷新
- 带业务逻辑的增量数据处理
6.1 Canal原理讲解
MySQL主备复制原理
- MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
- MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
- MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
Canal 工作原理
- canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
- canal 解析 binary log 对象(原始为 byte 流)
6.2 MySQL开启binlog
对于MySQL , 需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,my.cnf 中配置如下
$ vim /etc/my.cnf
在最文件尾部添加如下配置:
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
注意: 针对阿里云 RDS for MySQL , 默认打开了 binlog , 并且账号默认具有 binlog dump 权限 , 不需要任何权限或者 binlog 设置,可以直接跳过这一步。
授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant:
CREATE USER canal IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
FLUSH PRIVILEGES;
重启mysql容器
$ systemctl stop mysqld.service
$ systemctl start mysqld.service
查看是否开启binlog:
show variables like 'log_bin';
6.2 Canal安装
我们采用docker安装方式:
6.2.1 canal-admin安装
1.创建数据库
create database canal_manager;
CREATE USER canal IDENTIFIED BY 'canal';
grant all privileges on canal_manager.* to canal@'%';
flush privileges;
2.canal_manager.sql
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `canal_manager` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_bin */;
USE `canal_manager`;
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for canal_adapter_config
-- ----------------------------
DROP TABLE IF EXISTS `canal_adapter_config`;
CREATE TABLE `canal_adapter_config` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`category` varchar(45) NOT NULL,
`name` varchar(45) NOT NULL,
`status` varchar(45) DEFAULT NULL,
`content` text NOT NULL,
`modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for canal_cluster
-- ----------------------------
DROP TABLE IF EXISTS `canal_cluster`;
CREATE TABLE `canal_cluster` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(63) NOT NULL,
`zk_hosts` varchar(255) NOT NULL,
`modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for canal_config
-- ----------------------------
DROP TABLE IF EXISTS `canal_config`;
CREATE TABLE `canal_config` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`cluster_id` bigint(20) DEFAULT NULL,
`server_id` bigint(20) DEFAULT NULL,
`name` varchar(45) NOT NULL,
`status` varchar(45) DEFAULT NULL,
`content` text NOT NULL,
`content_md5` varchar(128) NOT NULL,
`modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `sid_UNIQUE` (`server_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for canal_instance_config
-- ----------------------------
DROP TABLE IF EXISTS `canal_instance_config`;
CREATE TABLE `canal_instance_config` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`cluster_id` bigint(20) DEFAULT NULL,
`server_id` bigint(20) DEFAULT NULL,
`name` varchar(45) NOT NULL,
`status` varchar(45) DEFAULT NULL,
`content` text NOT NULL,
`content_md5` varchar(128) DEFAULT NULL,
`modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `name_UNIQUE` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for canal_node_server
-- ----------------------------
DROP TABLE IF EXISTS `canal_node_server`;
CREATE TABLE `canal_node_server` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`cluster_id` bigint(20) DEFAULT NULL,
`name` varchar(63) NOT NULL,
`ip` varchar(63) NOT NULL,
`admin_port` int(11) DEFAULT NULL,
`tcp_port` int(11) DEFAULT NULL,
`metric_port` int(11) DEFAULT NULL,
`status` varchar(45) DEFAULT NULL,
`modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for canal_user
-- ----------------------------
DROP TABLE IF EXISTS `canal_user`;
CREATE TABLE `canal_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(31) NOT NULL,
`password` varchar(128) NOT NULL,
`name` varchar(31) NOT NULL,
`roles` varchar(31) NOT NULL,
`introduction` varchar(255) DEFAULT NULL,
`avatar` varchar(255) DEFAULT NULL,
`creation_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
-- ----------------------------
-- Records of canal_user
-- ----------------------------
BEGIN;
INSERT INTO `canal_user` VALUES (1, 'admin', '6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9', 'Canal Manager', 'admin', NULL, NULL, '2019-07-14 00:05:28');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
创建 canal 目录,编辑 application.yml
文件
mkdir -p /app/docker/canal/standalone/canal-admin/{conf,logs}
cd /app/docker/canal/standalone/canal-admin/conf
vim application.yml
文件内容如下:
server:
port: 8089
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
spring.datasource:
address: 192.168.254.160:3306
database: canal_manager
username: canal
password: canal
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false
hikari:
maximum-pool-size: 30
minimum-idle: 1
canal:
adminUser: admin
adminPasswd: admin
启动命令:
docker run --privileged -p 8089:8089 \
-v /app/docker/canal/standalone/canal-admin/conf:/home/admin/canal-admin/conf \
-v /app/docker/canal/standalone/canal-admin/logs:/home/admin/canal-admin/logs \
-v /app/docker/canal/standalone/canal-admin/conf/application.yml:/home/admin/canal-admin/conf/application.yml \
--name canal-admin \
-d canal/canal-admin:v1.1.7
登录canal-admin,ip为安装主机ip,端口为8089,用户为admin,密码为123456
访问 http://192.168.254.160:8089/
,进入 canal-admin 界面。
注:canal_manager.sql 提供的脚本中,canal_user 表提供的默认⽤户名为: canal,密码为:6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9,也就是 SELECT PASSWORD('123456'); 的值
所以登陆 canal-admin 管理平台的⽤户名就是 admin/123456
6.2.2 canal-server 安装
docker run -p 11111:11111 --name canal-server \
-e canal.admin.register.auto=true \
-e canal.admin.register.name=canal-server \
-e canal.admin.manager=192.168.254.160:8089 \
-e canal.admin.port=11110 \
-e canal.admin.user=admin \
-e canal.admin.passwd=6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 \
-d canal/canal-server:v1.1.7
docker run -p 11111:11111 --name canal-server --privileged=true \
-v /app/docker/canal/standalone/canal-server/conf/canal.properties:/home/admin/canal-server/conf/canal.properties \
-d canal/canal-server:v1.1.7
部署完成就可以看到server了,这里的serverip是容器的ip,我们不需要进行修改,修改会导致异常的。当我们需要使用的时候直接填写宿主机ip即可。
进入容器,修改核心配置 canal.properties
和instance.properties
,canal.properties
是canal自身的配置,instance.properties
是需要同步数据的数据库连接配置。
$ docker exec -it canal /bin/bash
$ cd /home/admin/canal-server/conf
修改配置如下:
# position info
canal.instance.master.address=192.168.254.160:3306
另一处配置:
# table regex
#canal.instance.filter.regex=.*\\..*
#监听配置
canal.instance.filter.regex=zmall_goods.ad_items
配置完成后,重启canal
容器
docker restart canal
6.4 Canal微服务搭建
1.引入依赖
<!-- Canal 相关 -->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.protocol</artifactId>
<version>1.1.7</version>
<exclusions>
<exclusion>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</exclusion>
<exclusion>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.7</version>
</dependency>
2.编写监听类
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.*;
import com.alibaba.otter.canal.protocol.Message;
import java.net.InetSocketAddress;
import java.util.List;
public class SimpleCanalClientExample {
public static void main(String args[]) {
// 创建链接
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress("192.168.254.160", 11111), "mall", "", "");
int batchSize = 1000;
try {
connector.connect();
connector.subscribe(".*\\..*");
connector.rollback();
while (true) {
// 获取指定数量的数据
Message message = connector.getWithoutAck(batchSize);
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
try {
Thread.sleep(2000);
System.out.println("-----------------------");
} catch (InterruptedException e) {
}
} else {
// System.out.printf("message[batchId=%s,size=%s] \n", batchId, size);
printEntry(message.getEntries());
}
connector.ack(batchId); // 提交确认
// connector.rollback(batchId); // 处理失败, 回滚数据
}
} finally {
connector.disconnect();
}
}
private static void printEntry(List<Entry> entrys) {
for (Entry entry : entrys) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
RowChange rowChage = null;
try {
rowChage = RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
e);
}
EventType eventType = rowChage.getEventType();
System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s",
entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
eventType));
for (RowData rowData : rowChage.getRowDatasList()) {
if (eventType == EventType.DELETE) {
printColumn(rowData.getBeforeColumnsList());
} else if (eventType == EventType.INSERT) {
printColumn(rowData.getAfterColumnsList());
} else {
System.out.println("-------> before");
printColumn(rowData.getBeforeColumnsList());
System.out.println("-------> after");
printColumn(rowData.getAfterColumnsList());
}
}
}
}
private static void printColumn(List<Column> columns) {
for (Column column : columns) {
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
}
}
}
修改监听的数据库表数据即可看到收到的信息。
评论区