【微服务全家桶】-高级篇-4-多级缓存
0 多级缓存
首先需要导入商品案例
为了演示多级缓存,我们先导入一个商品管理的案例,其中包含商品的CRUD功能。我们将来会给查询商品添加多级缓存。
0.1 安装MySQL
后期做数据同步需要用到MySQL的主从功能,所以需要大家在虚拟机中,利用Docker来运行一个MySQL容器。
0.1.1 准备目录
为了方便后期配置MySQL,我们先准备两个目录,用于挂载容器的数据和配置文件目录:
# 进入/tmp目录
cd /tmp
# 创建文件夹
mkdir mysql
# 进入mysql目录
cd mysql
0.1.2 运行命令
进入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=123 \
--privileged \
-d \
mysql:5.7.25
0.1.3 修改配置
在/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
0.1.4 重启
配置修改后,必须重启容器:
docker restart mysql
0.2 导入SQL
Navicat连接mysql,创建utfmb4格式的heima数据库
接下来,利用Navicat客户端连接MySQL,然后导入提供的sql文件:
其中包含两张表:
- tb_item:商品表,包含商品的基本信息
- tb_item_stock:商品库存表,包含商品的库存信息
之所以将库存分离出来,是因为库存是更新比较频繁的信息,写操作较多。而其他信息修改的频率非常低。
0.3 导入Demo工程
下面导入提供的工程:
项目结构如图所示:
其中的业务包括:
- 分页查询商品
- 新增商品
- 修改商品
- 修改库存
- 删除商品
- 根据id查询商品
- 根据id查询库存
业务全部使用mybatis-plus来实现,如有需要请自行修改业务逻辑。
0.3.1 分页查询商品
在com.heima.item.web
包的ItemController
中可以看到接口定义:
0.3.2 新增商品
在com.heima.item.web
包的ItemController
中可以看到接口定义:
0.3.3 修改商品
在com.heima.item.web
包的ItemController
中可以看到接口定义:
0.3.4 修改库存
在com.heima.item.web
包的ItemController
中可以看到接口定义:
0.3.5 删除商品
在com.heima.item.web
包的ItemController
中可以看到接口定义:
这里是采用了逻辑删除,将商品状态修改为3
0.3.6 根据id查询商品
在com.heima.item.web
包的ItemController
中可以看到接口定义:
这里只返回了商品信息,不包含库存
0.3.7 根据id查询库存
在com.heima.item.web
包的ItemController
中可以看到接口定义:
0.3.8 启动
注意修改application.yml文件中配置的mysql地址信息:
需要修改为自己的虚拟机地址信息、还有账号和密码。
修改后,启动服务,访问:http://localhost:8081/item/10001即可查询数据
0.4 导入商品查询页面
商品查询是购物页面,与商品管理的页面是分离的。
部署方式如图:
我们需要准备一个反向代理的nginx服务器,如上图红框所示,将静态的商品页面放到nginx目录中。
页面需要的数据通过ajax向服务端(nginx业务集群)查询。
0.4.1 运行nginx服务
这里我已经给大家准备好了nginx反向代理服务器和静态资源。
我们找到课前资料的nginx目录:
将其拷贝到一个非中文目录下,运行这个nginx服务。
运行命令:
start nginx.exe
然后访问 http://localhost/item.html?id=10001即可:
0.4.2 反向代理
现在,页面是假数据展示的。我们需要向服务器发送ajax请求,查询商品数据。
打开控制台,可以看到页面有发起ajax查询数据:
而这个请求地址同样是80端口,所以被当前的nginx反向代理了。
查看nginx的conf目录下的nginx.conf文件:
其中的关键配置如下:
其中的192.168.150.101是我的虚拟机IP,也就是我的Nginx业务集群要部署的地方:
完整内容如下:
#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.204.129: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;
}
}
}
1 JVM进程缓存
1.1本地进程缓存-Caffeine 入门demo
@Test
void testBasicOps() {
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder().build();
// 存数据
cache.put("gf", "迪丽热巴");
// 取数据,不存在则返回null
String gf = cache.getIfPresent("gf");
System.out.println("gf = " + gf);
// 取数据,不存在则去数据库查询
String defaultGF = cache.get("defaultGF", key -> {
// 这里可以去数据库根据 key查询value
return "柳岩";
});
System.out.println("defaultGF = " + defaultGF);
}
gf = 迪丽热巴
defaultGF = 柳岩
进程已结束,退出代码为 0
1.2 Caffine三种缓存驱逐策略
1.2.1 基于容量
@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"));
}
先延迟10ms,给清理线程一点时间,驱逐需要时间,所以都能查到
gf1: 柳岩
gf2: 范冰冰
gf3: 迪丽热巴
把//Thread.sleep(10L);
注释解除,则前两次存储被驱逐
gf1: null
gf2: null
gf3: 迪丽热巴
1.2.2 基于时间
/*
基于时间设置驱逐策略:
*/
@Test
void testEvictByTime() throws InterruptedException {
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 10 秒
.build();
// 存数据
cache.put("gf", "柳岩");
// 获取数据
System.out.println("gf: " + cache.getIfPresent("gf"));
// 休眠一会儿
Thread.sleep(1200L);
System.out.println("gf: " + cache.getIfPresent("gf"));
}
gf: 柳岩
gf: null
修改为10s
gf: 柳岩
gf: 柳岩
1.3 实现商品本地进程缓存
1.3.1 声明并注入Bean
因为对两个表进行缓存,所以是两个缓存机制,最好两个都声明成Bean放到Spring容器中去,所以在com.heima.item引入配置类config,里面缓存类CaffeineConfig,有两个Cache类的Bean对象itemCache和stockCache
@Configuration
public class CaffeineConfig {
//商品Cache
@Bean
public Cache<Long, Item> itemCache() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10000)
.build();
}
//库存Cache
@Bean
public Cache<Long, Integer> stockCache() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10000)
.build();
}
}
因为需要在查询时使用缓存,所以在Controller中修改。
首先Autowired注入刚刚的Bean对象
@Autowired
private Cache<Long,Item> itemCache;
@Autowired
private Cache<Long, ItemStock> stockCache;
1.3.2 修改查询业务
修改商品查询业务,使用itemCache
@GetMapping("/{id}")
public Item findById(@PathVariable("id") Long id){
return itemService.query()
.ne("status", 3).eq("id", id)
.one();
}
修改为
@GetMapping("/{id}")
public Item findById(@PathVariable("id") Long id){
return itemCache.get(id, key -> itemService.query()
.ne("status", 3)
.eq("id", key)
.one());
}
修改库存查询业务,使用stockCache
@GetMapping("/stock/{id}")
public ItemStock findStockById(@PathVariable("id") Long id){
return stockService.getById(id);
}
修改为
@GetMapping("/stock/{id}")
public ItemStock findStockById(@PathVariable("id") Long id){
return stockCache.get(id, key -> stockService.getById(key));
}
1.3.3 测试
重启后访问 localhost:8081/item/10001
显示有一条访问数据库,当我们清空idea控制台,再次访问
没有显示访问数据库,说明是访问的缓存
于是我们在进行访问,查询库存 localhost:8081/item/stock/10001
2 Nginx开发语言-Lua
2.1 入门
2.1.1 Helloworld
centos自带lua
print("Hello,world")
shell控制台输入
[root@localhost mysql]# cd /tmp/Lua
[root@localhost Lua]# lua HelloWorld.lua
Hello,world
2.1.2 变量和循环
lua中字符串使用…拼接的
local str1 = 'hello'..'world'
2.1.3 函数和条件控制
案例
arr1={'java','c++','python','lua','c'}
arr2={name1='jack',name2='rose',name3='sjb'}
arr3=nil
function printArr(arr)
if(arr==nil)
then
print('arr==null')
return 0
else
for key, value in pairs(arr) do
print(key,value)
end
end
end
printArr(arr1)
print('================================')
printArr(arr2)
print('================================')
printArr(arr3)
[root@localhost Lua]# lua table-demp.lua
1 java
2 c++
3 python
4 lua
5 c
================================
name1 jack
name3 sjb
name2 rose
================================
arr==null
3 多级缓存
3.1 OpenResty
3.1.1 安装
首先你的Linux虚拟机必须联网
1)安装开发库
首先要安装OpenResty的依赖开发库,执行命令:
yum install -y pcre-devel openssl-devel gcc --skip-broken
2)安装OpenResty仓库
你可以在你的 CentOS 系统中添加 openresty
仓库,这样就可以便于未来安装或更新我们的软件包(通过 yum check-update
命令)。运行下面的命令就可以添加我们的仓库:
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
如果提示说命令不存在,则运行:
yum install -y yum-utils
然后再重复上面的命令
3)安装OpenResty
然后就可以像下面这样安装软件包,比如 openresty
:
yum install -y openresty
4)安装opm工具
opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。
如果你想安装命令行工具 opm
,那么可以像下面这样安装 openresty-opm
包:
yum install -y openresty-opm
5)目录结构
默认情况下,OpenResty安装的目录是:/usr/local/openresty
看到里面的nginx目录了吗,OpenResty就是在Nginx基础上集成了一些Lua模块。
6)配置nginx的环境变量
打开配置文件:
vi /etc/profile
在最下面加入两行:
export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH
NGINX_HOME:后面是OpenResty安装目录下的nginx的目录
然后让配置生效:
source /etc/profile
3.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
然后访问页面:http://192.168.204.129:8081,注意ip地址替换为你自己的虚拟机IP:
3.1.3 备注
加载OpenResty的lua模块:
#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
common.lua
-- 封装函数,发送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
-- 将方法导出
local _M = {
read_http = read_http
}
return _M
释放Redis连接API:
-- 关闭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
读取Redis数据的API:
-- 查询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
开启共享词典:
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m;
3.2 OpenResty快速入门
案例
3.2.1 修改conf文件
修改openresty的nginx.conf下http,加载Lua模块并并且监听/api/item
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;
location /api/item {
# 默认相应类型
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;
}
}
}
因为content_by_lua_file lua/item.lua;
决定返回的数据是在nginx下lua目录下的item.lua文件,
这是我们要编写的,没有就要创建!
3.2.2 编写item.lua文件
ngx.say('{"id":10001,"name":"SALSA AIR","title":"RIMOWA 29寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":19900,"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
nginx -s reload
清除win上的nginx,再重启,重新访问http://localhost/item.html?id=10001
与之前静态页面已经不同
{"id":10001,"name":"SALSA AIR","title":"RIMOWA 21寸托运箱拉杆箱 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}
3.3 请求参数处理
案例
3.3.1 路径占位符
修改nginx.conf
location ~ /api/item/(\d+) {
# 默认相应类型
default_type application/json;
# 响应结果由lua/item.lua文件来决定
content_by_lua_file lua/item.lua;
}
修改item.lua
-- 获取路径参数
local id=ngx.var[1]
-- 拼接结果并返回
ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 29寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":19900,"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
nginx -s reload
访问http://localhost/item.html?id=10003,实际转发的时候是http://localhost/api/item/10003
3.4 查询Tomcat
应该先查缓存再查Tomcat,这里我们演示先查Tomcat
案例
3.4.1 封装http查询的函数
添加location
location /item {
proxy_pass http://192.168.204.1:8081;
}
location ~ /api/item/(\d+) {
# 默认相应类型
default_type application/json;
# 响应结果由lua/item.lua文件来决定
content_by_lua_file lua/item.lua;
}
封装http查询的函数
在/usr/local/openresty/lualib下创建common.lua文件
-- 封装函数,发送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查询失败, path: ", path , ", args: ", args)
ngx.exit(404)
end
return resp.body
end
-- 将方法导出
local _M = {
read_http = read_http
}
return _M
nginx发送的location实际是被自己捕获了,所以要转发proxy_pass到http://192.168.204.1:8081的Tomcat中,再通过read_http来访问http://192.168.204.1:8081的Tomcat返回的数据;
修改item.lua,引入common.lua
先返回一部分看一下
--导入common函数库
local common = require('common')
local read_http = common.read_http
-- 获取路径参数
local id=ngx.var[1]
-- 拼接结果并返回
--ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 29寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":19900,"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}')
-- 查询商品信息
local itemJSON=read_http("/item/"..id,nil)
-- 查询库存信息
local stockJSON=read_http("/item/stock/"..id,nil)
--拼接结果并返回
ngx.say(itemJSON)
重新加载nginx
nginx -s reload
成功访问
3.4.2 组装json数据
要把两个JSON拼装在一起,要转换为Lua对象-Table,则需要cjson模块
-- 导入common函数库
local common = require('common')
local read_http = common.read_http
-- 导入cjson函数库
local cjson=require('cjson')
-- 获取路径参数
local id=ngx.var[1]
-- 拼接结果并返回
--ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 29寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":19900,"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}')
-- 查询商品信息
local itemJSON=read_http("/item/"..id,nil)
-- 查询库存信息
local stockJSON=read_http("/item/stock/"..id,nil)
-- 要转换为Lua对象-Table
local item=cjson.decode(itemJSON)
local stock=cjson.decode(stockJSON)
-- 组合数据
item.stock=stock.stock
item.sold=stock.sold
-- 转为json结果并返回
ngx.say(cjson.encode(item))
重启nginx,nginx -s reload
,成功查到库存
3.5 负载均衡
修改openresty中nginx.conf,用hash $request_uri;
来做哈希,访问tomcat
upstream tomcat-cluster {
hash $request_uri;
server http://192.168.204.1:8081;
server http://192.168.204.1:8082;
}
server {
listen 8081;
server_name localhost;
location /item {
proxy_pass http://tomcat-cluster;
}
启动8082的tomcat
然后访问http://localhost/item.html?id=10001,是8082服务的
访问http://localhost/item.html?id=10002,是8081服务的
3.6 添加Redis缓存
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置地址
redis:
host: 192.168.204.129
在config包中添加RedisHandler类,用ObjectMapper来进行序列化
@Component
public class RedisHandler implements InitializingBean {
@Autowired
private StringRedisTemplatee redisTemplate;
@Autowired
private IItemService itemService;
@Autowired
private IItemStockService itemStockService;
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public void afterPropertiesSet() throws Exception {
//初始化缓存
//1.查询所有的商品
List<Item> itemList = itemService.list();
//1.1 将商品信息存入redis
for (Item item : itemList) {
//1.2 将商品信息序列化
String itemJson = MAPPER.writeValueAsString(item);
//1.3 将商品信息存入redis
redisTemplate.opsForValue().set("item:id" + item.getId(), itemJson);
}
//2.查询所有的库存
List<ItemStock> itemStockList = itemStockService.list();
//2.1 将库存信息存入redis
for (ItemStock itemStock : itemStockList) {
//2.2 将库存信息序列化
String itemStockJson = MAPPER.writeValueAsString(itemStock);
//2.3 将库存信息存入redis
redisTemplate.opsForValue().set("item:stock:id" + itemStock.getId(), itemStockJson);
}
}
}
重新启动发现,Spring初始化时有两次查询
查看RedisManager
如果RedisManager乱码则需要配置RedisTemplate的序列化器,在config包下创建RedisConfig类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
//配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
3.7 OpenResty操作Redis
先修改common.lua,引入读取redis的方法
-- 导入redis
local redis=require('resty.redis')
-- 初始化Redis
local red = redis:new();
red:set_timeouts(1000,1000,1000)
-- 关闭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
-- 查询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
-- 封装函数,发送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查询失败, path: ", path , ", args: ", args)
ngx.exit(404)
end
return resp.body
end
-- 将方法导出
local _M = {
read_http = read_http,
read_redis=read_redis
}
return _M
修改item.lua,封装查询方法
-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis=common.read_redis
-- 封装查询函数
function read_data(key,path,params)
-- 优先查redis
local resp = read_redis("127.0.0.1",6379,key)
-- 判断查询结果
if not resp then
ngx.log(ngx.ERR,"redis查询失败查询tomcat,key:",key)
-- redis查询失败查询tomcat
resp=read_http(path,params)
else
ngx.log(ngx.ERR, "redis查询成功", ", key: ", key)
end
return resp
end
-- 导入cjson函数库
local cjson=require('cjson')
-- 获取路径参数
local id=ngx.var[1]
-- 拼接结果并返回
--ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 29寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":19900,"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}')
-- 查询商品信息
local itemJSON=read_data("item:id:"..id,"/item/"..id,nil)
-- 查询库存信息
local stockJSON=read_data("item:stock:id:"..id,"/item/stock/"..id,nil)
-- 要转换为Lua对象-Table
local item=cjson.decode(itemJSON)
local stock=cjson.decode(stockJSON)
-- 组合数据
item.stock=stock.stock
item.sold=stock.sold
-- 转为json结果并返回
ngx.say(cjson.encode(item))
在Spring加载时将数据库数据放入Redis中,即使关闭数据库,nginx也能从redis中拿到数据
3.8 为OpenResty添加Redis本地缓存
在OpenResty的nginx.conf中加入共享词典(本地缓存)
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;;";
# 添加共享词典
lua_shared_dict item_cache 150m;
就可以在/usr/local/openresty/nginx/lua下item.lua中导入共享词典
-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis=common.read_redis
-- 导入cjson函数库
local cjson=require('cjson')
-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache
案例
3.8.1 修改item.lua逻辑
expire为有效期
-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis=common.read_redis
-- 导入cjson函数库
local cjson=require('cjson')
-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache
-- 封装查询函数
function read_data(key,path,params,expire)
-- 1.先查询openresty的本地缓存
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查询失败查询tomcat,key:",key)
-- 3.redis查询失败查询http
val=read_http(path,params)
end
end
-- 4.查询成功 把数据写入本地缓存
item_cache:set(key,val,expire)
-- 5.返回数据
return val
end
-- 获取路径参数
local id=ngx.var[1]
-- 拼接结果并返回
--ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 29寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":19900,"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}')
-- 查询商品信息
local itemJSON=read_data("item:id:"..id,"/item/"..id,nil,1800)
-- 查询库存信息
local stockJSON=read_data("item:stock:id:"..id,"/item/stock/"..id,nil,60)
-- 要转换为Lua对象-Table
local item=cjson.decode(itemJSON)
local stock=cjson.decode(stockJSON)
-- 组合数据
item.stock=stock.stock
item.sold=stock.sold
-- 转为json结果并返回
ngx.say(cjson.encode(item))
重新加载nginx,nginx -s reload
查看日志
tail -f error.log
完成本地缓存
4 缓存同步
4.1 缓存同步策略
4.2 Canal
4.2.1 安装和配置Canal
下面我们就开启mysql的主从同步机制,让Canal来模拟salve
4.2.1.1 开启MySQL主从
Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。
这里以之前用Docker运行的mysql为例:
1)开启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这个库
最终效果:
[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
2)设置用户权限
接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对heima这个库的操作权限。
create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
FLUSH PRIVILEGES;
重启mysql容器即可
docker restart mysql
测试设置是否成功:在mysql控制台,或者Navicat中,输入命令:
show master status;
4.2.1.2 安装Canal
1)创建网络
我们需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:
docker network create heima
让mysql加入这个网络:
docker network connect heima mysql
2)安装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.instance.master.address=mysql:3306
:数据库地址和端口,如果不知道mysql容器地址,可以通过docker inspect 容器id
来查看-e canal.instance.dbUsername=canal
:数据库用户名-e canal.instance.dbPassword=canal
:数据库密码-e canal.instance.filter.regex=
:要监听的表名称
表名称监听支持的语法:
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
进入容器内部
docker exec -it canal bash
查看canal日志
tail -f canal-server/logs/canal/canal.log
2024-03-14 14:11:52.993 [main] INFO com.alibaba.otter.canal.deployer.CanalLauncher - ## set default uncaught exception handler
2024-03-14 14:11:53.040 [main] INFO com.alibaba.otter.canal.deployer.CanalLauncher - ## load canal configurations
2024-03-14 14:11:53.053 [main] INFO com.alibaba.otter.canal.deployer.CanalStarter - ## start the canal server.
2024-03-14 14:11:53.111 [main] INFO com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[172.22.0.3(172.22.0.3):11111]
2024-03-14 14:11:54.773 [main] INFO com.alibaba.otter.canal.deployer.CanalStarter - ## the canal server is running now ......
再查看heima的log
tail -f canal-server/logs/heima/heima.log
退出docker容器
4.3 使用Canal
1)引入依赖
<dependency>
<groupId>top.javatool</groupId>
<artifactId>canal-spring-boot-starter</artifactId>
<version>1.2.1-RELEASE</version>
</dependency>
2)配置canal
canal:
destination: heima
server: 192.168.204.129:11111
3)编写Canal客户端
进行映射
@Data
@TableName("tb_item")
public class Item {
@TableId(type = IdType.AUTO)
@Id
private Long id;//商品id
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;
}
编写RedisHandler的逻辑
public void saveItem(Item item) {
try {
String itemJson = MAPPER.writeValueAsString(item);
redisTemplate.opsForValue().set("item:id:" + item.getId(), itemJson);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public void deleteItemById(Long id) {
redisTemplate.delete("item:id:" + id);
}
编写itemHandler
@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) {
//1.将新增的商品信息存入本地JVM缓存
itemCache.put(item.getId(),item);
//2.将新增的商品信息存入redis
redisHandler.saveItem(item);
}
@Override
public void update(Item before, Item after) {
//1.将新增的商品信息存入本地JVM缓存
itemCache.put(after.getId(),after);
//2.将新增的商品信息存入redis
redisHandler.saveItem(after);
}
@Override
public void delete(Item item) {
//1.删除本地JVM缓存
itemCache.invalidate(item.getId());
//2.删除redis
redisHandler.deleteItemById(item.getId());
}
}
重启会修改数据库的同时同步修改jvm和redis的缓存