目录
一、传统缓存的问题、多级缓存方案。
二、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。
六、多级缓存总结。
在Java中,进程缓存和缓存也是两个不同的概念。
进程缓存:在Java中,进程缓存通常指JVM的堆内存,它是Java虚拟机为每个Java进程分配的内存空间。Java进程可以使用堆内存来存储对象、数组等数据结构,以及执行方法时所需的局部变量、方法参数等。Java程序可以通过调整JVM的参数来控制堆内存的大小,从而影响程序的性能和内存占用。
缓存:在Java中,缓存通常指应用程序中的缓存机制,用于临时存储经常访问的数据,以提高数据访问速度。Java应用程序可以使用各种缓存框架来实现缓存机制,例如Ehcache、Guava Cache、Redis等。这些框架通常提供了一些API来支持数据的读取、写入、删除等操作,并可以通过配置文件或代码来指定缓存的容量、过期时间、失效策略等参数。
总的来说,Java中的进程缓存和缓存都是为了提高程序的性能和响应速度而存在的,但它们的作用和实现方式有所不同。进程缓存是JVM为每个Java进程分配的内存空间,用于存储Java对象和方法执行时所需的数据;而缓存是应用程序中的一种机制,用于缓存经常访问的数据,以减少对数据库或其他数据源的访问次数,提高程序的性能。
需要注意的是,Caffeine是一个进程级别的缓存,它只在单个Java进程内生效。
为了演示多级缓存,我们先导入一个商品管理的案例,其中包含商品的CRUD功能。我们将来会给查询商品添加多级缓存。
1.安装MySQL
后期做数据同步需要用到MySQL的主从功能,所以需要大家在虚拟机中,利用Docker来运行一个MySQL容器。
1.1.准备目录
为了方便后期配置MySQL,我们先准备两个目录,用于挂载容器的数据和配置文件目录:
# 进入/tmp目录 cd /tmp # 创建文件夹 mkdir mysql # 进入mysql目录 cd mysql1.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.251.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=10001.4.重启
配置修改后,必须重启容器:
docker restart mysql2.导入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; } } }
这里是使用代码方式写的。
使用案例:
public class CaffeineTest { /* 基本用法测试 */ @Test void testBasicOps() throws UnsupportedEncodingException { // 创建缓存对象 Cache
cache = Caffeine.newBuilder().build(); // 存数据 // cache.put("gf", "aaa"); // 取数据,不存在则返回null String gf = cache.getIfPresent("gf"); System.out.println("gf = " + gf); // 取数据,不存在则去数据库查询 String defaultGF = cache.get("defaultGF", key -> { // 这里可以去数据库根据 key查询value return "lll"; }); System.out.println("defaultGF = " + defaultGF); /** * 输出结果为: * gf = null * defaultGF = lll */ } /* 基于大小设置驱逐策略: */ @Test void testEvictByNum() throws InterruptedException { // 创建缓存对象 Cache 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: null System.out.println("gf2: " + cache.getIfPresent("gf2"));//gf2: null System.out.println("gf3: " + cache.getIfPresent("gf3"));//gf3: 迪丽热巴 } /* 基于时间设置驱逐策略: */ @Test void testEvictByTime() throws InterruptedException { // 创建缓存对象 Cache 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 } }
加载Cache成为Bean:
@Configuration public class CaffeineConfig { @Bean public Cache
itemCache(){ return Caffeine.newBuilder() .initialCapacity(100)//初始化100个key容量 .maximumSize(10_000)//上限是10000个key容量 .build(); } @Bean public Cache itemStockCache(){ return Caffeine.newBuilder() .initialCapacity(100)//初始化100个key容量 .maximumSize(10_000)//上限是10000个key容量 .build(); } } 使用Caffeine缓存:
@RestController @RequestMapping("item") public class ItemController { @Autowired private IItemService itemService; @Autowired private IItemStockService stockService; @Autowired private Cache
itemCache; @Autowired private Cache 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里面的进程缓存:
CentOS中自带Lua环境。
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。官网:https://www.lua.org/
简单写一个lua脚本:
lua中字符串拼接是使用..连接的,如local str = 'hello' .. 'world' #打印出来是helloworld
官方网站: https://openresty.org/cn/
1.安装
首先你的Linux虚拟机必须联网
1)安装开发库
首先要安装OpenResty的依赖开发库,执行命令:
yum install -y pcre-devel openssl-devel gcc --skip-broken2)安装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 openresty4)安装opm工具
opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。
如果你想安装命令行工具
opm
,那么可以像下面这样安装openresty-opm
包:yum install -y openresty-opm5)目录结构
默认情况下,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:$PATHNGINX_HOME:后面是OpenResty安装目录下的nginx的目录
然后让配置生效:
source /etc/profile2.启动和运行
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 然后访问页面: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 -- 记录错误信息,返回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;
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也要重新加载,否则的话是访问失败(还是原来的样子,没有变化)的。
~:波浪线表示后面跟着正则表达式匹配。
案例:
修改openResty中的nginx的nginx.conf文件。
修改openResty中的nginx目录下的lua目录下的item.lua文件。
都改完后执行nginx -s reload重新加载,然后访问。
适用于所有虚拟机连接windows系统的便捷方式:虚拟机的IP地址前三个数字不变,第四个数字替换为1,、就一定能得到wdows地址。(前提是windows防火墙关闭)
例如:
虚拟机IP地址:192.168.203.129
连接windows系统使用:192.168.203.1
lua文件的语句结束不用“;”,但我发现使用了“;”也没有报错,要使用英文分号。
将函数导出:意思就是加载这个模块(类似java中的导包)的文件可以使用该函数。这里的发送请求会被反向代理拦截,然后发到指定IP地址。
在Nginx中,使用
hash $request_uri;
可以实现基于请求URI的负载均衡策略。这个策略会根据请求的URI对后端服务器进行哈希计算,并将同一URI的请求始终分发到同一台后端服务器上。$
符号表示引用变量的开始。在这种上下文中,$request_uri
代表了请求的URI变量。计算请求路径的哈希值,根据哈希值取余tomcat服务器数量,保证同一个请求路径只会发给同一个tomcat处理,保证进程缓存的可用性。
操作如下:
初始化redis缓存:
@Component public class RedisHandler implements InitializingBean { @Autowired private StringRedisTemplate redisTemplate; @Autowired private IItemService itemService; @Autowired private IItemStockService stockService; @Autowired private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public void afterPropertiesSet() throws Exception { //初始化缓存 //1.查询商品信息 List
- 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
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); } } }
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 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 not found, 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 -- 导入cjson库 local cjson = require('cjson') -- 封装查询函数 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查询失败,尝试查询http,key:",key) -- redis查询失败 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))
改完文件后保存,并查询加载nginx。
注意:如果没有其效果,那就查lua后缀名的文件内容是否有写错。(我都是因为写错导致没有效果,可以查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 then ngx.log(ngx.ERR,"本地缓存查询失败,尝试查询redis,key:",key) -- 查询redis val = read_redis('127.0.0.1',6379,key) -- 判断查询结果 if not val then ngx.log(ngx.ERR,"redis查询失败,尝试查询http,key:",key) -- redis查询失败 val = read_http(path,params) end end -- 查询成功,把数据写入本地缓存 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))
使用MQ还是有一些代码侵入。
我们使用下面这种:下面这种代码侵入更少。
下面我们就开启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 mysql2.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
Canal框架 概念: canal是用java开发的基于数据库增量日志解析,提供增量数据订阅&消费的中间件。目前,canal主要支持了MySQL的binlog解析,解析完成后才利用canal client 用来处理获得的相关数据。
Canal 是阿里巴巴开源的数据库变更数据抓取和同步框架,用于监听数据库的变更,并将这些变更事件传输到消息中间件或者其他存储介质中。
RedisHandler implements InitializingBean类:
@Component public class RedisHandler implements InitializingBean { @Autowired private StringRedisTemplate redisTemplate; @Autowired private IItemService itemService; @Autowired private IItemStockService stockService; @Autowired private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public void afterPropertiesSet() throws Exception { //初始化缓存 //1.查询商品信息 List
- 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
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 { //1 item序列化为json String json = MAPPER.writeValueAsString(item); //2 存入redis 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); } } ItemHandler implements EntryHandler
- 类:
@CanalTable("tb_item") @Component public class ItemHandler implements EntryHandler
- { @Autowired private RedisHandler redisHandler; @Autowired private Cache
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()); } }