文章目录
一、多级缓存概念
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能。
用作缓存的Nginx是业务Nginx,需要部署为集群,再有专门的Nginx用来做反向代理。
二、准备工作【导入案例,并搭建Nginx反向代理】
本章实现橙色部分
2.1 导入商品案例
2.1.1 安装MySQL
后期做数据同步
需要用到MySQL的主从功能,所以需要大家在虚拟机中,利用Docker来运行一个MySQL容器。
为了方便后期配置MySQL,我们先准备两个目录,用于挂载容器的数据和配置文件目录:
# 进入/tmp目录
cd /tmp
# 创建文件夹
mkdir mysql
# 进入mysql目录
cd mysql
进入mysql目录后,执行下面的Docker命令:
docker run \
-p 3306:3306 \
--name mysql \
-v $PWD/conf:/etc/mysql/conf.d \
-v $PWD/logs:/logs \
-v $PWD/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--privileged \
-d \
mysql:5.7.25
在/tmp/mysql/conf目录添加一个my.cnf文件,作为mysql的配置文件:
# 创建文件
touch /tmp/mysql/conf/my.cnf
文件的内容如下:
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
配置修改后,必须重启容器:
docker restart mysql
2.1.2 导入SQL
接下来,利用Navicat客户端连接MySQL,然后导入课前资料提供的sql文件:item.sql
其中包含两张表:
- tb_item:商品表,包含商品的基本信息
- tb_item_stock:商品库存表,包含商品的库存信息
之所以将库存分离出来,是因为库存是更新比较频繁的信息,写操作较多。而其他信息修改的频率非常低。
2.1.3 导入Demo工程
下面导入课前资料提供的工程:item-service
项目结构如图所示:
其中的业务包括:
- 分页查询商品
- 新增商品
- 修改商品
- 修改库存
- 删除商品
- 根据id查询商品
- 根据id查询库存
业务全部使用mybatis-plus来实现,如有需要请自行修改业务逻辑。
- 分页查询商品
在com.heima.item.web
包的ItemController
中可以看到接口定义:
- 新增商品
在com.heima.item.web
包的ItemController
中可以看到接口定义:
- 修改商品
在com.heima.item.web
包的ItemController
中可以看到接口定义:
- 修改库存
在com.heima.item.web
包的ItemController
中可以看到接口定义:
- 删除商品
在com.heima.item.web
包的ItemController
中可以看到接口定义:
这里是采用了逻辑删除,将商品状态修改为3
- 根据id查询商品
在com.heima.item.web
包的ItemController
中可以看到接口定义:
这里只返回了商品信息,不包含库存
- 根据id查询库存
在com.heima.item.web
包的ItemController
中可以看到接口定义:
2.1.4 启动
注意修改application.yml文件中配置的mysql地址信息:
需要修改为自己的虚拟机地址信息、还有账号和密码。
修改后,启动服务,访问:http://localhost:8081/item/10001即可查询数据
2.1.5 导入商品查询页面
商品查询是购物页面,与商品管理的页面是分离的。
部署方式如图:
我们需要准备一个反向代理的nginx服务器,如上图红框所示,将静态的商品页面放到nginx目录中。
页面需要的数据通过ajax向服务端(nginx业务集群)查询。
- 运行nginx服务
这里我已经给大家准备好了nginx反向代理服务器和静态资源。
我们找到课前资料的nginx目录:nginx-1.18.0
将其拷贝到一个非中文目录下,运行这个nginx服务。
运行命令:
start nginx.exe
然后访问 http://localhost/item.html?id=10001即可:
- 反向代理
现在,页面是假数据展示的。我们需要向服务器发送ajax请求,查询商品数据。
打开控制台,可以看到页面有发起ajax查询数据:
而这个请求地址同样是80端口,所以被当前的nginx反向代理了。
查看nginx的conf目录下的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 65;
upstream nginx-cluster{
server 192.168.150.101:8081;
}
server {
listen 80;
server_name localhost;
location /api {
proxy_pass http://nginx-cluster;
}
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
三、JVM进程缓存【第三级缓存】
本章实现红色框部分
3.1 本地进程缓存与分布式缓存的区别
3.2 本地进程缓存:Caffeine
Caffeine是一个基于|ava8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。
GitHub地址:https://github.com/ben-manes/caffeine
第一步:引入依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
第二步:基本用法存/取数据
/**
基本用法测试
*/
@Test
void testBasicOps() {
// 1.创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder().build();
// 2.存数据
cache.put("gf", "迪丽热巴");
// 3.取数据,不存在则返回null
String gf = cache.getIfPresent("gf");
System.out.println("gf = " + gf);
// 4.取数据,不存在则去数据库查询
String defaultGF = cache.get("defaultGF", key -> {
// 这里可以去数据库根据 key查询value
return "柳岩";
});
System.out.println("defaultGF = " + defaultGF);
}
第三步:缓存驱逐策略
/**
基于大小设置驱逐策略:
*/
@Test
void testEvictByNum() throws InterruptedException {
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
// 设置缓存大小上限为 1
.maximumSize(1)
.build();
// 存数据
cache.put("gf1", "柳岩");
cache.put("gf2", "范冰冰");
cache.put("gf3", "迪丽热巴");
// 延迟10ms,给清理线程一点时间
Thread.sleep(10L);
// 获取数据
System.out.println("gf1: " + cache.getIfPresent("gf1"));
System.out.println("gf2: " + cache.getIfPresent("gf2"));
System.out.println("gf3: " + cache.getIfPresent("gf3"));
}
结果:
gf1: null
gf2: null
gf3: 迪丽热巴
/**
基于时间设置驱逐策略:
*/
@Test
void testEvictByTime() throws InterruptedException {
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 1 秒
.build();
// 存数据
cache.put("gf", "柳岩");
// 获取数据
System.out.println("gf: " + cache.getIfPresent("gf"));
// 休眠一会儿
Thread.sleep(1200L);
System.out.println("gf: " + cache.getIfPresent("gf"));
}
}
结果:
gf: 柳岩
gf: null
3.3 案例
第一步:新建一个Config类
/**
* 初始化本地缓存Caffeine
*/
@Configuration
public class CaffeineConfig {
/**
* item商品的缓存:
* 缓存初始大小100
* 缓存上限10000
*/
@Bean
public Cache<Long, Item> itemCache(){
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10_000)
.build();
}
/**
* stock库存的缓存:
* 缓存初始大小100
* 缓存上限10000
*/
@Bean
public Cache<Long, ItemStock> stockCache(){
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10_000)
.build();
}
}
第二步:编写业务代码
@RestController
@RequestMapping("item")
public class ItemController {
@Autowired
private IItemService itemService;
@Autowired
private IItemStockService stockService;
@Autowired
private Cache<Long, Item> itemCache;
@Autowired
private Cache<Long, ItemStock> stockCache;
@GetMapping("/{id}")
public Item findById(@PathVariable("id") Long id) {
// 优先根据item缓存的id查,没有再去去数据库查
return itemCache.get(id, key -> itemService.query()
.ne("status", 3).eq("id", key)
.one()
);
}
@GetMapping("/stock/{id}")
public ItemStock findStockById(@PathVariable("id") Long id) {
// 优先根据stock缓存的id查,没有再去去数据库查
return stockCache.get(id, key -> stockService.getById(id));
}
}
第三步:启动服务,第一次查询 http://localhost:8081/item/10001 ,控制台会出现查询语句日志,再次查询,并没有查询语句日志,说明数据已经到缓存中了。
四、Nginx编程:Lua语法
4.1 初识Lua
CenOS自带Loa,因此不用安装
可以使用lua命令
直接打开编辑
4.2 变量
上面的local
表示局部变量
字符串拼接是用..
,例如:local str = 'hello ' .. 'world!'
4.3 循环
4.4 条件控制和函数
local arr = {'java','lua'}
local arr1
local function printArr(arr)
if (not arr) then
print('数组不能为空')
return nil
end
for i,val in ipairs(arr)do
print(val)
end
end
printArr(arr)
printArr(arr1)
输出:
[root@iZ2ze1r1nnqykr8zfme6cjZ tmp]# vi hello.lua
[root@iZ2ze1r1nnqykr8zfme6cjZ tmp]# lua hello.lua
java
lua
数组不能为空
五、多级缓存
本章实现红色框部分
5.1 安装OpenResty
5.1.1 安装
首先你的Linux虚拟机必须联网
- 安装开发库
首先要安装OpenResty的依赖开发库,执行命令:
yum install -y pcre-devel openssl-devel gcc --skip-broken
- 安装OpenResty仓库
你可以在你的 CentOS 系统中添加 openresty
仓库,这样就可以便于未来安装或更新我们的软件包(通过 yum check-update
命令)。运行下面的命令就可以添加我们的仓库:
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
如果提示说命令不存在,则运行:
yum install -y yum-utils
然后再重复上面的命令
- 安装OpenResty
然后就可以像下面这样安装软件包,比如 openresty
:
yum install -y openresty
- 安装opm工具
opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。
如果你想安装命令行工具 opm
,那么可以像下面这样安装 openresty-opm
包:
yum install -y openresty-opm
- 目录结构
默认情况下,OpenResty安装的目录是:/usr/local/openresty
看到里面的nginx目录了吗,OpenResty就是在Nginx基础上集成了一些Lua模块。
- 配置nginx的环境变量
打开配置文件:
vi /etc/profile
在最下面加入两行:
export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH
NGINX_HOME:后面是OpenResty安装目录下的nginx的目录
然后让配置生效:
source /etc/profile
5.1.2 启动和运行
OpenResty底层是基于Nginx的,查看OpenResty目录的nginx目录,结构与windows中安装的nginx基本一致:
所以运行方式与nginx基本一致:
# 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop
nginx的默认配置文件注释太多,影响后续我们的编辑,这里将nginx.conf中的注释部分删除,保留有效部分。
修改/usr/local/openresty/nginx/conf/nginx.conf
文件,内容如下:
#user nobody;
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8081;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
在Linux的控制台输入命令以启动nginx:
nginx
然后通过ps -ef | grep nginx
查看:
然后访问页面:http://192.168.150.101:8081,注意ip地址替换为你自己的虚拟机IP:
5.2 OpenResty快速入门
第一步:修改nginx.conf文件
- 加载OpenResty的lua模块:
#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
- 在nginx.conf的server下面,添加对/api/item这个路径的监听
location /api/item{
#响应类型,这里返回json
default_type application/json;
#响应数据由 lua/item.lua这个文件来决定
content_by_lua_file lua/item.lua;
}
nginx.conf
#user nobody;
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
# c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
server {
listen 8081;
server_name localhost;
# 监听反向代理来的请求:/api/item
location /api/item{
# 响应类型,这里返回json
default_type application/json;
# 响应数据由 lua/item.lua这个文件来决定
content_by_lua_file lua/item.lua;
}
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
第二步:编写item.lua的代码
- 在nginx目录下创建一个lua/item.lua文件
[root@iZ2ze1r1nnqykr8zfme6cjZ openresty]# cd /usr/local/openresty/nginx
[root@iZ2ze1r1nnqykr8zfme6cjZ nginx]# mkdir lua
[root@iZ2ze1r1nnqykr8zfme6cjZ nginx]# touch lua/item.lua
- 编写业务内容
-- 返回假数据,这里的ngx.say()函数,就是写数据到Response中
ngx.say('{"id":10001,"name":"SALSA AIR","title":"RIMOWA 2666寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')
- 重新加载配置
nginx -s reload
- 刷新
http://localhost/item.html?id=10001
查看页面数据已经修改,如下:
如果不能成功,检查本机和虚拟机的配置nginx配置文件,然后重启启动nginx,命令为:
start nginx
【windows】或者nginx
【CentOS】
上述的总结流程
5.3 请求参数处理
修改nginx.conf
:注意location ~ /api/item/(\d+)
#user nobody;
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
# c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
server {
listen 8081;
server_name localhost;
# 监听反向代理来的请求:/api/item
location ~ /api/item/(\d+){
# 响应类型,这里返回json
default_type application/json;
# 响应数据由 lua/item.lua这个文件来决定
content_by_lua_file lua/item.lua;
}
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
修改item.lua
解释:id = ngx.var[1]
获取http://localhost/item.html?id=10003
的参数,"id":' .. id ..'
将10003使用..
拼接并返回给页面
-- 获取路径参数
local id = ngx.var[1]
-- 返回结果
ngx.say('{"id":' .. id ..',"name":"SALSA AIR","title":"RIMOWA 2666寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')
5.4 查询Tomcat
本节实现红色部分
5.4.1 nginx发送http请求
我们可以把http查询的请求封装为一个函数,放到0penResty函数库中,方便后期使用
-
在/usr/local/openresty/lualib目录下创建common.lua文件:
vi /usr/local/openresty/lualib/common.lua
-
在common.lua中封装http查询的函数,发起http请求
common.lua
-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)
-- 发送http请求
local resp = ngx.location.capture(path,{
method = ngx.HTTP_GET,
args = params,
})
if not resp then
-- 记录错误信息,返回404
ngx.log(ngx.ERR, "http查询失败,路径为: ", path , ", args: ", args)
ngx.exit(404)
end
return resp.body
end
-- 将方法导出
local _M = {
read_http = read_http
}
return _M
5.4.2 nginx发出请求后,反向代理给tomcat
在nginx.conf的server下增加,记得此IP地址要是你电脑主机的IP地址
# 反向代理给tomcat
location /item {
proxy_pass http://192.168.150.1:8081;
}
一定要注意:如果你的主机IP与服务器IP不属于同一个局域网,那么nginx无法访问你的地址,因为你的地址是内网地址。因此要做cpolar内网穿透,并将上述IP地址改成经内网穿透够的外网地址
5.4.3 编写item.lua业务,获取从本地tomcat响应请求结果
将从tomcat查询到的数据进行拼接,然后序列化返回给前端页面
-- 案例3
-- 导入common函数库【自己编写的】,common.lua在/usr/local/openresty/lualib目录下
local common = require('common')
local read_http = common.read_http
-- 导入cjson解析库,也是在/usr/local/openresty/lualib目录下,默认就有此文件
local cjson = require('cjson')
-- 获取路径参数
local id = ngx.var[1]
-- 查询商品信息
local itemJSON = read_http("/item/" .. id, nil)
-- 查询库存信息
local stockJSON = read_http("/item/stock/" .. id, nil)
-- JSON转化成lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold
-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))
输入http://localhost/item.html?id=10001
,可以看到从本地tomcat查到数据并显示了
5.5 Tomcat集群的负载均衡
修改nginx.conf
添加tomcat集群,并使用hash $request_uri
哈希运算,保证每次查询同一个值到同一个tomcat中访问。
#user nobody;
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
# c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
# 定义tomcat集群
upstream tomcat-cluster {
hash $request_uri;
server 198.168.101.1:8081;
server 198.168.101.1:8082;
}
server {
listen 8081;
server_name localhost;
# 反向代理给tomcat集群,tomcat-cluster在上面定义
location /item {
proxy_pass http://tomcat-cluster;
}
# 监听反向代理来的请求:/api/item
location ~ /api/item/(\d+){
# 响应类型,这里返回json
default_type application/json;
# 响应数据由 lua/item.lua这个文件来决定
content_by_lua_file lua/item.lua;
}
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
5.6 Redis的冷启动与缓存预热
冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。
缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。
- 利用Docker安装Redis
docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes
- 在item-service服务中引入Redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置Redis地址
spring:
redis:
host: 192.168.150.101
- 编写初始化类
@Component
public class RedisHandler implements InitializingBean {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private IItemService itemService;
@Autowired
private IItemStockService stockService;
// jason处理工具
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public void afterPropertiesSet() throws Exception {
// 初始化缓存
// 1.查询商品信息
List<Item> itemList = itemService.list();
// 2.放入缓存
for (Item item : itemList) {
// 2.1.item序列化为JSON
String json = MAPPER.writeValueAsString(item);
// 2.2.存入redis
redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
}
// 3.查询商品库存信息
List<ItemStock> stockList = stockService.list();
// 4.放入缓存
for (ItemStock stock : stockList) {
// 2.1.item序列化为JSON
String json = MAPPER.writeValueAsString(stock);
// 2.2.存入redis
redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
}
}
}
- 查看redis,数据已经放入缓存中
5.7 查询Rdeis缓存【第二级缓存】
本节实现红色部分
OpenResty提供了操作Redis的模块,我们只需要引入该模块即可:在/usr/local/openresty/lualib/common.lua中
- 引入redis模块,并初始化redis对象
- 封装函数,用来释放redis连接,其实是放入连接池
- 封装函数,从redis读数据并返回
演示:关闭本地server服务,因为上面redis缓存预热已经将数据放入到了redis中,因此访问http://localhost/item.html?id=10005
可以查到数据【从redis缓存中查的】。
common.lua
-- 1.引入redis模块,/usr/local/openresty/lualib/resty/redis.lua
local redis = require('resty.redis')
-- 初始化redis对象
local red = redis:new()
-- 设置redis超时时间:建立请求 发送请求 响应请求的超时时间
red:set_timeouts(1000,1000,1000)
-- 2.关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)
local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.log(ngx.ERR, "放入redis连接池失败: ", err)
end
end
-- 3.建立redis连接,读数据
-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, key)
-- 获取一个连接
local ok, err = red:connect(ip, port)
if not ok then
ngx.log(ngx.ERR, "连接redis失败 : ", err)
return nil
end
-- 查询redis
local resp, err = red:get(key)
-- 查询失败处理
if not resp then
ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
end
--得到的数据为空处理
if resp == ngx.null then
resp = nil
ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
end
-- 释放连接,放入连接池
close_redis(red)
return resp
end
-- 4.封装函数,发送http请求,并解析响应
local function read_http(path, params)
local resp = ngx.location.capture(path,{
method = ngx.HTTP_GET,
args = params,
})
if not resp then
-- 记录错误信息,返回404
ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)
ngx.exit(404)
end
return resp.body
end
-- 5.将方法导出
local _M = {
read_http = read_http, -- 记得加逗号
read_redis = read_redis
}
return _M
item.lua
-- 案例4:封装一个read_data,实现先查询redis,未命中再查tomact
-- 1.导入common函数库【自己编写的】,common.lua在/usr/local/openresty/lualib目录下
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 2.导入cjson解析库,也是在/usr/local/openresty/lualib目录下,默认就有此文件
local cjson = require('cjson')
-- 3.封装查询函数
-- reids请求参数,tomcat的http请求路径,参数
function read_data(key, path, params)
-- 查询redis
local resp = read_redis("127.0.0.1", 6379, key)
-- 判断查询结果
if not resp then
ngx.log("redis查询失败,尝试查询http,key:", key)
-- redis查询失败,去查询http
resp = read_http(path, params)
end
return resp
end
-- 获取路径参数
local id = ngx.var[1]
-- 查询商品信息
local itemJSON = read_data("item:id:" .. id, "/item/" .. id, nil)
-- 查询库存信息
local stockJSON = read_data("item:stock:id:" .. id, "/item/stock/" .. id, nil)
-- JSON转化成lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold
-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))
5.8 Nginx本地缓存【第一级缓存】
0penResty为Nginx提供了shard dict
的功能,可以在nginx的多个worker之间共享数据,实现缓存功能。
- 开启共享字典,在nginx.conf的http下添加配置:
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m;
- 操作共享字典:
-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 存储,指定key、value、过期时间,单位s,默认为0表示永不过期
item_cache:set('key','value',1000)
-- 读取
local val = item_cache:get('key')
实战:
在nginx.conf
中加入nginx本地缓存
http {
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m;
}
编写item.lua
-- 案例5:
-- 1)实现先查询nginx本地缓存,未命中再查redis,未命中再查tomact
-- 2)查询redis或tomcat成功后,将数据写入本地缓存,并设置有效期
-- 3)商品的基本信息有效期30分钟,库存信息有效期1分钟
-- 1.导入common函数库【自己编写的】,common.lua在/usr/local/openresty/lualib目录下
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入共享词典,nginx本地缓存
-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 2.导入cjson解析库,也是在/usr/local/openresty/lualib目录下,默认就有此文件
local cjson = require('cjson')
-- 3.封装查询函数
-- reids请求参数,tomcat的http请求路径,参数
function read_data(key, expire, path, params)
-- 1)查询nginx本地缓存
local val = item_cache:get(key)
if not val then
ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询redis,key:", key)
-- 2)查询redis
val = read_redis("127.0.0.1", 6379, key)
-- 判断查询结果
if not val then
ngx.log(ngx.ERR, "redis查询失败,尝试查询http,key:", key)
-- 3)redis查询失败,去查询http
val = read_http(path, params)
end
end
-- 查询成功,把数据写入本地缓存
item_cache:set(key, val, expire)
-- 返回数据
return val
end
-- 获取路径参数
local id = ngx.var[1]
-- 查询商品信息
local itemJSON = read_data("item:id:" .. id, 1800, "/item/" .. id, nil)
-- 查询库存信息
local stockJSON = read_data("item:stock:id:" .. id, 60, "/item/stock/" .. id, nil)
-- JSON转化成lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold
-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))
查看日志:第一次会将redis的数据放到nginx本地缓存,当再次查询时,直接从本地缓存中查询
六、缓存同步策略
当数据库进行修改时,缓存的内容也要进行相应的修改,因此需要完成数据同步。
6.1 常见缓存策略
6.2 安装Canal
下面我们就开启mysql的主从同步机制,让Canal来模拟salve
6.2.1 开启MySQL主从
Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。
这里以之前用Docker运行的mysql为例:
- 开启binlog
打开mysql容器挂载的日志文件,我的在/tmp/mysql/conf
目录:
修改文件:
vi /tmp/mysql/conf/my.cnf
添加内容:
log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima
配置解读:
log-bin=/var/lib/mysql/mysql-bin
:设置binary log文件的存放地址和文件名,叫做mysql-binbinlog-do-db=heima
:指定对哪个database记录binary log events,这里记录heima这个库
最终效果:/tmp/mysql/conf/my.cnf
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima
重启mysql容器,可以看到多了一个mysql-bin.000001
- 设置用户权限
接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对heima这个库的操作权限。
create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
FLUSH PRIVILEGES;
可以看到创建了一个canal用户:
重启mysql容器即可
docker restart mysql
测试设置是否成功:在mysql控制台,或者Navicat中,输入命令:
show master status;
6.2.2 安装Canal
- 创建网络
我们需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:
docker network create heima
让mysql加入这个网络:
docker network connect heima mysql
- 安装Canal
课前资料中提供了canal的镜像压缩包:
大家可以上传到虚拟机,然后通过命令导入:
docker load -i canal.tar
然后运行命令创建Canal容器:
docker run -p 11111:11111 --name canal \
-e canal.destinations=heima \
-e canal.instance.master.address=mysql:3306 \
-e canal.instance.dbUsername=canal \
-e canal.instance.dbPassword=canal \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false \
-e canal.instance.filter.regex=heima\\..* \
--network heima \
-d canal/canal-server:v1.1.5
说明:
-p 11111:11111
:这是canal的默认监听端口-e canal.destinations=heima
:所属集群名称-e canal.instance.master.address=mysql:3306
:数据库地址和端口,因为mysql与canal同属一个网络,因此可以用mysql代替IP地址。如果不知道mysql容器地址,可以通过docker inspect 容器id
来查看-e canal.instance.dbUsername=canal
:数据库用户名-e canal.instance.dbPassword=canal
:数据库密码-e canal.instance.filter.regex=
:要监听的表名称--network heima \
:将canal放入heima这个网络中
表名称监听支持的语法:
mysql 数据解析关注的表,Perl正则表达式.
多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\)
常见例子:
1. 所有表:.* or .*\\..*
2. canal schema下所有表: canal\\..*
3. canal下的以canal打头的表:canal\\.canal.*
4. canal schema下的一张表:canal.test1
5. 多个规则组合使用然后以逗号隔开:canal\\..*,mysql.test1,mysql.test2
通过docker logs -f canal
查看日志,是否启动成功
- Canal与mysql是否建立连接
通过docker exec -it canal bash
进入canal容器内部
通过tail -f canal-server/logs/canal/canal.log
查看canal运行日志
通过tail -f canal-server/logs/heima/heima.log
查看其他运行日志
最后通过exit
退出容器
6.3 监听Canal
Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。不过这里我们会使用GitHub上的第三方开源的canal-starter。地址:https://github.com/NormanGyllenhaal/canal-client
- 引入依赖:
<dependency>
<groupId>top.javatool</groupId>
<artifactId>canal-spring-boot-starter</artifactId>
<version>1.2.1-RELEASE</version>
</dependency>
- 编写配置:
canal:
destination: heima # canal实例名称,要跟虚拟机上设置的destination一致
server: 39.107.236.163:11111 # canal地址
- 编写监听器,监听canal消息
@CanalTable("tb_item")
@Component
public class ItemHandler implements EntryHandler<Item> {
@Autowired
private RedisHandler redisHandler;
@Autowired
private Cache<Long, Item> itemCache;
@Override
public void insert(Item item) {
// 写数据到JVM进程缓存
itemCache.put(item.getId(), item);
// 写数据到redis
redisHandler.saveItem(item);
}
@Override
public void update(Item before, Item after) {
// 写数据到JVM进程缓存
itemCache.put(after.getId(), after);
// 写数据到redis
redisHandler.saveItem(after);
}
@Override
public void delete(Item item) {
// 删除数据到JVM进程缓存
itemCache.invalidate(item.getId());
// 删除数据到redis
redisHandler.deleteItemById(item.getId());
}
}
RedisHandler.java
@Component
public class RedisHandler implements InitializingBean {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private IItemService itemService;
@Autowired
private IItemStockService stockService;
// jason处理工具
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public void afterPropertiesSet() throws Exception {
// 初始化缓存
// 1.查询商品信息
List<Item> itemList = itemService.list();
// 2.放入缓存
for (Item item : itemList) {
// 2.1.item序列化为JSON
String json = MAPPER.writeValueAsString(item);
// 2.2.存入redis
redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
}
// 3.查询商品库存信息
List<ItemStock> stockList = stockService.list();
// 4.放入缓存
for (ItemStock stock : stockList) {
// 2.1.item序列化为JSON
String json = MAPPER.writeValueAsString(stock);
// 2.2.存入redis
redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
}
}
public void saveItem(Item item) {
try {
String json = MAPPER.writeValueAsString(item);
redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public void deleteItemById(Long id) {
redisTemplate.delete("item:id:" + id);
}
}
- Canal推送给canal-client的是被修改的这一行数据(row),而我们引入的canal-client则会帮我们把行数据封装到ltem实体类中。这个过程中需要知道数据库与实体的映射关系,要用到PA的几个注解:
@Data
@TableName("tb_item")
public class Item {
@TableId(type = IdType.AUTO)
@Id
private Long id;//商品id
@Column(name = "name")
private String name;//商品名称
private String title;//商品标题
private Long price;//价格(分)
private String image;//商品图片
private String category;//分类名称
private String brand;//品牌名称
private String spec;//规格
private Integer status;//商品状态 1-正常,2-下架
private Date createTime;//创建时间
private Date updateTime;//更新时间
@TableField(exist = false)
@Transient
private Integer stock;
@TableField(exist = false)
@Transient
private Integer sold;
}
- 测试,修改10001的价格,发现本机控台日志消息变化,并且访问
http://localhost/item.html?id=10001
也发生变化
八、多级缓存总结
九、额外说明:cpolar内网穿透(将私网暴露成公网供外部使用)
第一步:下载并注册账号,cpolar官方:https://www.cpolar.com/
第二步:配置隧道
第二步:查看公网地址