Spring Cloud之多级缓存

目录

传统缓存

多级缓存

JVM进程缓存

Caffeine

缓存驱逐策略

实现进程缓存

常用Lua语法

数据类型

变量声明

循环使用

定义函数

条件控制

安装OpenResty

实现Nginx业务逻辑编写

请求参数解析

实现lua访问tomcat

JSON的序列化和反序列化

Tomcat的集群负载均衡

添加Redis缓存

启动Redis

查询Redis缓存

Nginx本地缓存

缓存同步策略

Canal

安装和配置Canal

监听Canal

多级缓存访问流程


资料下载:day04-多级缓存

下载完成后跟着案例导入说明去做

传统缓存

传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库,存在下面的问题

  • 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈
  • Redis缓存失效时,会对数据库产生冲击

多级缓存

多级缓存主要压力在于nginx,在生产环境中,我们需要通过部署nginx本地缓存集群以及一个nginx反向代理到本地缓存

JVM进程缓存

缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:

  • 分布式缓存,例如Redis:
    • 优点:存储容量更大、可靠性更好、可以在集群间共享
    • 缺点:访问缓存有网络开销
    • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
  • 进程本地缓存,例如HashMap、GuavaCache
    • 优点:读取本地内存,没有网络开销,速度更快
    • 缺点:存储容量有限、可靠性较低、无法共享
    • 场景:性能要求较高,缓存数据量较小

Caffeine

案例测试代码

@Test
void testBasicOps() {// 创建缓存对象Cache<String, String> cache = Caffeine.newBuilder().build();// 存数据cache.put("name", "张三");// 取数据,不存在则返回nullString name = cache.getIfPresent("name");System.out.println("name = " + name);// 取数据,不存在则去数据库查询String defaultName = cache.get("defaultName", key -> {// 这里可以去数据库根据 key查询valuereturn "李四";});System.out.println("defaultName = " + defaultName);
}

运行结果如下

缓存驱逐策略

Caffeine提供了三种缓存驱逐策略:

  • 基于容量:设置缓存的数量上限
  • 基于时间:设置缓存的有效时间
  • 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据,性能较差。

默认情况下,当缓存数据过期时,并不会立即将其清理和驱逐,而是在一次读或写操作后,或是在空闲时间完成对失效数据的驱逐。

基于容量实现

    /*基于大小设置驱逐策略:*/@Testvoid testEvictByNum() throws InterruptedException {// 创建缓存对象Cache<String, String> cache = Caffeine.newBuilder()// 设置缓存大小上限为 1.maximumSize(1).build();// 存数据cache.put("name1", "张三");cache.put("name2", "李四");cache.put("name3", "王五");// 延迟10ms,给清理线程一点时间Thread.sleep(10L);// 获取数据System.out.println("name1: " + cache.getIfPresent("name1"));System.out.println("name2: " + cache.getIfPresent("name2"));System.out.println("name3: " + cache.getIfPresent("name3"));}

运行结果如下 

基于时间实现

/*基于时间设置驱逐策略:*/@Testvoid testEvictByTime() throws InterruptedException {// 创建缓存对象Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 10 秒.build();// 存数据cache.put("name", "张三");// 获取数据System.out.println("name: " + cache.getIfPresent("name"));// 休眠一会儿Thread.sleep(1200L);System.out.println("name: " + cache.getIfPresent("name"));}

运行结果如下 

实现进程缓存

利用Caffeine实现下列需求:

  • 给根据id查询商品的业务添加缓存,缓存未命中时查询数据库
  • 给根据id查询商品库存的业务添加缓存,缓存未命中时查询数据库
  • 缓存初始大小为100
  • 缓存上限为10000

添加缓存对象

@Configuration
public class CaffeineConfig {/*** 商品信息缓存* @return*/@Beanpublic Cache<Long, Item> itemCache(){return Caffeine.newBuilder().initialCapacity(100).maximumSize(10_000).build();}/*** 商品库存缓存* @return*/@Beanpublic Cache<Long, ItemStock> itemStockCache(){return Caffeine.newBuilder().initialCapacity(100).maximumSize(10_000).build();}
}

在ItemController中写入查询本地缓存的方法

    @Autowiredprivate Cache<Long, Item> itemCache;@Autowiredprivate Cache<Long, ItemStock> itemStockCache;@GetMapping("/{id}")public Item findById(@PathVariable("id") Long id) {return itemCache.get(id, key -> {return itemService.query().ne("status", 3).eq("id", key).one();});}@GetMapping("/stock/{id}")public ItemStock findStockById(@PathVariable("id") Long id) {return itemStockCache.get(id,key->{return stockService.getById(id);});}

修改完成后,访问localhost:8081/item/10001,观察控制台

存在一次数据库查询。后续再次查询相同id数据不会再次查询数据库。至此实现了JVM进程缓存。

常用Lua语法

Nginx与Redis的业务逻辑编写并不是通过Java语言,而是通过Lua。Lua是一种轻量小巧的脚本语言,用标准的C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

入门案例,输出hello world

在linux中创建一个文本文件

touch hello.lua
# 进入vi模式
vi hello.lua
# 打印hello world。输入以下内容
print("hello world")# 保存退出后,运行lua脚本
lua hello.lua

或是直接输入命令启动lua控制台

lua

直接输入命令即可

数据类型

数据类型

描述

nil

表示一个无效值,类似于Java中的null,但在条件表达式中代表false

boolean

包含:true与false

number

表示双精度类型的实浮点数(简单来说,是数字都可以使用number表示)

string

字符串,由单引号或双引号来表示

function

由C或是Lua编写的函数

table

Lua中的表其实是一个“关联数组”,数组的索引可以是数字,字符串或表类型。在 Lua里,table的创建是通过“构造表达式”来完成,最简单构造表达式是{},用来创建一个空表。

变量声明

Lua声明变量的时候,并不需要指定数据类型

-- local代表局部变量,不加修饰词,代表全局变量
local str ='hello'
local num =10
local flag =true
local arr ={'java','python'} --需要注意的是,访问数组元素时,下标是从1开始
local table ={name='Jack',age=10} --类似于Java中的map类型,访问数据时是通过table['key']或是table.key

循环使用

-- 声明数组
local arr={'zhangsan','lisi','wangwu'}
-- 进行循环操作
for index,value in ipairs(arr) doprint(index,value)
end
-- lua 脚本中,for循环从do开始end结束,数组解析使用ipairs
-- 声明table
local table={name='zhangsan',age=10}
-- 进行循环操作
for key,value in pairs(table) doprint(key,value)
end
-- table解析使用pairs

执行lua脚本

定义函数

-- 声明数组
local arr={'zhangsan','lisi','wangwu'}
-- 定义函数
local function printArr(arr)for index,value in ipairs(arr) doprint(index,value)end
end
-- 执行函数
printArr(arr)

执行lua脚本

条件控制

操作符

描述

实例

and

逻辑与操作符。若A为false,则返回A,否则返回B

(A and B)为false

or

逻辑或操作符。若A为true,则返回A,否则返回B

(A or B)为true

not

逻辑非操作符。与逻辑运算结果相反

not(A and B)为true

-- 声明数组
local table={name='zhangsan',sex='boy',age=15}
-- 定义函数
local function printTable(arr)if(not arr) thenprint('table中不存在该字段')return nilendprint(arr)
end
-- 执行函数
printTable(table.name)
printTable(table.addr)

执行lua脚本

安装OpenResty

是基于Nginx的一个组件,主要作用是对Nginx编写业务逻辑

yum install -y pcre-devel openssl-devel gcc --skip-brokenyum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
# 如果失败则先执行下面一条语句后再执行上面这条
yum install -y yum-utils yum install -y openrestyyum install -y openresty-opm

配置nginx的环境变量

vi /etc/profile# 在最下面插入如下信息
export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH# 保存后刷新配置
source /etc/profile

修改/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;}}
}

启动nginx

# 启动nginx

nginx

# 重新加载配置

nginx -s reload

# 停止

nginx -s stop

启动后,访问虚拟机的8081端口,如果正常跳转页面如下

实现Nginx业务逻辑编写

先分析请求转发流程。打开win系统上的nginx路由配置文件

接下来就需要对虚拟机中的nginx添加业务逻辑了

对虚拟机Nginx中的配置文件添加如下代码

    # 放入http模块下#lua 模块lua_package_path "/usr/local/openresty/lualib/?.lua;;";#c模块     lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  # 放入server模块下location /api/item {# 响应类型为jsondefault_type application/json;# 响应结果来源content_by_lua_file lua/item.lua;}

编写lua脚本

在nginx目录下创建lua文件夹,并创建lua脚本

mkdir lua
touch lua/item.lua

先使用假数据测试是否可以正常响应

ngx.say('{"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}')

访问localhost/item.html?id=10001。查看控制台是否正常响应。如果出现如下错误,去观察win系统下的nginx日志,我的打印了如下错误

2023/11/07 19:29:38 [error] 16784#2812: *34 connect() failed (10061: No connection could be made because the target machine actively refused it) while connecting to upstream, client: 127.0.0.1, server: localhost, request: "GET /api/item/10001 HTTP/1.1", upstream: "http://192.168.10.10:8081/api/item/10001", host: "localhost", referrer: "http://localhost/item.html?id=10001"

解决方法,打开任务管理器,将所有关于nginx的服务全部结束再次重启win系统下的nginx即可。如果不是此类错误,请查看linux系统下的错误日志。

请求参数解析

参数格式

参数实例

参数解析代码示例

路径占位符

/item/1001

拦截路径中:location ~ /item/(\d+){}

~:表示使用正则表达式

(\d+):表示至少有一位数字

Lua脚本中:local id = ngx.var[1]

匹配到的参数会存入ngx.var数组中,通过下标获取

请求头

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.reg.read bodv()

获取body中的ison参数,返回值是string类型

local jsonBody = ngx.req.get_body_data()

修改linux中nginx的配置文件,实现参数解析

		location ~ /api/item/(\d+) {# 响应类型为jsondefault_type application/json;# 响应结果来源content_by_lua_file lua/item.lua;}

修改lua脚本

-- 获取参数
local id = ngx.var[1]
-- 返回结果
ngx.say('{"id":'..id..',"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}')

访问id为10002的参数,可以发现id随着参数改变,而不是伪数据了

实现lua访问tomcat

nginx提供了内部API用来发送http请求

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地址和端口而是会被内部拦截,这个时候我们还需要编写一个路由器,发送到对应的服务器。修改linux中的nginx.conf文件添加如下配置

		location /item {proxy_pass http://192.168.10.11:8081;}

发起Http请求我们可以封装成一个方法,让其他请求发起时也可以调用,因此,我们可以在lualib文件夹下,创建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

修改item.lua脚本,不再返回伪数据,而是查询真实的数据

-- 导入common函数库
local common = require('common')
local read_http = common.read_http-- 获取参数
local id = ngx.var[1]
-- 查询商品信息
local itemJSON = read_http('/item/'..id,nil)
-- 查询库存信息
local stockJSON = read_http('/item/stock/'..id,nil)
-- 返回结果
ngx.say(itemJSON)

这里只返回了商品信息,接下来访问其他id的商品,查看是否可以查询出商品信息

JSON的序列化和反序列化

引入cjson模块,实现序列化与反序列化

-- 导入common函数库
local common = require('common')
local cjson = require('cjson')
local read_http = common.read_http-- 获取参数
local id = ngx.var[1]
-- 查询商品信息
local itemJSON = read_http('/item/'..id,nil)
-- 查询库存信息
local stockJSON = read_http('/item/stock/'..id,nil)
-- 反序列化JSON商品信息为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))

Tomcat的集群负载均衡

这里我们访问的服务端口是写死的,但通常tomcat是一个集群,因此,我们需要修改我们linux的配置文件,配置tomcat集群

由于Tomcat的负载均衡策略为轮询,那么就会产生一个问题,tomcat集群的进程缓存是不共享的,也就是说,第一次访问8081生成的缓存,在第二次访问8082时,是不存在的,会在8082也生成一份相同的缓存。所以我们需要保证访问同一个id的请求,会被路由到存在缓存的那个tomcat服务器上。这就需要我们修改负载均衡算法。实际实现很简单,只需要在tomcat集群配置添加一行

实现原理是,nginx会对拦截到的请求进行hash算法,然后对集群数量进行取余。从而保证对同一个id的请求都会被路由到同一个tomcat服务器。

添加Redis缓存

本地缓存在访问进程缓存之间,应该先去查询Redis缓存,在添加Redis缓存时,又存在冷启动与缓存预热问题。

  • 冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。
  • 缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保

启动Redis

在docker中输入如下命令

docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes

启动成功后使用RESP连接redis

成功连接后,我们需要进行预热,我们的数据不多,将所有的数据全都缓存进去即可,编写一个初始化Handler

@Component
public class RedisHandler implements InitializingBean {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate ItemService itemService;@Autowiredprivate ItemStockService itemStockService;private final static ObjectMapper MAPPER = new ObjectMapper();/*** Bean生命周期之生成Bean对象之后属性填充** @throws Exception*/@Overridepublic void afterPropertiesSet() throws Exception {//将数据库中的数据进行填充//查询商品数据并填充List<Item> listItems = itemService.list();List<ItemStock> listStock = itemStockService.list();for (Item listItem : listItems) {String itemJson = MAPPER.writeValueAsString(listItem);redisTemplate.opsForValue().set("itemInfo:id:"+listItem.getId(),itemJson);}for (ItemStock itemStock : listStock) {String itemJson = MAPPER.writeValueAsString(itemStock);redisTemplate.opsForValue().set("itemStock:id:"+itemStock.getId(),itemJson);}}
}

重启项目,我们就可以看到redis中已经存在了商品数据

查询Redis缓存

启动成功并添加数据后,我们接下来去实现本地缓存查询Redis缓存。这个时候我们还需要编写lua脚本

修改common.lua脚本

-- 引入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 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
endlocal _M = {  read_http = read_http,read_redis = read_redis
}  

修改item.lua脚本

-- 导入common函数库
local common = require('common')
local cjson = require('cjson')
local read_http = common.read_http
local read_redis = common.read_redis-- 封装函数
function read_data(key,path,params)--查询Redislocal resp = read_redis('127.0.0.1',6379,key)if not resp thenngx.log("查询redis失败,key为:",key)resp = read_http(path,params)endreturn resp
end
-- 获取参数
local id = ngx.var[1]-- 查询商品信息
local itemJSON = read_data('itemInfo:id:'..id,'/item/'..id,nil)
-- 查询库存信息
local stockJSON = read_data('itemStock:id:'..id,'/item/stock/'..id,nil)
-- 反序列化JSON商品信息为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))

我们关闭tomcat服务,直接访问,测试是否是通过Redis获取到内容

Nginx本地缓存

接下来我们去实现在本地缓存中进行查询

OpenResty为Nginx提供了shard dict的功能,可以在nginx的多的worker之间共享数据,实现缓存功能。

修改CentOS中的nginx.conf文件,开启该功能。

	#开启共享字典,名字叫item_cache,缓存大小150兆lua_shared_dict item_cache 150m;

接下来修改item.lua中的read_data代码,先进行本地查询

-- 导入common函数库
local common = require("common")
local cjson = require('cjson')
local read_http = common.read_http
local read_redis = common.read_redis
-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache-- 封装函数
function read_data(key,expire,path,params)--先去查询本地缓存local val = item_cache:get(key)if not val then --查询Redisngx.log(ngx.ERR,"本地缓存不存在,去查询redis")val = read_redis("127.0.0.1",6379,key)if not val thenngx.log(ngx.ERR,"查询redis失败,key为:",key)val = read_http(path,params)endend-- 将数据写入本地缓存,并设置过期时间item_cache:set(key,val,expire)return val
end
-- 获取参数
local id = ngx.var[1]
-- 查询商品信息
local itemJSON = read_data("itemInfo:id:"..id,1800,'/item/'..id,nil)
ngx.log(ngx.ERR,"itmeJson的信息为:",itemJSON)
-- 查询库存信息
local stockJSON = read_data("itemStock:id:"..id,60,'/item/stock/'..id,nil)
-- 反序列化JSON商品信息为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))

接下来进行页面访问。第一次访问结果如下,后续再次访问不会再打印日志。说明的确是走了本地缓存

缓存同步策略

缓存数据同步的常见方式有三种:

设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新

  • 优势:简单、方便
  • 缺点:时效性差,缓存过期之前可能不一致
  • 场景:更新频率较低,时效性要求低的业务

同步双写:在修改数据库的同时,直接修改缓存

  • 优势:时效性强,缓存与数据库强一致
  • 缺点:有代码侵入,耦合度高;
  • 场景:对一致性、时效性要求较高的缓存数据

异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据

  • 优势:低耦合,可以同时通知多个缓存服务
  • 缺点:时效性一般,可能存在中间不一致状态
  • 场景:时效性要求一般,有多个服务需要同步

Canal

Canal,译意为水道/管道/沟渠,Canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。

Canal是基于MySQL的主从同步来实现的,MySQL主从同步的原理如下:

MySQL master将数据变更写入二进制日志( binary log),其中记录的数据叫做binary log events

MySQL slave将master的binary log events拷贝到它的中继日志(relay log)

MySQL slave重放relay log中事件,将数据变更反映它自己的数据。

Cansl将自己伪装成MySQL的一个节点,从而监听master的binary log变化。再将得到的变化信息传递到Canal的客户端,从而完成对其他数据库的同步。

安装和配置Canal

首先要进行文件配置。开启binlog功能

# 进入MySQL的配置文件
vi /tmp/mysql/conf/my.cnf# 添加如下内容
# binary log存放位置
log-bin=/var/lib/mysql/mysql-bin
# 指定数据库名称
binlog-do-db=heima# 添加完成后,重启Mysql
docker restart mysql

设置用户权限。接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对item这个库的操作权限。

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;

创建网络

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

docker network create item

让mysql加入这个网络:

docker network connect item mysql

安装Canal。将资料中的Canal.tar加载到虚拟机中

通过命令导入:

docker load -i canal.tar

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

docker run -p 11111:11111 --name canal \
-e canal.destinations=item \
-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=item\\..* \
--network item \
-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=:要监听的库名称

监听Canal

在项目中的pom文件中引入依赖

<dependency><groupId>top.javatool</groupId><artifactId>canal-spring-boot-starter</artifactId><version>1.2.1-RELEASE</version>
</dependency>

添加配置文件中的内容

canal:destination: item #启动时指定的容器名称server: 192.168.116.131:11111 #canal地址

编写监听器

@Component
@CanalTable("tb_item")//需要监听哪个表
public class ItemHandler implements EntryHandler<Item> {@Autowiredprivate RedisHandler redisHandler;@Autowiredprivate Cache<Long,Item> itemCache;@Overridepublic void insert(Item item) {//更新redis数据库redisHandler.saveItem(item);//更新JVM缓存itemCache.put(item.getId(),item);}@Overridepublic void update(Item before, Item after) {redisHandler.saveItem(after);itemCache.put(after.getId(),after);}@Overridepublic void delete(Item item) {redisHandler.deleteById(item.getId());itemCache.invalidate(item.getId());}
}

启动服务器,会发现控制台一直输出消息

测试能否同步缓存修改,访问localhost:8081端口,对数据进行修改。控制台打印效果如下

访问商品商品页面,也能够发现修改的数据发生了改变,并且服务器没有输出任何查询数据库的日志。

多级缓存访问流程

 

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

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

相关文章

MacOS升级后命令行出现xcrun: error: invalid active developer path报错信息

在Mac上用g编译cpp文件时&#xff0c;出现以下&#xff08;类似于工具环境问题的&#xff09;报错&#xff1a; 解决方案&#xff1a;重新安装最新版的MacOS Command Line Tools xcode-select --install重新尝试编译&#xff1a; 编译成功&#xff08;忽略这个warning&…

vue中插槽slot

一、插槽-默认插槽 1.作用 让组件内部的一些 结构 支持 自定义 2.需求 将需要多次显示的对话框,封装成一个组件 3.问题 组件的内容部分&#xff0c;不希望写死&#xff0c;希望能使用的时候自定义。怎么办 4.插槽的基本语法 组件内需要定制的结构部分&#xff0c;改用&l…

OpenCV 图像复制和图像区域读写

图像复制 共享数据, 使用 new Mat(srcMat, ...) 和 newMatsrcMat 生成新的Mat都和原Mat共享数据, 也就是说如果修改某一Mat,其他Mat也会随之改变复制全新的Mat, 使用CopyTo() 和 Clone() 方法将生成一个全新的Mat, 新Mat和原Mat不共享数据. 图像区域和点的读写 区域读取: 通过s…

线上 kafka rebalance 解决

上周末我们服务上线完毕之后发生了一个kafka相关的异常&#xff0c;线上的kafka频繁的rebalance&#xff0c;详细的报错我已经贴到下面&#xff0c;根据字面意思&#xff1a;消费者异常 org.apache.kafka.clients.consumer.CommitFailedException: 无法完成提交&#xff0c;因为…

《第三期(先导课)》之《Python 开发环境搭建》

文章目录 《第 1 节 初始Python》《第 6 节 pip包管理工具》 《第 1 节 初始Python》 。。。 《第 6 节 pip包管理工具》 pip是Python的包管理工具,用于安装、升级和管理Python包。 pip是Python标准库之外的一个第三方工具,可以从Python Package Index(PyPI)下载和安装各种P…

C4D移动坐标轴位置的技巧

我们所创建的模型&#xff0c;刚创建的时候中心的位置就是中心坐标的位置了&#xff0c;如图所示 我们可以选择一个视图模式更好的观察效果 文章源自四五设计网-https://www.45te.com/35303.html 然后将模型给C掉 这样模型变成了可以编辑的模式后&#xff0c;选择左侧的坐标选…

软件测试|selenium执行js脚本

JavaScript是运行在客户端&#xff08;浏览器&#xff09;和服务器端的脚本语言&#xff0c;允许将静态网页转换为交互式网页。可以通过 Python Selenium WebDriver 执行 JavaScript 语句&#xff0c;在Web页面中进行js交互。那么js能做的事&#xff0c;Selenium应该大部分也能…

SLAM从入门到精通(被忽视的基础图像处理)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 工业上用激光slam的多&#xff0c;用视觉slam的少&#xff0c;这是大家都知道的常识。毕竟对于工业来说&#xff0c;健壮和稳定是我们必须要考虑的…

MySQL的表格去重,史上最简便的算法,一看就会

首先&#xff0c;表格my_tab02存在很多重复的数据&#xff1a; #表格的去重 方法一&#xff1a; 详细内容传送门&#xff1a;表格的去重 -- 思路&#xff1a; -- 1.先创建一张临时表 my_tmp,该表的结构和my_tab02一样 -- 2.把my_tmp的记录通过distinct关键字 处理后 把记录复…

Spring Boot 请求/actuator/beans 无法访问 返回404

问题复现 在保证项目加入了spring-boot-starter-actuator依赖&#xff0c;并成功启动后。通过浏览器进行访问&#xff0c;返回如下图结果&#xff1a; 问题排查 1. 查看日志 从日志中可以看到基于路径’/actuator’下只暴露了一个端点 2. 访问http://localhost:8080/actua…

机器视觉opencv答题卡识别系统 计算机竞赛

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 答题卡识别系统 - opencv python 图像识别 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分…

改进YOLOv5:结合ICCV2023|动态蛇形卷积,构建不规则目标识别网络

🔥🔥🔥 提升多尺度、不规则目标检测,创新提升 🔥🔥🔥 🔥🔥🔥 捕捉图像特征和处理复杂图像特征 🔥🔥🔥 👉👉👉: 本专栏包含大量的新设计的创新想法,包含详细的代码和说明,具备有效的创新组合,可以有效应用到改进创新当中 👉👉👉: �…

C++ http协议POST body raw 字段向服务器发送请求

环境&#xff1a;ubuntu系统c使用http协议不是很方便&#xff0c;通过curl库我们可以很方便使用http协议&#xff0c;由于我的请求方式比较特殊&#xff0c;在网上没有找到相关的资料&#xff0c;之前使用python实现过一版&#xff0c;但是当设备数量超过100台时&#xff0c;程…

操作系统 day08(进程通信)

进程通信的概念 进程间通信是指两个进程之间产生数据交互进程通信需要操作系统的支持&#xff0c;由于进程是分配系统资源&#xff08;包括内存地址&#xff09;的单位&#xff0c;因此各进程拥有的内存地址空间相互独立。同时为了保证安全&#xff0c;一个进程不能直接访问另…

Android Glide transform旋转rotate圆图CircleCrop,Kotlin

Android Glide transform旋转rotate圆图CircleCrop&#xff0c;Kotlin import android.graphics.Bitmap import android.os.Bundle import android.util.Log import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import com.bumptech.glide.load…

vue 子页面通过暴露属性,实现主页面的某事件的触发

目录 1.前言2.代码2-1 子页面2-2 主页面 1.前言 需求&#xff1a;当我在子页面定义了一个定时器&#xff0c;点击获取验证码&#xff0c;计时器开始倒计时&#xff0c;在这个定时器没有走完&#xff0c;退出关闭子页面&#xff0c;再次进入子页面&#xff0c;定时器此时会被刷…

代码随想录 Day41 动态规划09 LeetCode T121 买卖股票的最佳时机 T122 买卖股票的最佳时机II

前言 这两题看起来是不是有点眼熟,其实我们在贪心章节就已经写过了这两道题,当时我们用的是将利润分解,使得我们始终得到的是最大利润 假如第 0 天买入&#xff0c;第 3 天卖出&#xff0c;那么利润为&#xff1a;prices[3] - prices[0]。 相当于(prices[3] - prices[2]) (pri…

自动化实战 - 测试个人博客系统

前言 本篇使用Selenium3Junit5对个人博客进行自动化测试&#xff0c;如有错误&#xff0c;请在评论区指正&#xff0c;让我们一起交流&#xff0c;共同进步&#xff01; 文章目录 前言一.web自动化测试用例二.测试准备1.注册界面自动化测试测试过程中遇到的Bug: 2.登录界面自动…

【云备份|| 日志 day5】文件热点管理模块

云备份day5 热点管理模块 热点管理模块 服务器端的热点文件管理是对上传的非热点文件进行压缩存储&#xff0c;节省磁盘空间。 而热点文件的判断在于上传的文件的最后一次访问时间是否在热点判断时间之内&#xff0c;比如如果一个文件一天都没有被访问过我们就认为这是一个非…

采集Prestashop独立站

这是一个用Lua编写的爬虫程序&#xff0c;用于采集Prestashop独立站的内容。爬虫程序使用代理信息&#xff1a;proxy_host: jshk.com.cn。 -- 首先&#xff0c;我们需要导入所需的库 local http require(socket.http) local url require(socket.url)-- 然后&#xff0c;我们…