多级缓存:亿级流量的缓存方案
文章目录
- 一.多级缓存的引入
- 二.JVM进程缓存
- 三.Lua语法入门
- 四.多级缓存
- 1.OpenResty
- 2.查询Tomcat
- 3.Redis缓存预热
- 4.查询Redis缓存
- 5.Nginx本地缓存
- 6.缓存同步
一.多级缓存的引入
传统缓存的问题
传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库,存在下面的问题:
-
请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈
-
Redis缓存失效时,会对数据库产生冲击
多级缓存方案
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能:
用作缓存的Nginx是业务Nginx,需要部署为集群,再有专门的Nginx用来做反向代理:
二.JVM进程缓存
本地进程缓存
缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:
-
分布式缓存,例如Redis:(已经学习过了)
-
优点:存储容量更大、可靠性更好、可以在集群间共享
-
缺点:访问缓存有网络开销
-
场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
-
-
进程本地缓存,例如HashMap、GuavaCache:
-
优点:读取本地内存,没有网络开销,速度更快
-
缺点:存储容量有限、可靠性较低、无法共享
-
场景:性能要求较高,缓存数据量较小
-
Caffeine
Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址:https://github.com/ben-manes/caffeine
Caffeine的读写速度:
Caffeine示例
可以通过item-service项目中的单元测试来学习Caffeine的使用:
@Test
void testBasicOps() {// 创建缓存对象Cache<String, String> cache = Caffeine.newBuilder().build();// 存数据cache.put("gf", "迪丽热巴");// 取数据,不存在则返回nullString gf = cache.getIfPresent("gf");System.out.println("gf = " + gf); // 取数据,不存在则去数据库查询String defaultGF = cache.get("defaultGF", key -> {// 这里可以去数据库根据 key查询value return "柳岩";});System.out.println("defaultGF = " + defaultGF);
}
Caffeine提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载。(可以在官网查看到)
推荐使用 cache.get(key, k -> value) 操作来在缓存中不存在该key对应的缓存元素的时候进行计算生成(可以理解为直接查询mysql数据库等操作,当然,查询操作要写在get方法的第二个匿名内部类中)并直接写入至缓存内,而当该key对应的缓存元素存在的时候将会直接返回存在的缓存值。一次 cache.put(key, value) 操作将会直接写入或者更新缓存里的缓存元素,在缓存中已经存在的该key对应缓存值都会直接被覆盖。值得注意的是,当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get 也许会返回 null 。
当然,也可以使用Cache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作。
Caffeine提供了三种缓存驱逐策略:
1.基于容量:设置缓存的数量上限
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder().maximumSize(1) // 设置缓存大小上限为 1 .build();
2.基于时间:设置缓存的有效时间
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(10)) // 设置缓存有效期为 10 秒,从最后一次写入开始计时 .build();
3.基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。
在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。
案例:实现进程缓存
针对实现商品的查询的本地进程缓存
利用Caffeine实现下列需求:
-
给根据id查询商品的业务添加缓存,缓存未命中时查询数据库
-
给根据id查询商品库存的业务添加缓存,缓存未命中时查询数据库
-
缓存初始大小为100
-
缓存上限为10000
实现步骤:
1.初始化Bean:
@Configuration
public class CaffeineConfig {@Beanpublic Cache<Long, Item> itemCache(){return Caffeine.newBuilder().initialCapacity(100).maximumSize(10_000).build();}@Beanpublic Cache<Long, ItemStock> stockCache(){return Caffeine.newBuilder().initialCapacity(100).maximumSize(10_000).build();}
}
2.修改业务(查询)代码
@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 stockCache.get(id, key -> stockService.getById(key));}
三.Lua语法入门
初识Lua
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。官网:https://www.lua.org/
变量和循环
数据类型
其中,table数据类型可以是数组或者map集合,这取决于是如何声明的
可以利用type函数测试给定变量或者值的类型:
变量
Lua声明变量的时候,并不需要指定数据类型(很类似js):
访问table(下标从开始,不是从0开始):
循环
数组、table都可以利用for循环来遍历:
-
遍历数组:
-
遍历table:
条件控制、函数
函数
定义函数的语法:
例如,定义一个函数,用来打印数组:
条件控制
类似Java的条件控制,例如if、else语法:
与java不同,布尔表达式中的逻辑运算是基于英文单词:
四.多级缓存
1.OpenResty
初识OpenResty
OpenResty® 是一个基于 Nginx的高性能 Web 平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点:
-
具备Nginx的完整功能
-
基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块
-
允许使用Lua自定义业务逻辑、自定义库
官方网站:https://openresty.org/cn/
简单来说:OpenResty的目标是通过将Lua脚本语言嵌入Nginx中,使开发人员能够使用Lua来扩展Nginx的功能。
OpenResty快速入门
案例:实现商品详情页数据查询
商品详情页面目前展示的是假数据,在浏览器的控制台可以看到查询商品信息的请求:
而这个请求最终被反向代理到虚拟机的OpenResty集群:
需求:在OpenResty中接收这个请求,并返回一段商品的假数据。
实现步骤:
1.修改nginx.conf文件:
在nginx.conf的http下面,添加对OpenResty的Lua模块的加载:
# 加载lua 模块 lua_package_path "/usr/local/openresty/lualib/?.lua;;"; # 加载c模块 lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
这两行代码主要是引入了Lua模块和Lua C模块,方便后面引入API来处理HTTP请求等
2.在nginx.conf的server下面,添加对/api/item这个路径的监听:
location /api/item {# 响应类型,这里返回jsondefault_type application/json;# 响应数据由 lua/item.lua这个文件来决定content_by_lua_file lua/item.lua;}
这段代码可以理解为SpringMVC框架中的表现层(Controller),监听一个"/api/item"路径,具体的响应数据为json,具体的业务逻辑代码在"item.lua"中编写(类似于业务层Service)
3.编写item.lua文件
-- 返回假数据,这里的ngx.say()函数,就是写数据到Response中
ngx.say('{"id":10001,"name":"SALSA AIR}')
这里先使用假数据,后面讲解动态返回数据
请求参数处理
OpenResty提供了各种API用来获取不同类型的请求参数:
案例:获取请求路径中的商品id信息,拼接到json结果中返回
在查询商品信息的请求中,通过路径占位符的方式,传递了商品id到后台:
需求:在OpenResty中接收这个请求,并获取路径中的id信息,拼接到结果的json字符串中返回
实现步骤:
1.在OpenResty的Nginx下的配置文件编写请求变量拦截
location ~ /api/item/(\d+) {# 相应类型,这里返回jsondefault_type application/json;# 响应数据由 lua/item.lua这个文件来决定content_by_lua_file lua/item.lua;}
2.在item.lua文件获取id变量并进行拼接返回
local id = ngx.var[1]ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 26寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":18888,"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}')
2.查询Tomcat
实现多级缓存的步骤是,当OpenResty集群没有缓存时,查询Redis缓存,当Redis缓存未命中访问Tomcat进程缓存,进程缓存未命中时通知Tomcat服务器查询数据库返回数据并存入数据到OpenResty作为缓存,这里先忽略访问缓存,实现OpenResty请求访问Tomcat服务器查询数据
需要说明的是:当查询未命中多级缓存中任意一级时,当前所在的级就必须查询其他级(前一级只会查询后面的一级,因为前面的级包含的缓存数据都没有命中)来补充所需的缓存数据
案例:获取请求路径中的商品id信息,根据id向Tomcat查询商品信息
这里要修改item.lua,满足下面的需求(步骤):
1.获取请求参数中的id
2.根据id向Tomcat服务发送请求,查询商品信息
3.根据id向Tomcat服务发送请求,查询库存信息
4.组装商品信息、库存信息,序列化为JSON格式并返回
前置知识1:nginx内部发送Http请求
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和端口。这个请求会被nginx内部的server监听并处理。
但是我们希望这个请求发送到Tomcat服务器,所以还需要编写一个server来对这个路径做反向代理:
location /path {# 这里是windows电脑的ip和Java服务端口,需要确保windows防火墙处于关闭状态proxy_pass http://192.168.150.1:8081; }
案例实现在配置文件编写监听/item路径即可
前置知识2:封装http查询函数
由于和Tomcat通信在一个新项目是十分频繁的过程,这里可以封装http查询的函数,放到OpenResty函数库中,方便后期使用。
步骤:
1.在/usr/local/openresty/lualib目录下创建common.lua文件:
vi /usr/local/openresty/lualib/common.lua
2.在common.lua中封装http查询的函数
-- 封装函数,发送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
这里指定在/usr/local/openresty/lualib目录是因为在OpenResty下的Nginx配置文件中添加了对模块的加载,接下来就可以直接使用这个库
# 加载lua 模块 lua_package_path "/usr/local/openresty/lualib/?.lua;;"; # 加载c模块 lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
实现案例:
1.修改item.lua文件
-- 引入自定义工具模块
local common = require("common")
local read_http = common.read_http-- 获取路径参数
local id = ngx.var[1]-- 根据id查询商品
local itemJSON = read_http("/item/".. id, nil)
-- 根据id查询商品库存
local itemStockJSON = read_http("/item/stock/".. id, nil)
查询到的是商品、库存的json格式数据,我们需要将两部分数据组装,需要用到JSON处理函数库。
2.拼接JSON数据并进行返回
JSON结果处理
OpenResty提供了一个cjson模块用来处理JSON的序列化和反序列化。
官方地址:https://github.com/openresty/lua-cjson/
步骤主要分为以下两个:
1.引入cjson模块:
local cjson = require "cjson"
2.(1)序列化:
local obj = {name = 'jack',age = 21
}
local json = cjson.encode(obj)
2.(2)反序列化:
local json = '{"name": "jack", "age": 21}'
-- 反序列化
local obj = cjson.decode(json);
print(obj.name)
简单来说:序列化就是encode,反序列化就是decode
了解JSON序列化和反序列化就能进行结果拼接返回了:
-- 反序列化JSON数据/JSON转换成lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(itemStockJSON)-- 组合数据
item.stock = stock.stock
item.sold = stock.sold-- 把item序列化为json且返回数据
ngx.say(cjson.encode(item))
访问id为10002返回的数据(记得关闭防火墙):
总结案例:
当一个url为http://localhost/item.html?id=10002访问时,执行的大致操作:
1.前端解析url拼接为http://localhost/api/item/10002
2.windows上的nginx服务器监控到/api的请求后,反向代理到OpenResty集群,并做了负载均衡到具体的ip+端口
3.虚拟机上的OpenResty服务器监测到/api/item/(\d+)的请求,执行item.lua内的业务逻辑
4.item.lua内使用函数接收到请求和路径变量id,请求路径/item/id或/item/stock/id并被自身监听到,反向代理url到windows上的Tomcat服务器并被controller监测到,执行查询业务返回JSON数据到OpenResty服务器,服务器使用resp变量接收到返回数据,并组合数据返回到页面
需要注意的是:导入模块使用的函数为require;操作序列化反序列化JSON数据使用的模块为cjson
Tomcat集群的负载均衡
在实际业务中,OpenResty服务器请求Tomcat服务器,Tomcat服务器肯定是一个集群,这就涉及到了请求的分散(负载均衡)来减轻单个Tomcat服务器的压力,在OpenResty服务器请求Tomcat服务器时,Tomcat服务器不仅会查询数据返回给OpenResty服务器,还会保存查询数据到进程缓存中
但是,如果OpenResty服务器使用的是轮询的方式访问Tomcat集群,这就导致了如果是同一个id请求服务器,每一台Tomcat服务器都必须要查询数据库,进程缓存就显得没有用了(直到每一个Tomcat服务器都查询数据库才都有这个id的进程缓存,进程缓存才开始生效,这不是我们希望看到的)所以OpenResty服务器的负载均衡策略必须改变
这里采用的hash负载均衡策略(hash $request_uri),这种策略会对id进行hash运算,得到一个具体值会访问一台具体的Tomcat服务器,如果是同一个id,就会hash到同一个结果访问到同一台Tomcat服务器,从而导致请求会利用到线程缓存
3.Redis缓存预热
在多级缓存中,当OpenResty服务器中查询不到缓存时,应该查询下一级缓存即Redis缓存
冷启动与缓存预热
冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。
缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。
案例实现步骤:
1.利用Docker安装Redis
docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes
2.在item-service服务中引入Redis依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
3.配置Redis地址(直接配置即可)
4.编写初始化类
@Component
public class RedisHandler implements InitializingBean {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Autowiredprivate IItemService itemService;@Autowiredprivate IItemStockService itemStockService;private static final ObjectMapper MAPPER=new ObjectMapper();/*** 这个方法会在这个类注册成bean后执行** @throws Exception*/@Overridepublic void afterPropertiesSet() throws Exception {//查询数据库数据List<Item> itemList = itemService.list();List<ItemStock> itemStockList = itemStockService.list();//保存到redis中for (Item item : itemList) {//转换数据为JSON对象String jsonItem =MAPPER.writeValueAsString(item);redisTemplate.opsForValue().set("item:id:" + item.getId(), jsonItem);}for (ItemStock itemStock : itemStockList) {//转换数据为JSON对象String jsonItemStock =MAPPER.writeValueAsString(itemStock);redisTemplate.opsForValue().set("item:stock:id:" + itemStock.getId(), jsonItemStock);}}
}
需要注意的有三点:
1.缓存预热使用到接口类为InitializingBean
2.实现接口类中的方法后,这个方法会在整个RedisHandler注册成为bean实例后执行
3.可以用Spring原始的序列化工具ObjectMapper来将对象转换为JSON字符串(调用方法writeValueAsString)
4.查询Redis缓存
OpenResty的Redis模块
OpenResty提供了操作Redis的模块,我们只要引入该模块就能直接使用:
- 引入Redis模块,并初始化Redis对象
-- 引入redis模块
local redis = require("resty.redis")
-- 初始化redis对象
local red = redis:new()
-- 设置redis超时时间
red:set_timeouts(1000,1000,1000)
- 封装函数,用来释放Redis连接,其实是放入连接池
-- 关闭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读数据并返回
-- 查询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
别忘了对外暴露调用函数:
local _M = { read_http = read_http,read_redis = read_redis
}
案例:查询商品时,优先Redis缓存查询
需求:
-
修改item.lua,封装一个函数read_data,实现先查询Redis,如果未命中,再查询tomcat
-
修改item.lua,查询商品和库存时都调用read_data这个函数
实现代码:
-- 引入cjson的模块用来处理JSON的序列化和反序列化
local cjson = require("cjson")
-- 引入自定义工具模块
local common = require("common")-- 获取模块中的函数
local read_http = common.read_http
local read_redis = common.read_redis-- 定义查询函数
local function read_data(key,path,params)-- 查询redislocal resp = read_redis("127.0.0.1",6379,key)-- 判断redis是否命中if not resp then-- redis查询失败,查询httpresp = read_http(path,params)endreturn resp
end-- 获取路径参数
local id = ngx.var[1]-- 根据id查询商品
local itemJSON = read_data("item:id:"..id, "/item/"..id, nil)-- 根据id查询商品库存
local itemStockJSON = read_data("item:stock:id:"..id, "/item/stock/"..id, nil)-- 反序列化JSON数据/JSON转换成lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(itemStockJSON)-- 组合数据
item.stock = stock.stock
item.sold = stock.sold-- 把item序列化为json且返回数据
ngx.say(cjson.encode(item))
需要解释的是:对于其中的代码:
-- 查询redislocal resp = read_redis("127.0.0.1",6379,key)
填写127.0.0.1是因为OpenResty服务器和Redis服务器都存在一台虚拟机上,使用回环地址即可访问
5.Nginx本地缓存
OpenResty为Nginx提供了shard dict的功能,可以在一台nginx服务器的多个worker之间共享数据,实现缓存功能。
- 开启共享字典,在nginx.conf的http下添加配置:
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m;
- 操作共享字典:
-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 存储, 指定key、value、过期时间,单位s,默认为0代表永不过期
item_cache:set('key', 'value', 1000)
-- 读取
local val = item_cache:get('key')
案例:在查询商品时,优先查询OpenResty的本地缓存
需求:
-
修改item.lua中的read_data函数,优先查询本地缓存,未命中时再查询Redis、Tomcat
-
查询Redis或Tomcat成功后,将数据写入本地缓存,并设置有效期
-
商品基本信息,有效期30分钟
-
库存信息,有效期1分钟
实现代码:
1.先在nginx.conf配置文件添加共享词典配置
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150mlua_shared_dict item_cache 150m;
2.编写业务逻辑(.lua文件中)
-- 引入cjson的模块用来处理JSON的序列化和反序列化
local cjson = require("cjson")
-- 引入自定义工具模块
local common = require("common")-- 获取模块中的函数
local read_http = common.read_http
local read_redis = common.read_redis-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache-- 定义查询函数
local function read_data(key,path,params,validTime)-- 先查询本地缓存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)-- 判断redis是否命中if not val thenngx.log(ngx.ERR, "redis查询失败,尝试查询http, key:", key)-- redis查询失败,查询httpval = read_http(path,params)endenditem_cache:set(key,val,validTime)return val
end-- 获取路径参数
local id = ngx.var[1]-- 根据id查询商品
local itemJSON = read_data("item:id:"..id, "/item/"..id, nil, 1800)-- 根据id查询商品库存
local itemStockJSON = read_data("item:stock:id:"..id, "/item/stock/"..id, nil, 60)-- 反序列化JSON数据/JSON转换成lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(itemStockJSON)-- 组合数据
item.stock = stock.stock
item.sold = stock.sold-- 把item序列化为json且返回数据
ngx.say(cjson.encode(item))
6.缓存同步
缓存同步策略
缓存数据同步的常见方式有三种:
-
设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新
-
优势:简单、方便
-
缺点:时效性差,缓存过期之前可能不一致
-
场景:更新频率较低,时效性要求低的业务
-
-
同步双写:在修改数据库的同时,直接修改缓存
-
优势:时效性强,缓存与数据库强一致
-
缺点:有代码侵入,耦合度高;
-
场景:对一致性、时效性要求较高的缓存数据
-
-
异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
-
优势:低耦合,可以同时通知多个缓存服务
-
缺点:时效性一般,可能存在中间不一致状态
-
场景:时效性要求一般,有多个服务需要同步
-
对于异步通知,有以下两种:
1.基于MQ的异步通知:
2.基于Canal的异步通知:
初识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的客户端,进而完成对其它数据库的同步。
对于安装Canal需要注意的是:1.首先开启mysql的主从 2.安装canal,并使cancl和mysql处于同一网络下
监听Canal
Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。
Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。不过这里我们会使用GitHub上的第三方开源的canal-starter。(操作Canal会更加方便)地址:链接: https://github.com/NormanGyllenhaal/canal-client
实现步骤:
引入依赖:
<dependency><groupId>top.javatool</groupId><artifactId>canal-spring-boot-starter</artifactId><version>1.2.1-RELEASE</version></dependency>
编写配置:
canal:destination: heima # canal实例名称,要跟canal-server运行时设置的destination一致server: 192.168.109.130:11111 # canal地址
编写监听器,监听Canal消息:
这里就使用案例来演示了:
package com.heima.item.canal;import com.github.benmanes.caffeine.cache.Cache;
import com.heima.item.config.RedisHandler;
import com.heima.item.pojo.Item;
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) {itemCache.put(item.getId(), item);redisHandler.saveItem(item);}@Overridepublic void update(Item before, Item after) {itemCache.put(after.getId(), after);redisHandler.saveItem(after);}@Overridepublic void delete(Item item) {itemCache.invalidate(item.getId());redisHandler.deleteItemById(item.getId());}
}
具体来说:
Canal推送给canal-client的是被修改的这一行数据(row),而我们引入的canal-client则会帮我们把行数据封装到Item实体类中。这个过程中需要知道数据库与实体的映射关系,要用到JPA的几个注解:
package com.heima.item.pojo;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;import java.util.Date;@Data
@TableName("tb_item")
public class Item {@TableId(type = IdType.AUTO)@Idprivate Long id;//商品idprivate 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)@Transientprivate Integer stock;@TableField(exist = false)@Transientprivate Integer sold;
}
具体来说:
到此为止,可以实现mysql数据库修改,同步对redis的数据进行变更,而对于OpenResty内的数据,推荐缓存不是更新速度很频繁的数据并且使用了设置过期时间进行数据更新
相关文章:
多级缓存:亿级流量的缓存方案
文章目录 一.多级缓存的引入二.JVM进程缓存三.Lua语法入门四.多级缓存1.OpenResty2.查询Tomcat3.Redis缓存预热4.查询Redis缓存5.Nginx本地缓存6.缓存同步 一.多级缓存的引入 传统缓存的问题 传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未…...
C语言——高精度乘法
一、引子 高精度乘法相较于高精度加法和减法有更多的不同,加法和减法是一位对应一位进行操作的,而乘法是一个数的每一位对另一个数的每一位进行操作,需要的计算步骤更多。 二、核心算法 void Calculate(int num1[], int num2[], int numres…...
为什么C语言没有被C++所取代呢?
今日话题,为什么C语言没有被C所取代呢?虽然C是一个功能更强大的语言,但C语言在嵌入式领域仍然广泛使用,因为它更轻量级、更具可移植性,并且更适合在资源受限的环境中工作。这就是为什么C语言没有被C所取代的原因。如果…...
基于Spring的枚举类+策略模式设计(以实现多种第三方支付功能为例)
摘要 最近阅读《贯彻设计模式》这本书,里面使用一个更真实的项目来介绍设计模式的使用,相较于其它那些只会以披萨、厨师为例的设计模式书籍是有些进步。但这书有时候为了使用设计模式而强行朝着对应的 UML 图来设计类结构,并且对设计理念缺少…...
基于Linphone android sdk开发Android软话机
1.Linphone简介 1.1 简介 LinPhone是一个遵循GPL协议的开源网络电话或者IP语音电话(VOIP)系统,其主要如下。使用linphone,开发者可以在互联网上随意的通信,包括语音、视频、即时文本消息。linphone使用SIP协议&#…...
[论文分享]TimeDRL:多元时间序列的解纠缠表示学习
论文题目:TimeDRL: Disentangled Representation Learning for Multivariate Time-Series 论文地址:https://arxiv.org/abs/2312.04142 代码地址:暂无 关键要点:多元时间序列,自监督表征学习,分类和预测 摘…...
分享一个好看的vs主题
最近发现了一个很好看的vs主题(个人认为挺好看的),想要分享给大家。 主题的名字叫NightOwl,和vscode的主题颜色挺像的。操作方法也十分简单,首先我们先在最上面哪一行找到扩展。 然后点击管理扩展,再搜索栏…...
什么是云呼叫中心?
云呼叫中心作为一种高效的企业呼叫管理方案,越来越受到企业的青睐,常被用于管理客服和销售业务。那么,云呼叫中心到底是什么? 什么是云呼叫中心? 云呼叫中心是一种基于互联网的呼叫管理系统,与传统的呼叫…...
还在用nvm?来试试更快的node版本管理工具——fnm
前言 📫 大家好,我是南木元元,热衷分享有趣实用的文章,希望大家多多支持,一起进步! 🍅 个人主页:南木元元 目录 什么是node版本管理 常见的node版本管理工具 fnm是什么 安装fnm …...
【Hadoop精讲】HDFS详解
目录 理论知识点 角色功能 元数据持久化 安全模式 SecondaryNameNode(SNN) 副本放置策略 HDFS写流程 HDFS读流程 HA高可用 CPA原则 Paxos算法 HA解决方案 HDFS-Fedration解决方案(联邦机制) 理论知识点 角色功能 元数据持久化 另一台机器就…...
企业需要哪些数字化管理系统?
企业需要哪些数字化管理系统? ✅企业引进管理系统肯定是为了帮助整合和管理大量的数据,从而优化业务流程,提高工作效率和生产力。 ❌但是,如果各个系统之间不互通、无法互相关联数据的话,反而会增加工作量和时间成本…...
【vue】开发常见问题及解决方案
有一些问题不限于 Vue,还适应于其他类型的 SPA 项目。 1. 页面权限控制和登陆验证页面权限控制 页面权限控制是什么意思呢? 就是一个网站有不同的角色,比如管理员和普通用户,要求不同的角色能访问的页面是不一样的。如果一个页…...
飞天使-k8s知识点3-卸载yum 安装的k8s
要彻底卸载使用yum安装的 Kubernetes 集群,您可以按照以下步骤进行操作: 停止 Kubernetes 服务: sudo systemctl stop kubelet sudo systemctl stop docker 卸载 Kubernetes 组件: sudo yum remove -y kubelet kubeadm kubectl…...
ZooKeeper 集群搭建
文章目录 ZooKeeper 概述选举机制搭建前准备分布式配置分布式安装解压缩并重命名配置环境配置服务器编号配置文件 操作集群编写脚本运行脚本搭建过程中常见错误 ZooKeeper 概述 Zookeeper 是一个开源的分布式服务协调框架,由Apache软件基金会开发和维护。以下是对Z…...
Meson:现代的构建系统
Meson是一款现代化、高性能的开源构建系统,旨在提供简单、快速和可读性强的构建脚本。Meson被设计为跨平台的,支持多种编程语言,包括C、C、Fortran、Python等。其目标是替代传统的构建工具,如Autotools和CMake,提供更简…...
【大模型AIGC系列课程 5-2】视觉-语言大模型原理
重磅推荐专栏: 《大模型AIGC》;《课程大纲》 本专栏致力于探索和讨论当今最前沿的技术趋势和应用领域,包括但不限于ChatGPT和Stable Diffusion等。我们将深入研究大型模型的开发和应用,以及与之相关的人工智能生成内容(AIGC)技术。通过深入的技术解析和实践经验分享,旨在…...
震惊!难怪别人家的孩子越来越聪明,原来竟是因为它
前段时间工作调动给孩子换了个新学校,刚开始担心她不能适应新学校的授课方式,但任课老师对她评价很高,夸她上课很专注。 为了训练孩子的专注力,作为家长可没少下功夫,画画,下五子棋等益智游戏的兴趣班没少…...
Linux操作系统(UMASK+SUID+SGID+STICK)
UMASK反掩码 如何查看反掩码:直接在终端窗口运行 umask root用户反掩码:0022 普通用户反掩码:0002 UMASK的作用:确定目录,文件的缺省权限值 以root身份创建目录,观察目录的9位权限值 以root身份创建普通文件…...
Java 中单例模式的常见实现方式
目录 一、什么是单例模式? 二、单例模式有什么作用? 三、常见的创建单例模式的方式 1、饿汉式创建 2、懒汉式创建 3、DCL(Double Checked Lock)双检锁方式创建 3.1、synchronized 同步锁的基本使用 3.2、使用 DCL 中存在的疑…...
【C语言】自定义类型之联合和枚举
目录 1. 前言2. 联合体2.1 联合体类型的声明2.2 联合体的特点2.3 相同成员的结构体和联合体对比2.4 联合体大小的计算2.4 判断当前机器的大小端 3. 枚举3.1 枚举类型的声明3.2 枚举类型的优点3.3 枚举类型的使用 1. 前言 在之前的博客中介绍了自定义类型中的结构体,…...
使用Mosquitto/python3进行MQTT连接
一、简介 MQTT(消息队列遥测传输)是ISO 标准(ISO/IEC PRF 20922)下基于发布/订阅范式的消息协议。它工作在 TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议,为此,它需要一个消息中间件。 …...
JavaWeb笔记之前端开发HTML
一、引言 1.1HTML概念 网页,是网站中的一个页面,通常是网页是构成网站的基本元素,是承载各种网站应用的平台。通俗的说,网站就是由网页组成的。通常我们看到的网页都是以htm或html后缀结尾的文件,俗称 HTML文件。 …...
通过IP地址定位解决被薅羊毛问题
随着互联网的普及,线上交易和优惠活动日益增多,这也为一些不法分子提供了可乘之机。他们利用技术手段,通过大量注册账号或使用虚假IP地址进行异常操作,以获取更多的优惠或利益,这种行为被称为“薅羊毛”。对于企业和平…...
Leetcode 122 买卖股票的最佳时机 II
题意理解: 已知:一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格 如何哪个时间点买入,哪个时间点卖出,多次交易,能够收益最大化 目的:收益最大化 解题思路: 使用贪心…...
音频文件合成
音频文件合成 音频文件合成 http://ffmpeg.org/download.html https://blog.csdn.net/u013314786/article/details/89682800 http://www.360doc.com/content/19/0317/01/10519289_822112563.shtml https://chaijunkun.blog.csdn.net/article/details/116491526?spm1001.210…...
20231220将NanoPC-T4(RK3399)开发板的Android10的SDK按照Rockchip官方挖掘机开发板编译打包刷机之后启动跑飞
20231220将NanoPC-T4(RK3399)开发板的Android10的SDK按照Rockchip官方挖掘机开发板编译打包刷机之后启动跑飞 2023/12/20 17:19 简略步骤:rootrootrootroot-X99-Turbo:~/3TB$ tar --use-compress-programpigz -xvpf rk3399-android-10.git-20210201.tgz rootrootro…...
vivo 容器平台资源运营实践
作者:vivo 互联网服务器团队 - Chen Han 容器平台针对业务资源申请值偏大的运营问题,通过静态超卖和动态超卖两种技术方案,使业务资源申请值趋于合理化,提高平台资源装箱率和资源利用率。 一、背景 在Kubernetes中,容…...
ASP.NET Core面试题之Redis高频问题
🎈🎈在.NET后端开发岗位中,如今也少不了、微服务、分布式、高并发高可用相关的面试题🎈🎈 👍👍本文分享一些整理的Redis高频面试题🎉 👍👍机会都是给有准备…...
【教程】Ubuntu基本软件安装
文章目录 一、搜狗输入法安装二、百度网盘安装三、划词翻译 一、搜狗输入法安装 全网最准确的Ubuntu 20.04 安装搜狗输入法的步骤 二、百度网盘安装 百度云盘for Linux安装教程和体验 三、划词翻译 ubuntu最好用的划词翻译词典:有道词典和GoldenDict...
Jenkins 构建环境指南
目录 Delete workspace before build starts(常用) Use secret text(s) or file(s) (常用) Add timestamps to the Console Output (常用) Inspect build log for published build scans Terminate a …...
flask做大型网站开发/自助建站的优势
1、多选题: 可以运行R语言的操作系统包括( ): 选项: A:Linux B:Windows C:Mac OS D:Android 答案: 【Linux;Windows;Mac OS 】 2、单选题: ( )可以命令将R语言升级到最新版本。…...
大型网站故障/吉林seo基础知识
Elasticsearch的删除也是很灵活的,下次我再介绍,DeleteByQuery的方式。今天就先介绍一个根据ID删除。上代码。 package com.sojson.core.elasticsearch.manager; import org.elasticsearch.action.delete.DeleteResponse; import com.sojson.common.mo…...
网络运维网站/软文推广是什么
多家市调机构近日预估二季度印度智能手机市场发生重大变化,中国手机企业的市场份额或出现一定幅度的下滑,而三星和苹果这两家手机企业可望趁机抢夺市场。一季度的数据显示,中国手机企业在印度市场已取得绝对优势,市调机构Canalys公…...
建设银行内部网站/百度搜索排名
AFNetworking和ASIHTTPRequest,大致如下: 使用上:AFN是用上较ASI略简单,但扩展不如ASI;AFN能按普通的block写法直接用闭包的写法,但是ASI不行,这样ASI的代码就比较松散; 效率:AFN基于…...
个人网站做博客还是做论坛/百度seo分析工具
注:本作品曾获高校优秀论文奖,时隔4年后,现由gogogan授权WWW.ITPASSBIBLO.COM 独家发行,如有媒体转载请保留出处。中文摘要<?xml:namespace prefix o ns "urn:schemas-microsoft-com:office:office" />随着INT…...
成品超市网站/seo排名查询软件
我们打开昨天已经创建好的项目myself.pro.注意保存的项目路径不能含有中文.接下来我们继续来学习,看如何进行可视化编程. 双点你的工程管理窗口中界面文件mainwindow.ui.双点后如图所示. 控件组窗口包含所有控件(控件就是一个叫法而已,例如按钮,文本框,标签等都被称为控件),使…...