54.多级缓存

目录

一、传统缓存的问题、多级缓存方案。

二、JVM进程缓存。

1)进程缓存和缓存。

2)导入商品案例。

1.安装MySQL

2.导入SQL

3.导入Demo工程

4.导入商品查询页面

3)初识Caffeine(就是在springboot学过的注解方式的cache)。

4)实现进程缓存。

三、Lua语法入门。

1)初识Lua。

2)数据类型、变量和循环。

3)函数、条件控制。

四、多级缓存。

1)安装OpenResty。

1.安装

2.启动和运行

3.备注

2)OpenResty快速入门。

3)请求参数处理。

4)查询Tomcat。

5)Tomcat集群的负载均衡。

6)Redis缓存预热。

7)查询Redis缓存。

8)Nginx本地缓存。

五、缓存同步策略。

1)数据同步策略。

2)安装Canal。

2.1)初识Canal。 

2.2)安装和配置Canal。

1.开启MySQL主从

2.安装Canal

3)监听Canal。

六、多级缓存总结。


一、传统缓存的问题、多级缓存方案。

二、JVM进程缓存。

1)进程缓存和缓存。

在Java中,进程缓存和缓存也是两个不同的概念。

  1. 进程缓存:在Java中,进程缓存通常指JVM的堆内存,它是Java虚拟机为每个Java进程分配的内存空间。Java进程可以使用堆内存来存储对象、数组等数据结构,以及执行方法时所需的局部变量、方法参数等。Java程序可以通过调整JVM的参数来控制堆内存的大小,从而影响程序的性能和内存占用。

  2. 缓存:在Java中,缓存通常指应用程序中的缓存机制,用于临时存储经常访问的数据,以提高数据访问速度。Java应用程序可以使用各种缓存框架来实现缓存机制,例如Ehcache、Guava Cache、Redis等。这些框架通常提供了一些API来支持数据的读取、写入、删除等操作,并可以通过配置文件或代码来指定缓存的容量、过期时间、失效策略等参数。

总的来说,Java中的进程缓存和缓存都是为了提高程序的性能和响应速度而存在的,但它们的作用和实现方式有所不同。进程缓存是JVM为每个Java进程分配的内存空间,用于存储Java对象和方法执行时所需的数据;而缓存是应用程序中的一种机制,用于缓存经常访问的数据,以减少对数据库或其他数据源的访问次数,提高程序的性能。

需要注意的是,Caffeine是一个进程级别的缓存,它只在单个Java进程内生效。

2)导入商品案例。

为了演示多级缓存,我们先导入一个商品管理的案例,其中包含商品的CRUD功能。我们将来会给查询商品添加多级缓存。

1.安装MySQL

后期做数据同步需要用到MySQL的主从功能,所以需要大家在虚拟机中,利用Docker来运行一个MySQL容器。

1.1.准备目录

为了方便后期配置MySQL,我们先准备两个目录,用于挂载容器的数据和配置文件目录:

# 进入/tmp目录
cd /tmp
# 创建文件夹
mkdir mysql
# 进入mysql目录
cd mysql

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

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

1.4.重启

配置修改后,必须重启容器:

docker restart mysql

2.导入SQL

接下来,利用Navicat客户端连接MySQL,然后导入课前资料提供的sql文件:

其中包含两张表:

  • tb_item:商品表,包含商品的基本信息

  • tb_item_stock:商品库存表,包含商品的库存信息

之所以将库存分离出来,是因为库存是更新比较频繁的信息,写操作较多。而其他信息修改的频率非常低。

3.导入Demo工程

下面导入课前资料提供的工程:

项目结构如图所示:

其中的业务包括:

  • 分页查询商品

  • 新增商品

  • 修改商品

  • 修改库存

  • 删除商品

  • 根据id查询商品

  • 根据id查询库存

业务全部使用mybatis-plus来实现,如有需要请自行修改业务逻辑。

3.1.分页查询商品

com.heima.item.web包的ItemController中可以看到接口定义:

3.2.新增商品

com.heima.item.web包的ItemController中可以看到接口定义:

3.3.修改商品

com.heima.item.web包的ItemController中可以看到接口定义:

3.4.修改库存

com.heima.item.web包的ItemController中可以看到接口定义:

3.5.删除商品

com.heima.item.web包的ItemController中可以看到接口定义:

这里是采用了逻辑删除,将商品状态修改为3

3.6.根据id查询商品

com.heima.item.web包的ItemController中可以看到接口定义:

这里只返回了商品信息,不包含库存

3.7.根据id查询库存

com.heima.item.web包的ItemController中可以看到接口定义:

3.8.启动

注意修改application.yml文件中配置的mysql地址信息:

需要修改为自己的虚拟机地址信息、还有账号和密码。

修改后,启动服务,访问:http://localhost:8081/item/10001即可查询数据

4.导入商品查询页面

商品查询是购物页面,与商品管理的页面是分离的。

部署方式如图:

我们需要准备一个反向代理的nginx服务器,如上图红框所示,将静态的商品页面放到nginx目录中。

页面需要的数据通过ajax向服务端(nginx业务集群)查询。

4.1.运行nginx服务

这里我已经给大家准备好了nginx反向代理服务器和静态资源。

我们找到课前资料的nginx目录:

将其拷贝到一个非中文目录下,运行这个nginx服务。

运行命令:

start nginx.exe

然后访问 http://localhost/item.html?id=10001即可:

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.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;}}
}

3)初识Caffeine(就是在springboot学过的注解方式的cache)。

这里是使用代码方式写的。 

使用案例:

public class CaffeineTest {/*基本用法测试*/@Testvoid testBasicOps() throws UnsupportedEncodingException {// 创建缓存对象Cache<String, String> cache = Caffeine.newBuilder().build();// 存数据
//        cache.put("gf", "aaa");// 取数据,不存在则返回nullString gf = cache.getIfPresent("gf");System.out.println("gf = " + gf);// 取数据,不存在则去数据库查询String defaultGF = cache.get("defaultGF", key -> {// 这里可以去数据库根据 key查询valuereturn "lll";});System.out.println("defaultGF = " + defaultGF);/*** 输出结果为:* gf = null* defaultGF = lll*/}/*基于大小设置驱逐策略:*/@Testvoid 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);//打印三个都有数据,因为还没来得及清理(偶尔也是清理掉的,即前两个为null)。打开这个后,前两个为null,最后一个有数据// 获取数据System.out.println("gf1: " + cache.getIfPresent("gf1"));//gf1: nullSystem.out.println("gf2: " + cache.getIfPresent("gf2"));//gf2: nullSystem.out.println("gf3: " + cache.getIfPresent("gf3"));//gf3: 迪丽热巴}/*基于时间设置驱逐策略:*/@Testvoid 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"));//gf: 柳岩// 休眠一会儿Thread.sleep(1200L);System.out.println("gf: " + cache.getIfPresent("gf"));//gf: null}
}

4)实现进程缓存。

加载Cache成为Bean:

@Configuration
public class CaffeineConfig {@Beanpublic Cache<Long, Item> itemCache(){return Caffeine.newBuilder().initialCapacity(100)//初始化100个key容量.maximumSize(10_000)//上限是10000个key容量.build();}@Beanpublic Cache<Long, ItemStock> itemStockCache(){return Caffeine.newBuilder().initialCapacity(100)//初始化100个key容量.maximumSize(10_000)//上限是10000个key容量.build();}
}

使用Caffeine缓存:

@RestController
@RequestMapping("item")
public class ItemController {@Autowiredprivate IItemService itemService;@Autowiredprivate IItemStockService stockService;@Autowiredprivate Cache<Long,Item> itemCache;@Autowiredprivate Cache<Long,ItemStock> stockCache;......省略@GetMapping("/{id}")public Item findById(@PathVariable("id") Long id){//itemCache.get()方法的第二个参数是一个lambda表达式,它接受一个类型为Long的键(即id),然后返回一个类型为Item的值。return itemCache.get(id,key -> itemService.query().ne("status", 3).eq("id", key).one());}@GetMapping("/stock/{id}")public ItemStock findStockById(@PathVariable("id") Long id){return stockCache.get(id,key -> stockService.getById(id));}
}

我们这里实现的就是Tomcat+java里面的进程缓存:

三、Lua语法入门。

1)初识Lua。

CentOS中自带Lua环境。

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。官网:https://www.lua.org/

简单写一个lua脚本:

2)数据类型、变量和循环。

lua中字符串拼接是使用..连接的,如local str = 'hello' .. 'world'  #打印出来是helloworld

3)函数、条件控制。

四、多级缓存。

1)安装OpenResty。

官方网站: https://openresty.org/cn/

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

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.150.101:8081,注意ip地址替换为你自己的虚拟机IP:

3.备注

下面的这些是OpenResty快速入门时要使用的东西。

加载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-- 记录错误信息,返回404ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)ngx.exit(404)endreturn 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 thenngx.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 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)endclose_redis(red)return resp
end

开启共享词典:

# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m; 

2)OpenResty快速入门。

1.该展示的是windows下的nginx的反向代理服务器的nginx.conf文件。

2.该展示的是linux下的openResty里的nginx的nginx.conf文件。

这个是添加到openResty中的nginx的nginx.conf里面的html标签中。

lua_package_path "/usr/local/openresty/lualib/?.lua;;"; 表示在lualib目录下以lua后缀名的模块文件都加载进来。

lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  表示在lualib目录下以so后缀名的模块文件都加载进来。

3.编写item.lua文件。

注意:写好文件后,linux的openResty中的nginx要重新加载,windows中的nginx也要重新加载,否则的话是访问失败(还是原来的样子,没有变化)的。

3)请求参数处理。

~:波浪线表示后面跟着正则表达式匹配。

案例:

修改openResty中的nginx的nginx.conf文件。

修改openResty中的nginx目录下的lua目录下的item.lua文件。

都改完后执行nginx -s reload重新加载,然后访问。

4)查询Tomcat。

适用于所有虚拟机连接windows系统的便捷方式:虚拟机的IP地址前三个数字不变,第四个数字替换为1,、就一定能得到wdows地址。(前提是windows防火墙关闭)

例如:

虚拟机IP地址:192.168.203.129

连接windows系统使用:192.168.203.1

lua文件的语句结束不用“;”,但我发现使用了“;”也没有报错,要使用英文分号。

将函数导出:意思就是加载这个模块(类似java中的导包)的文件可以使用该函数。这里的发送请求会被反向代理拦截,然后发到指定IP地址。

5)Tomcat集群的负载均衡。

在Nginx中,使用 hash $request_uri; 可以实现基于请求URI的负载均衡策略。这个策略会根据请求的URI对后端服务器进行哈希计算,并将同一URI的请求始终分发到同一台后端服务器上。$ 符号表示引用变量的开始。在这种上下文中,$request_uri代表了请求的URI变量。

计算请求路径的哈希值,根据哈希值取余tomcat服务器数量,保证同一个请求路径只会发给同一个tomcat处理,保证进程缓存的可用性。

操作如下:

6)Redis缓存预热。

初始化redis缓存:

@Component
public class RedisHandler implements InitializingBean {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate IItemService itemService;@Autowiredprivate IItemStockService stockService;@Autowiredprivate static final ObjectMapper MAPPER = new ObjectMapper();@Overridepublic void afterPropertiesSet() throws Exception {//初始化缓存//1.查询商品信息List<Item> 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.查询库存信息List<ItemStock> 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);}}
}

7)查询Redis缓存。

common.lua文件:

-- 引入redis模块
local redis = require('resty.redis')
-- 初始化redis
local red = redis:new()
-- 设置redis超时时间
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 thenngx.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 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)endclose_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-- 记录错误信息,返回404ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)ngx.exit(404)endreturn 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
-- 导入cjson库
local cjson = require('cjson')-- 封装查询函数
function read_data(key,path,params)-- 查询redislocal resp = read_redis('127.0.0.1',6379,key)-- 判断查询结果if not resp thenngx.log(ngx.ERR,"redis查询失败,尝试查询http,key:",key)-- redis查询失败resp = 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))

改完文件后保存,并查询加载nginx。

注意:如果没有其效果,那就查lua后缀名的文件内容是否有写错。(我都是因为写错导致没有效果,可以查nginx日志,一般会告诉你因何错位)

8)Nginx本地缓存。

在 Nginx 中,"worker" 是指工作进程(worker process)。Nginx 的主进程负责管理整个服务器,而工作进程则负责处理实际的客户端请求。每个工作进程相互独立,它们可以同时处理多个客户端连接和请求。

linux的openResty里的nginx的nginx.conf文件:

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
-- 88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888
-- 封装查询函数
function read_data(key,expire,path,params)-- 查询本地缓存local val = item_cache:get(key) if not val thenngx.log(ngx.ERR,"本地缓存查询失败,尝试查询redis,key:",key)-- 查询redisval = read_redis('127.0.0.1',6379,key)-- 判断查询结果if not val thenngx.log(ngx.ERR,"redis查询失败,尝试查询http,key:",key)-- redis查询失败val = read_http(path,params)endend-- 查询成功,把数据写入本地缓存item_cache:set(key,val,expire)-- 返回数据return val
end
-- 888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888
--获取路径参数
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))

五、缓存同步策略。

1)数据同步策略。

使用MQ还是有一些代码侵入。

我们使用下面这种:下面这种代码侵入更少。

2)安装Canal。

2.1)初识Canal。 

2.2)安装和配置Canal

下面我们就开启mysql的主从同步机制,让Canal来模拟salve

1.开启MySQL主从

Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。

这里以之前用Docker运行的mysql为例:

1.1.开启binlog

打开mysql容器挂载的日志文件,我的在/tmp/mysql/conf目录:

这里是因为创建mysql容器的时候已经把mysql容器目录挂载到主机了,所以可以直接在主机修改对应文件。

修改文件:

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-bin

  • binlog-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在配置文件中,[mysqld]是一个段(section)的名称,表示 MySQL 服务器的配置部分。

然后重启mysql容器:

1.2.设置用户权限

接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对heima这个库的操作权限。(这里是在mysql里面执行,使用mysql客户端登录执行即可)

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;

2.安装Canal

2.1.创建网络

我们需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:

docker network create heima

让mysql加入这个网络:

docker network connect heima mysql

2.3.安装Canal

课前资料中提供了canal的镜像压缩包:

大家可以上传到虚拟机,然后通过命令导入:

docker load -i canal.tar

然后运行命令创建Canal容器:

在docker中,容器在同一个网络中可以使用容器名连接。

docker run -p 11111:11111 --name canal \
-e canal.destinations=heima \
-e canal.instance.master.address=mymysql: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 

3)监听Canal。

Canal框架 概念: canal用java开发的基于数据库增量日志解析,提供增量数据订阅&消费的中间件。目前,canal主要支持了MySQL的binlog解析,解析完成后才利用canal client 用来处理获得的相关数据。

Canal 是阿里巴巴开源的数据库变更数据抓取和同步框架,用于监听数据库的变更,并将这些变更事件传输到消息中间件或者其他存储介质中。

RedisHandler implements InitializingBean类:

@Component
public class RedisHandler implements InitializingBean {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate IItemService itemService;@Autowiredprivate IItemStockService stockService;@Autowiredprivate static final ObjectMapper MAPPER = new ObjectMapper();@Overridepublic void afterPropertiesSet() throws Exception {//初始化缓存//1.查询商品信息List<Item> 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.查询库存信息List<ItemStock> 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 {//1 item序列化为jsonString json = MAPPER.writeValueAsString(item);//2 存入redisredisTemplate.opsForValue().set("item:id:"+item.getId(),json);} catch (JsonProcessingException e) {throw new RuntimeException(e);}}public void deleteItemById(Long id){redisTemplate.delete("item:id:"+id);}
}

ItemHandler implements EntryHandler<Item>类:

@CanalTable("tb_item")
@Component
public class ItemHandler implements EntryHandler<Item> {@Autowiredprivate RedisHandler redisHandler;@Autowiredprivate Cache<Long, 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());}
}

六、多级缓存总结。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/193998.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NAND Flash和NOR Flash的异同

NAND Flash和NOR Flash是两种常见的闪存类型。 NOR Flash是Intel于1988年首先开发出来的存储技术&#xff0c;改变了原先由EPROM和EEPROM一统天下的局面。 NAND Flash是东芝公司于1989年发布的存储结构&#xff0c;强调降低每比特的成本&#xff0c;更高的性能&#xff0c;并…

栈和队列OJ题——15.循环队列

15.循环队列 622. 设计循环队列 - 力扣&#xff08;LeetCode&#xff09; * 解题思路&#xff1a; 通过一个定长数组实现循环队列 入队&#xff1a;首先要判断队列是否已满&#xff0c;再进行入队的操作&#xff0c;入队操作需要考虑索引循环的问题&#xff0c;当索引越界&…

网络接口规范

1、基本物理层: a) RJ45接口作为最基本的网络接口之一有两种形式&#xff1a;对于百兆网口有4条线&#xff0c;2对差分线&#xff1b;对于千兆网口有4对差分线。RJ45水晶头是有8个凹槽和8个触点&#xff08;8p8c&#xff09;的接头&#xff0c;分为集成网络变压器和非集成网络变…

2022年9月8日 Go生态洞察:Go Developer Survey 2022 Q2 结果分析

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

物奇平台电容触摸功能调试

是否需要申请加入数字音频系统研究开发交流答疑群(课题组)?可加我微信hezkz17, 本群提供音频技术答疑服务,+群赠送语音信号处理降噪算法,蓝牙耳机音频,DSP音频项目核心开发资料, 物奇平台电容触摸功能调试 1 修改按键驱动宏 2 编译生成wpk 文件,import 导入烧录文件。…

水果编曲软件fl studio手机版下载

fl studio mobile手机版中文名水果编曲软件&#xff0c;它是一款非常不错的音乐编曲软件&#xff0c;凭借简单易上手的操作方式&#xff0c;强悍且实用的功能&#xff0c;深受到了音乐创作者的喜爱&#xff0c;不仅仅提供了广阔的音乐创作空间&#xff0c;可以让用户对舞曲、轻…

工具网站:随机生成图片的网站

一个随机生成图片的网站&#xff1a;Lorem Picsum。 有时候&#xff0c;我们做静态页面需要大量图片去填充内容&#xff0c;以使用该网站去生成指定尺寸的图片。每次打开页面都会获取不同的图片&#xff0c;就不用我们做静态页面开发的时候&#xff0c;绞尽脑汁去找图片了。 …

振南技术干货集:ChatGPT,现在我做单片机/嵌入式开发已经离不开它了!(2)

注解目录 &#xff08;此文部分内客由 ChatGPT 生成&#xff0c;你分得出来哪些是人写的&#xff0c;哪些是 ChatGPT 生成的吗?&#xff09; 20.1 恐怖的 ChatGPT 2023年ChatGPT有多火?比 TikTok火4 倍都不止!什么是“范式革命”?从石器时代到飞机大炮就是范式革命。AI绘…

Python读取栅格遥感影像并加以辐射校正后导出为Excel的一列数据

本文介绍基于Python语言中的gdal模块&#xff0c;读取一景.tif格式的栅格遥感影像文件&#xff0c;提取其中每一个像元的像素数值&#xff0c;对像素值加以计算&#xff08;辐射定标&#xff09;后&#xff0c;再以一列数据的形式将计算后的各像元像素数据保存在一个.csv格式文…

IDA常用操作、快捷键总结以及使用技巧

先贴一张官方的图&#xff0c;然后我再总结一下&#xff0c;用的频率比较高的会做一些简单标注 快捷键 F系列【主要是调试状态的处理】 F2 添加/删除断点F4 运行到光标所在位置F5 反汇编F7 单步步入F8 单步跳过F9 持续运行直到输入/断点/结束 shift系列【主要是调出对应的页…

【RotorS仿真系列】Ardrone模型介绍

ardrone是rotors仿真框架提供的一款机型&#xff0c;因为该机型与我们实际使用的机型参数相近&#xff0c;所以这里对它的参数做特别整理和记录。 一、模型参数总结 ardrone的gazebo模型如下图所示&#xff1a; 根据ardrone.yaml&#xff0c;其关键参数如下所示&#xff1a…

Project 1: The Game of Hog(CS61A)

&#xff08;第一阶段&#xff09;问题 5a&#xff08;3 分&#xff09; 实现该函数&#xff0c;该函数模拟了完整的 Hog 游戏。球员 交替轮流掷骰子&#xff0c;直到其中一名玩家达到分数。playgoal 您现在可以忽略 Feral Hogs 规则和论点; 您将在问题 5b 中实现它。feral_h…

树莓派多串口通信

树莓派多串口通信 串口配置串口通信函数分析串口通信示例代码 参考博文1&#xff1a;树莓派 4 UART 多串口配置通信参考博文2&#xff1a;树莓派wiringPi库详解关于树莓派相关其他环境配置可参考&#xff1a;快速上手树莓派关于wiringPi库初始化与IO口开发可参考&#xff1a;树…

调优--学习笔记

1&#xff0c;Presto调优 数据存储格式 1&#xff09;合理设置分区 与Hive类似&#xff0c;Presto会根据元信息读取分区数据&#xff0c;合理的分区能减少Presto数据读取量&#xff0c;提升查询性能。 2&#xff09;使用列式存储 Presto对ORC文件读取做了特定优化&#xff0c…

Qt OpenCV 学习(一):环境搭建

对应版本 Qt 5.15.2OpenCV 3.4.9MinGW 8.1.0 32-bit 1. OpenCV 下载 确保安装 Qt 时勾选了 MinGW 编译器 本文使用 MinGW 编译好的 OpenCV 库&#xff0c;无需自行编译 确保下载的 MinGW 和上述安装 Qt 时勾选的 MinGW 编译器位数一致&#xff0c;此处均为 x86/32-bit下载地址…

《微信小程序开发从入门到实战》学习四十

4.2 云开发JSON数据库 4.2.11 更新数据 使用数据库API更新数据有两种方法&#xff1a;一.将记录局部更新的update方法&#xff1b;二.以替换的方式更新记录的set方法 update方法可以局部更新一个记录或一个集合的多个记录&#xff0c;更新时只有指定字段更新&#xff0c;其他…

智能诊疗体验:整合AI技术的互联网医院小程序开发

在科技化的趋势下&#xff0c;互联网医院小程序的开发变得愈发重要&#xff0c;尤其是通过整合人工智能&#xff08;AI&#xff09;技术&#xff0c;进一步提升了就医的效率。 一、引言 互联网医院小程序其开发目标是提高医疗服务的效率&#xff0c;同时也也提升了用户的就医…

23种设计模式之C++实践(一)

23种设计模式之C++实践 1. 简介2. 基础知识3. 设计模式(一)创建型模式1. 单例模式——确保对象的唯一性1.2 饿汉式单例模式1.3 懒汉式单例模式比较IoDH单例模式总结2. 简单工厂模式——集中式工厂的实现简单工厂模式总结3. 工厂方法模式——多态工厂的实现工厂方法模式总结4.…

【像素画板】游戏地图编辑器-uniapp项目开发流程详解

嘿&#xff0c;用过像素画板没有哦&#xff0c;相信喜欢绘画的小朋友会对它感兴趣呢&#xff0c;用来绘制像素画非常好看&#xff0c;有没有发现&#xff0c;它是可以用来绘制游戏地图的&#xff0c;是不是很好奇&#xff0c;来一起看看吧。 像素画板&#xff0c;也叫像素画的绘…

c语言-归并排序

目录 1、归并排序基本思想 2、归并排序的实现&#xff08;递归法&#xff09; 2.1 代码实现递归法归并排序 3、归并排序的实现&#xff08;非递归法&#xff09; 3.1 修正边界问题 3.2 代码实现非递归法归并排序 结语&#xff1a; 前言&#xff1a; 归并排序是一种把数…