产品营销网站建设,商标注册网app,网页版微信二维码传送助手,专业的扬州网站建设文章目录 一、多级缓存概念二、准备工作【导入案例#xff0c;并搭建Nginx反向代理】2.1 导入商品案例2.1.1 安装MySQL2.1.2 导入SQL2.1.3 导入Demo工程2.1.4 启动2.1.5 导入商品查询页面 三、JVM进程缓存【第三级缓存】3.1 本地进程缓存与分布式缓存的区别3.2 本地进程缓存并搭建Nginx反向代理】2.1 导入商品案例2.1.1 安装MySQL2.1.2 导入SQL2.1.3 导入Demo工程2.1.4 启动2.1.5 导入商品查询页面 三、JVM进程缓存【第三级缓存】3.1 本地进程缓存与分布式缓存的区别3.2 本地进程缓存Caffeine3.3 案例 四、Nginx编程Lua语法4.1 初识Lua4.2 变量4.3 循环4.4 条件控制和函数 五、多级缓存5.1 安装OpenResty5.1.1 安装5.1.2 启动和运行 5.2 OpenResty快速入门上述的总结流程 5.3 请求参数处理5.4 查询Tomcat5.4.1 nginx发送http请求5.4.2 nginx发出请求后反向代理给tomcat5.4.3 编写item.lua业务获取从本地tomcat响应请求结果 5.5 Tomcat集群的负载均衡5.6 Redis的冷启动与缓存预热5.7 查询Rdeis缓存【第二级缓存】5.8 Nginx本地缓存【第一级缓存】 六、缓存同步策略6.1 常见缓存策略6.2 安装Canal6.2.1 开启MySQL主从6.2.2 安装Canal 6.3 监听Canal 八、多级缓存总结九、额外说明cpolar内网穿透将私网暴露成公网供外部使用 一、多级缓存概念 多级缓存就是充分利用请求处理的每个环节分别添加缓存减轻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_PASSWORD123456 \--privileged \-d \mysql:5.7.25在/tmp/mysql/conf目录添加一个my.cnf文件作为mysql的配置文件
# 创建文件
touch /tmp/mysql/conf/my.cnf文件的内容如下
[mysqld]
skip-name-resolve
character_set_serverutf8
datadir/var/lib/mysql
server-id1000配置修改后必须重启容器
docker restart mysql2.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?id10001即可 反向代理
现在页面是假数据展示的。我们需要向服务器发送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
第一步引入依赖 dependencygroupIdcom.github.ben-manes.caffeine/groupIdartifactIdcaffeine/artifactId/dependency第二步基本用法存/取数据 /**基本用法测试*/Testvoid testBasicOps() {// 1.创建缓存对象CacheString, String cache Caffeine.newBuilder().build();// 2.存数据cache.put(gf, 迪丽热巴);// 3.取数据不存在则返回nullString gf cache.getIfPresent(gf);System.out.println(gf gf);// 4.取数据不存在则去数据库查询String defaultGF cache.get(defaultGF, key - {// 这里可以去数据库根据 key查询valuereturn 柳岩;});System.out.println(defaultGF defaultGF);}第三步缓存驱逐策略 /**基于大小设置驱逐策略*/Testvoid testEvictByNum() throws InterruptedException {// 创建缓存对象CacheString, 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: 迪丽热巴/**基于时间设置驱逐策略*/Testvoid testEvictByTime() throws InterruptedException {// 创建缓存对象CacheString, 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: null3.3 案例 第一步新建一个Config类
/*** 初始化本地缓存Caffeine*/
Configuration
public class CaffeineConfig {/*** item商品的缓存* 缓存初始大小100* 缓存上限10000*/Beanpublic CacheLong, Item itemCache(){return Caffeine.newBuilder().initialCapacity(100).maximumSize(10_000).build();}/*** stock库存的缓存* 缓存初始大小100* 缓存上限10000*/Beanpublic CacheLong, ItemStock stockCache(){return Caffeine.newBuilder().initialCapacity(100).maximumSize(10_000).build();}
}第二步编写业务代码
RestController
RequestMapping(item)
public class ItemController {Autowiredprivate IItemService itemService;Autowiredprivate IItemStockService stockService;Autowiredprivate CacheLong, Item itemCache;Autowiredprivate CacheLong, 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 arr1local function printArr(arr)if (not arr) thenprint(数组不能为空)return nilendfor i,val in ipairs(arr)doprint(val)end
endprintArr(arr)
printArr(arr1)输出
[rootiZ2ze1r1nnqykr8zfme6cjZ tmp]# vi hello.lua
[rootiZ2ze1r1nnqykr8zfme6cjZ 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:$PATHNGINX_HOME后面是OpenResty安装目录下的nginx的目录
然后让配置生效
source /etc/profile5.1.2 启动和运行
OpenResty底层是基于Nginx的查看OpenResty目录的nginx目录结构与windows中安装的nginx基本一致
所以运行方式与nginx基本一致
# 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stopnginx的默认配置文件注释太多影响后续我们的编辑这里将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{#响应类型这里返回jsondefault_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/itemlocation /api/item{# 响应类型这里返回jsondefault_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文件
[rootiZ2ze1r1nnqykr8zfme6cjZ openresty]# cd /usr/local/openresty/nginx
[rootiZ2ze1r1nnqykr8zfme6cjZ nginx]# mkdir lua
[rootiZ2ze1r1nnqykr8zfme6cjZ 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.00000:00,updateTime:2019-04-30T16:00:00.00000:00,stock:2999,sold:31290})重新加载配置
nginx -s reload刷新http://localhost/item.html?id10001查看页面数据已经修改如下 如果不能成功检查本机和虚拟机的配置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/itemlocation ~ /api/item/(\d){# 响应类型这里返回jsondefault_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?id10003的参数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.00000:00,updateTime:2019-04-30T16:00:00.00000: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-- 记录错误信息返回404ngx.log(ngx.ERR, http查询失败路径为 , path , , args: , args)ngx.exit(404)endreturn resp.body
end
-- 将方法导出
local _M { read_http read_http
}
return _M5.4.2 nginx发出请求后反向代理给tomcat
在nginx.conf的server下增加记得此IP地址要是你电脑主机的IP地址 # 反向代理给tomcatlocation /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?id10001可以看到从本地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/itemlocation ~ /api/item/(\d){# 响应类型这里返回jsondefault_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依赖 dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency配置Redis地址
spring:redis:host: 192.168.150.101编写初始化类
Component
public class RedisHandler implements InitializingBean {Autowiredprivate StringRedisTemplate redisTemplate;Autowiredprivate IItemService itemService;Autowiredprivate IItemStockService stockService;// jason处理工具private static final ObjectMapper MAPPER new ObjectMapper();Overridepublic void afterPropertiesSet() throws Exception {// 初始化缓存// 1.查询商品信息ListItem itemList itemService.list();// 2.放入缓存for (Item item : itemList) {// 2.1.item序列化为JSONString json MAPPER.writeValueAsString(item);// 2.2.存入redisredisTemplate.opsForValue().set(item:id: item.getId(), json);}// 3.查询商品库存信息ListItemStock stockList stockService.list();// 4.放入缓存for (ItemStock stock : stockList) {// 2.1.item序列化为JSONString json MAPPER.writeValueAsString(stock);// 2.2.存入redisredisTemplate.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?id10005可以查到数据【从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 thenngx.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 thenngx.log(ngx.ERR, 连接redis失败 : , err)return nilend-- 查询redislocal resp, err red:get(key)-- 查询失败处理if not resp thenngx.log(ngx.ERR, 查询Redis失败: , err, , key , key)end--得到的数据为空处理if resp ngx.null thenresp nilngx.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-- 记录错误信息返回404ngx.log(ngx.ERR, http not found, path: , path , , args: , args)ngx.exit(404)endreturn resp.body
end
-- 5.将方法导出
local _M { read_http read_http, -- 记得加逗号read_redis read_redis
}
return _Mitem.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)-- 查询redislocal resp read_redis(127.0.0.1, 6379, key)-- 判断查询结果if not resp thenngx.log(redis查询失败尝试查询httpkey, key)-- redis查询失败去查询httpresp read_http(path, params)endreturn 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大小150mlua_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 thenngx.log(ngx.ERR, 本地缓存查询失败尝试查询rediskey, key)-- 2查询redisval read_redis(127.0.0.1, 6379, key)-- 判断查询结果if not val thenngx.log(ngx.ERR, redis查询失败尝试查询httpkey, key)-- 3redis查询失败去查询httpval read_http(path, params)endend-- 查询成功把数据写入本地缓存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-dbheima配置解读
log-bin/var/lib/mysql/mysql-bin设置binary log文件的存放地址和文件名叫做mysql-binbinlog-do-dbheima指定对哪个database记录binary log events这里记录heima这个库
最终效果/tmp/mysql/conf/my.cnf
[mysqld]
skip-name-resolve
character_set_serverutf8
datadir/var/lib/mysql
server-id1000
log-bin/var/lib/mysql/mysql-bin
binlog-do-dbheima重启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.destinationsheima \
-e canal.instance.master.addressmysql:3306 \
-e canal.instance.dbUsernamecanal \
-e canal.instance.dbPasswordcanal \
-e canal.instance.connectionCharsetUTF-8 \
-e canal.instance.tsdb.enabletrue \
-e canal.instance.gtidonfalse \
-e canal.instance.filter.regexheima\\..* \
--network heima \
-d canal/canal-server:v1.1.5说明:
-p 11111:11111这是canal的默认监听端口-e canal.destinationsheima所属集群名称-e canal.instance.master.addressmysql:3306数据库地址和端口因为mysql与canal同属一个网络因此可以用mysql代替IP地址。如果不知道mysql容器地址可以通过docker inspect 容器id来查看-e canal.instance.dbUsernamecanal数据库用户名-e canal.instance.dbPasswordcanal 数据库密码-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
引入依赖
dependencygroupIdtop.javatool/groupIdartifactIdcanal-spring-boot-starter/artifactIdversion1.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 EntryHandlerItem {Autowiredprivate RedisHandler redisHandler;Autowiredprivate CacheLong, Item itemCache;Overridepublic void insert(Item item) {// 写数据到JVM进程缓存itemCache.put(item.getId(), item);// 写数据到redisredisHandler.saveItem(item);}Overridepublic void update(Item before, Item after) {// 写数据到JVM进程缓存itemCache.put(after.getId(), after);// 写数据到redisredisHandler.saveItem(after);}Overridepublic void delete(Item item) {// 删除数据到JVM进程缓存itemCache.invalidate(item.getId());// 删除数据到redisredisHandler.deleteItemById(item.getId());}
}RedisHandler.java
Component
public class RedisHandler implements InitializingBean {Autowiredprivate StringRedisTemplate redisTemplate;Autowiredprivate IItemService itemService;Autowiredprivate IItemStockService stockService;// jason处理工具private static final ObjectMapper MAPPER new ObjectMapper();Overridepublic void afterPropertiesSet() throws Exception {// 初始化缓存// 1.查询商品信息ListItem itemList itemService.list();// 2.放入缓存for (Item item : itemList) {// 2.1.item序列化为JSONString json MAPPER.writeValueAsString(item);// 2.2.存入redisredisTemplate.opsForValue().set(item:id: item.getId(), json);}// 3.查询商品库存信息ListItemStock stockList stockService.list();// 4.放入缓存for (ItemStock stock : stockList) {// 2.1.item序列化为JSONString json MAPPER.writeValueAsString(stock);// 2.2.存入redisredisTemplate.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)Idprivate Long id;//商品idColumn(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)Transientprivate Integer stock;TableField(exist false)Transientprivate Integer sold;
}测试修改10001的价格发现本机控台日志消息变化并且访问http://localhost/item.html?id10001也发生变化 八、多级缓存总结 九、额外说明cpolar内网穿透将私网暴露成公网供外部使用
第一步下载并注册账号cpolar官方https://www.cpolar.com/ 第二步配置隧道 第二步查看公网地址