目录
JVM缓存(本地进程缓存)Caffeine技术栈基础介绍:
OpenResty技术栈基础介绍
Canal技术栈介绍
一、通过Caffeine实现进程缓存
1.1首先需要为你需要的数据构建Cache缓存对象,为了方便使用,可以将其声明为一个bean
1.2通过声明的Bean对象实现进程缓存
二、通过OpenResty扩展Nginx功能实现请求参数处理、查询tomcat
2.1安装OpenResty配置nginx的环境变量
2.1.1首先要安装OpenResty的依赖开发库,执行命令
2.1.2在 linux系统中添加 openresty 仓库,这样就可以便于未来安装或更新软件包,运行下面的命令添加仓库
2.1.3安装OpenResty
2.1.4安装opm工具
2.1.5查看安装目录
2.1.6配置nginx的环境变量
2.2启动和运行OpenResty
2.3配置windows端的nginx进行反向代理,路由请求到linux中的OpenResty中
2.3.1编辑windows的nginx.conf文件实现反向代理,监听前端发送的请求端口为80,并将请求路由到linux中,(为了方便,我关闭了windows的防火墙)
2.4配置OpenResty中的nginx中的nginx.conf文件(在后面有总体配置文件),以及编写lua代码实现nginx的本地缓存查询和redis操作
2.4.1实现OpenResty对路由的路径为/api/item的监听,并且定义返回的数据格式以及定义响应数据文
2.4.2在nginx.conf的http下面,添加对OpenResty的Lua模块的加载,这些数据库是为了之后操作redis和本地缓存所需要的依赖库
2.4.3最后一步配置反向代理,路由到tomcat服务器,这是因为,nginx本地缓存和redis缓存中都没有相关数据所以需要通过tomcat服务器查询自己的本地缓存或者数据库并数据返回过来,而要实现这一过程,就需要我们在指定的响应数据文件编写发送请求的代码,再由自己进行监听并路由到tomcat
2.4.5OpenResty的Nginx中的nginx.conf文件内容
2.5编写content_by_lua_file lua/item.lua中的item.lua实现对本地缓存和redis的操作
2.5.1创建对应文件:这个文件就是我们编写处理监听到访问请求后的处理逻辑(相当与service)
2.5.2对tomcat集群因为不同的tomcat服务器的本地缓存不同的解决方案
三.实现redis的热启动
四.缓存同步
4.1安装和配置Canal
4.1.1开启MySql主从
4.1.2设置用户权限
4.1.3修改后重启容器
4.1.4检查配置是否成功
4.1.5docker拉取canal/canal-server
4.1.6docker启动canal
4.2 配置canal集成到服务中
4.2.1引入依赖
4.2.2编写配置
4.2.3编写监听方法,监听Canal信息
4.2.4如果canal的logs日志发生以下错误(数据库的修改无法同步到redis和本地缓存)
前言:
多级缓存方案:
- 第一级缓存:通过浏览器客户端缓存,将页面静态资源缓存到浏览器中
- 第二级缓存:nginx的本地缓存,将部分动态数据存放到nginx本地
- 第三级缓存:通过nginx编写lua代码实现查询redis中的缓存数据
- 第四级缓存:查询tomcat的进程缓存,也称为JVM缓存,本篇使用Caffeine进行实现
多级缓存技术就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能,上面图例就是本篇文章实现多级缓存的整体结构,下面时缓存技术介绍,详情配置和使用可通过目录跳转
JVM缓存(本地进程缓存)Caffeine技术栈基础介绍:
缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:
- 分布式缓存,例如Redis:
- 优点:存储容量更大、可靠性更好、可以在集群间共享
- 缺点:访问缓存有网络开销
- 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
- 进程本地缓存,例如HashMap、GuavaCache:
- 优点:读取本地内存,没有网络开销,速度更快
- 缺点:存储容量有限、可靠性较低、无法共享
场景:性能要求较高,缓存数据量较小
这里小编将通过Caffeine实现本地缓存技术,因为Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine
Caffeine提供了三种缓存驱逐策略:
- 基于容量:设置缓存的数量上限
// 创建缓存对象 Cache<String, String> cache = Caffeine.newBuilder().maximumSize(1) // 设置缓存大小上限为 1.build();
- 基于时间:设置缓存的有效时间
// 创建缓存对象 Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(10)) // 设置缓存有效期为 10 秒,从最后一次写入开始计时.build();
基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。
注意:
在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。
OpenResty技术栈基础介绍
OpenResty®是一个基于 Nginx的高性能 Web 平台,底层是基于Nginx的,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
具备下列特点:
- 具备Nginx的完整功能
- 基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块
- 允许使用Lua自定义业务逻辑、自定义库
OpenResty的基础实现:
- 小编将通过在windows中本地部署Nginx进行反向代理将前端的页面访问请求发送到Linux系统中并指定对应端口
- 再在linux系统中部署OpenResty服务进行监听指定端口,拦截发过来的访问请求,如果请求是查询请求,则返回OpenResty中Nginx的本地缓存
- 如果Nginx的本地缓存不存在对应数据,则编写lua代码和调用OpenResty提供的方法库对Redis进行数据查询操作,如果查询到数据,将数据保存到本地缓存中,并将数据返回
- 如果redis也没有对应缓存数据,则通过OpenResty的nginx中编写代码发起向tomcat服务端发起访问请求并由自己进行监听并发送
- 然后查询tomcat服务器的本地缓存或者数据库,并将查询到的数据保存到redis和nginx本地缓存中
Canal技术栈介绍
缓存数据同步的常见方式有三种:
- 设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新
- 优势:简单、方便
- 缺点:时效性差,缓存过期之前可能不一致
- 场景:更新频率较低,时效性要求低的业务
- 同步双写:在修改数据库的同时,直接修改缓存
- 优势:时效性强,缓存与数据库强一致
- 缺点:有代码侵入,耦合度高;
- 场景:对一致性、时效性要求较高的缓存数据
- 异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
- 优势:低耦合,可以同时通知多个缓存服务
- 缺点:时效性一般,可能存在中间不一致状态
- 场景:时效性要求一般,有多个服务需要同步
Canal [kə'næl],译意为水道/管道/沟渠,canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。GitHub的地址:https://github.com/alibaba/canal
Canal是基于mysql的主从同步来实现的,MySQL主从同步的原理如下:
- MySQL master 将数据变更写入二进制日志( binary log),其中记录的数据叫做binary log events
- MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log)
- MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
Canal就是把自己伪装成MySQL的一个slave节点,从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端,进而完成对其它数据库的同步。
一、通过Caffeine实现进程缓存
1.1首先需要为你需要的数据构建Cache缓存对象,为了方便使用,可以将其声明为一个bean
配置示例如下:这里我为我需要缓存的item和itemStock两个实例定义了对应的缓存对象,调用Caffeine的newbuilder方法,构造出缓存容器的大小和上限。
Cache<Long, Item>中的泛型数据类型(相当于Map中的键值对),Long指代的是我商品id,Item指代的是我的item对象,也就是说,我们可以调用Cache的方法通过指定泛型中的第一个参数查找出第二个参数的值,和Map存放和读取数据类似
package com.heima.item.config;import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class CaffeineConfig {/*** 定义商品的本地缓存,初始容量为100,最大容量为10000* @return*/@Beanpublic Cache<Long, Item> itemCache() {return Caffeine.newBuilder().maximumSize(10000).initialCapacity(100).build();}/*** 定义商品库存的本地缓存,初始容量为100,最大容量为10000* @return*/@Beanpublic Cache<Long, ItemStock> itemStockCache() {return Caffeine.newBuilder().maximumSize(10000).build();}
}
1.2通过声明的Bean对象实现进程缓存
首先,注入声明的Cache的bean对象
@Autowiredprivate Cache<Long, Item> itemCache;@Autowiredprivate Cache<Long, ItemStock> itemStockCache;
然后,通过bean对象实例实现进程缓存,这里调用了Cache自带的API接口"get",这个接口实现了,通过传递的key(如,我这里的id)先在本地缓存中查询对应的值,如果未查询到数据,就将执行自定义逻辑处理,也就是下面的lambda表达式,我定义的处理逻辑是调用service层定义的接口,查询本地数据库,并将查询到的数据保存到本地缓存中,然后再返回给前端,之后下次再执行此命令时,将直接读取本地缓存里面的数据
@GetMapping("/{id}")public Item findById(@PathVariable("id") Long 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) {return itemStockCache.get(id, key -> stockService.getById(id));}
这样,我们就实现基本的本地缓存
二、通过OpenResty扩展Nginx功能实现请求参数处理、查询tomcat
前言:
执行yum安装发生错误可以查看我的另一篇文章:CentOS7--IP地址初始化失败、配置静态ip地址后dns解析错误、yum无法使用,yum镜像问题---超详细解决方法
2.1安装OpenResty配置nginx的环境变量
2.1.1首先要安装OpenResty的依赖开发库,执行命令
yum install -y pcre-devel openssl-devel gcc --skip-broken
2.1.2在 linux系统中添加 openresty
仓库,这样就可以便于未来安装或更新软件包,运行下面的命令添加仓库
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
如果提示说命令不存在,则运行:
yum install -y yum-utils
然后再重复上面的命令
2.1.3安装OpenResty
yum install -y openresty
2.1.4安装opm工具
opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。 如果你想安装命令行工具 opm
,那么可以像下面这样安装 openresty-opm
包:
yum install -y openresty-opm
2.1.5查看安装目录
默认情况下,OpenResty安装的目录是:/usr/local/openresty
2.1.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.2启动和运行OpenResty
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;
}
}
}
这里我监听的服务窗口时本地的8081,读者请自行根据需要改变自己的IP地址和端口号
启动nginx以查看nginx界面进行检查配置是否成功(通过windos访问linux的ip和端口需要开发linux端口,或者关闭防火墙)
2.3配置windows端的nginx进行反向代理,路由请求到linux中的OpenResty中
我们已经在linux中安装好了OpenResty,接下来就是将前端的请求处理转发到linux的OpenResty中,进行缓存数据查询
2.3.1编辑windows的nginx.conf文件实现反向代理,监听前端发送的请求端口为80,并将请求路由到linux中,(为了方便,我关闭了windows的防火墙)
2.4配置OpenResty中的nginx中的nginx.conf文件(在后面有总体配置文件),以及编写lua代码实现nginx的本地缓存查询和redis操作
2.4.1实现OpenResty对路由的路径为/api/item的监听,并且定义返回的数据格式以及定义响应数据文
server {
listen 8081;
server_name localhost;
location ~ /api/item/(\d+) {
# 响应类型,这里返回json
default_type application/json;
# 响应数据由 lua/item.lua这个文件来决定
content_by_lua_file lua/item.lua;
}
解析:
- location ~ /api/item/(\d+) : 表示监听路径为/api/item/(至少一个数字)的路径如(192.168.8.1:8080/api/item/10001)并且获取10001这个路径参数,为了以后进行数据查询
- default_type application/json: 定义返回的数据格式,将查询到的缓存数据,根据自己前端需要数据的需求进行返回
- content_by_lua_file lua/item.lua:定义响应数据内容,和实现对本地缓存数据和redis的查询,相当于我们SpringMVC的service层,完成具体业务,只不过一个的开发语言是java,而这里使用的lua
OpenResty提供了各种API用来获取不同类型的请求参数:
参数格式
参数示例
参数解析代码示例
路径占位符
/item/1001
# 1.正则表达式匹配:
location ~ /item/(\d+) {
content_by_lua_file lua/item.lua;
}
-- 2. 匹配到的参数会存入ngx.var数组中,
-- 可以用角标获取
local id = ngx.var[1]
请求头
id:1001
-- 获取请求头,返回值是table类型
local headers = ngx.req.get_headers()
Get请求参数
?id=1001
-- 获取GET请求参数,返回值是table类型
local getParams = ngx.req.get_uri_args()
Post表单参数
id=1001
-- 读取请求体
ngx.req.read_body()
-- 获取POST表单参数,返回值是table类型
local postParams = ngx.req.get_post_args()
JSON参数
{"id": 1001}
-- 读取请求体
ngx.req.read_body()
-- 获取body中的json参数,返回值是string类型
local jsonBody = ngx.req.get_body_data()
2.4.2在nginx.conf的http下面,添加对OpenResty的Lua模块的加载,这些数据库是为了之后操作redis和本地缓存所需要的依赖库
代码:
#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m;
位置:
2.4.3最后一步配置反向代理,路由到tomcat服务器,这是因为,nginx本地缓存和redis缓存中都没有相关数据所以需要通过tomcat服务器查询自己的本地缓存或者数据库并数据返回过来,而要实现这一过程,就需要我们在指定的响应数据文件编写发送请求的代码,再由自己进行监听并路由到tomcat
# 声明tomcat服务集群
upstream tomcat-cluster {
hash $request_uri;
server 192.168.8.1:8081;
server 192.168.8.1:8082;
}
#路径为
location /item {
proxy_pass http://tomcat-cluster;
}
2.4.5OpenResty的Nginx中的nginx.conf文件内容
注意:这里重新加载配置文件会报错,你需要将对应的文件(如item.lua)创建完成后再进行加载
#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;;";
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m;
# 声明tomcat服务集群
upstream tomcat-cluster {
hash $request_uri;
server 192.168.8.1:8081;
server 192.168.8.1:8082;
}
server {
listen 8081;
server_name localhost;
location /item {
proxy_pass http://tomcat-cluster;
}
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;
}
}
}
2.5编写content_by_lua_file lua/item.lua中的item.lua实现对本地缓存和redis的操作
2.5.1创建对应文件:这个文件就是我们编写处理监听到访问请求后的处理逻辑(相当与service)
具体处理代码:这里我封装了一些方法(read_http,read_redis)到一个自定义的common 包中(在后面),同时我的redis部署linux本地的docker容器,开放的端口为6380
--导入函数库,获取请求发送方法
local common = require('common')
local read_http = common.read_http
--获取redis操作函数
local read_redis = common.read_redis--导入共享词典,本地缓存local item_cache = ngx.shared.item_cache--封装查询函数
function read_data(key, expire, path, params)--查询本地缓存local resp = item_cache:get(key)if not resp then ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询redis获取数据, key", (key or nil))--查询redisresp = read_redis("127.0.0.1",6380,key)--判断查询结果if not resp thenngx.log(ngx.ERR, "redis 查询失败,尝试查询服务端获取数据, key:", (key or nil))--redis 查询失败,尝试查询服务端获取数据resp = read_http(path, params)end--查询成功,把数据写入本地缓存中item_cache:set(key, resp, expire)endreturn resp
end--导入cjson函数库,获取反序列化与序列化方法
local cjson = require('cjson')-- 获取路径参数
local id = ngx.var[1]
-- 查询商品信息
local itemJSON = read_data("item:id:" .. id, 1800, "/item/" .. id, nil)
--查询库存商品信息
local itemStockJSON = read_data("item:stock:id:" .. id, 60, "/item/stock/" .. id, nil)
--将得到的商品JSON数据转化为table数据
local item = cjson.decode(itemJSON)
--将得到的库存商品JSON数据转化为table数据
local itemStock = cjson.decode(itemStockJSON)
--组合数据
item.stock = itemStock.stock
item.sold = itemStock.sold
--再将组合数据序列化为JSON格式,再返回结果
ngx.say(cjson.encode(item))
nginx提供了内部API用以发送http请求:
demo:
local resp = ngx.location.capture("/path",{
method = ngx.HTTP_GET, -- 请求方式
args = {a=1,b=2}, -- get 方式传参数,和下面任选一个
body = "c=3&d=4" -- post方式传参数
})
返回的响应内容包括:
- resp.status:响应状态码
- resp.header:响应头,是一个table
- resp.body:响应体,就是响应数据
注意:这里的path是路径,并不包含IP和端口。这个请求会被nginx内部的server监听并处理。
但是我们希望这个请求发送到Tomcat服务器,所以还需要编写一个server来对这个路径做反向代理
common包文件地址:注意,自定义的包需要放在/lualib中否则无法扫描到
common文件的代码:
--导入redis
local redis = require('resty.redis')
--释放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)--初始化redislocal red = redis:new()--设置超时时间,建立连接时间,发送请求时间,等待响应时间red:set_timeout(1000,1000,1000)-- 获取一个连接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
2.5.2对tomcat集群因为不同的tomcat服务器的本地缓存不同的解决方案
通过配置nginx的hash负载均衡,实现发送的某些请求定向访问某一个tomcat服务器,这样解决问题了,比如我的/item/10001进行hash算法只会路由到tomcat8081
三.实现redis的热启动
冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。
缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。
1.导入依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
2.配置application进行服务发现
3.代码实现:这里继承了InitializingBean的接口并实现了afterPropertiesSet()方法,这个方法会在服务启动一开始就执行方法内的代码,实现将数据库的数据读取到redis中
package com.heima.item.utils;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import com.heima.item.service.impl.ItemService;
import com.heima.item.service.impl.ItemStockService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.List;@Component
public class RedisHandler implements InitializingBean {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate ItemService itemService;@Autowiredprivate ItemStockService itemStockService;//JSON处理工具private static final ObjectMapper MAPPER = new ObjectMapper();@Overridepublic void afterPropertiesSet() throws Exception {//获取产品列表List<Item> itemList = itemService.list();//放入缓存for (Item item : itemList) {//将商品对象转换为JSON对象String itemJson = MAPPER.writeValueAsString(item);//将JSON数据放入缓存redisTemplate.opsForValue().set("item:id:" + item.getId(), itemJson);}//获取产品库存列表List<ItemStock> itemStockList = itemStockService.list();//放入缓存for (ItemStock itemStock : itemStockList) {//将商品库存对象转换为JSON对象String itemStockJson = MAPPER.writeValueAsString(itemStock);//将JSON数据放入缓存redisTemplate.opsForValue().set("item:stock:id:" + itemStock.getId(), itemStockJson);}}/*** 商品信息新增修改redis的方法* @param item*/public void saveItem(Item item) {try {//将商品对象转换为JSON对象String itemJson = MAPPER.writeValueAsString(item);//将JSON数据放入缓存redisTemplate.opsForValue().set("item:id:" + item.getId(), itemJson);} catch (JsonProcessingException e) {throw new RuntimeException(e);}}/*** 通过商品id进行删除* @param id* @throws Exception*/public void deleteItem(Long id) {try {redisTemplate.delete("item:id:" + id);} catch (Exception e) {throw new RuntimeException(e);}}
}
四.缓存同步
4.1安装和配置Canal
4.1.1开启MySql主从
Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。这里我采用的是docker的Mysql容器,docker具体操作可以查看我的另一篇文章:Docker应用详解篇——docker的安装、配置docker镜像加速、docker常见命令详解、数据卷挂载,使用DockerCompose部署微服务集群
启动我的Mysql容器
docker run \
-p 3307:3306 \
--name mysql \
-v $PWD/conf:/etc/mysql/conf.d \
-v $PWD/logs:/logs \
-v $PWD/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=1234 \
--privileged \
-d \
mysql:5.7.25
打开mysql容器挂载的日志文件,我的在/res/mysql/conf
目录:
修改文件,再末尾添加
#配置binlog的存放位置/var/lib/mysql/以及名称mysql-bin
log-bin=/var/lib/mysql/mysql-bin
#配置binlog监听的数据库我这里的是demo
binlog-do-db=demo
全部文件内容:
[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=demo
4.1.2设置用户权限
接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对demo这个库的操作权限。
create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
FLUSH PRIVILEGES;
4.1.3修改后重启容器
docker restart mysql
4.1.4检查配置是否成功
测试设置是否成功:在mysql控制台,或者Navicat中,输入命令:
show master status;
4.1.5docker拉取canal/canal-server
docker pull canal/canal-server
拉取失败可重新配置镜像源
vi /etc/docker/daemon.json
更改为以下内容:
{
"registry-mirrors": [
"https://2a6bf1988cb6428c877f723ec7530dbc.mirror.swr.myhuaweicloud.com",
"https://docker.m.daocloud.io",
"https://hub-mirror.c.163.com",
"https://mirror.baidubce.com",
"https://your_preferred_mirror",
"https://dockerhub.icu",
"https://docker.registry.cyou",
"https://docker-cf.registry.cyou",
"https://dockercf.jsdelivr.fyi",
"https://docker.jsdelivr.fyi",
"https://dockertest.jsdelivr.fyi",
"https://mirror.aliyuncs.com",
"https://dockerproxy.com",
"https://mirror.baidubce.com",
"https://docker.m.daocloud.io",
"https://docker.nju.edu.cn",
"https://docker.mirrors.sjtug.sjtu.edu.cn",
"https://docker.mirrors.ustc.edu.cn",
"https://mirror.iscas.ac.cn",
"https://docker.rainbond.cc"
]
}
4.1.6docker启动canal
docker run -p 11111:11111 --name canal3 \
-e canal.destinations=cache \
-e canal.instance.master.address=192.168.8.128:3307 \
-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=demo\\..* \
--network cache-net \
-d canal/canal-server
解析:
- canal.destinations=cache:canal集群名为cache(可自定义)
- canal.instance.master.address:数据库地址
- canal.instance.dbUsername=canal : 配置canal的用户名需要与前面配置主从数据库的用户名相同
- canal.instance.filter.regex=demo\\..*:主从同步canal监听的数据库为demo中的全部表
表名称监听支持的语法:
mysql 数据解析关注的表,Perl正则表达式.
多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\)
常见例子:
1. 所有表:.* or .*\\..*
2. canal schema下所有表: canal\\..*
3. canal下的以canal打头的表:canal\\.canal.*
4. canal schema下的一张表:canal.test1
5. 多个规则组合使用然后以逗号隔开:canal\\..*,mysql.test1,mysql.test2
4.2 配置canal集成到服务中
4.2.1引入依赖
<dependency><groupId>top.javatool</groupId><artifactId>canal-spring-boot-starter</artifactId><version>1.2.1-RELEASE</version></dependency>
4.2.2编写配置
canal:destination: cacheserver: 192.168.8.128:11111
4.2.3编写监听方法,监听Canal信息
数据库发送增删改操作时就会被ItemHandler监听到,然后执行对应的方法体,完成对本地缓存和redis的操作
package com.heima.item.canal;import com.github.benmanes.caffeine.cache.Cache;
import com.heima.item.pojo.Item;
import com.heima.item.utils.RedisHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import top.javatool.canal.client.annotation.CanalTable;
import top.javatool.canal.client.handler.EntryHandler;//监听的数据库中的表的名称
@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);//写数据到redis中redisHandler.saveItem(item);}@Overridepublic void update(Item before, Item after) {//写数据到JVM缓存中itemCache.put(after.getId(), after);//写数据到redis中redisHandler.saveItem(after);}@Overridepublic void delete(Item item) {//删除本地缓存数据itemCache.invalidate(item.getId());//删除redis中的数据redisHandler.deleteItem(item.getId());}
}
Canal推送给canal-client的是被修改的这一行数据(row),而我们引入的canal-client则会帮我们把行数据封装到Item实体类中。这个过程中需要知道数据库与实体的映射关系,要用到JPA的几个注解:
4.2.4如果canal的logs日志发生以下错误(数据库的修改无法同步到redis和本地缓存)
Received error packet: errno = 1236, sqlstate = HY000 errmsg = unknown error reading log event on the master; the first event 'mysql-bin.000003' at 4, the last event read from '/var/lib/mysql/mysql-bin.000003' at 154, the last byte read from '/var/lib/mysql/mysql-bin.000003' at 154.
执行下面指令:
set global binlog_checksum='NONE'
然后重启Mysql容器,就解决问题了!