通过Caffeine实现JVM进程缓存、配置OpenResty完成nginx的本地缓存和redis操作,Canal实现缓存同步——配置多级缓存,一篇足矣

news2024/10/19 6:24:50

目录

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
  • 优点:存储容量更大、可靠性更好、可以在集群间共享
  • 缺点:访问缓存有网络开销
  • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
  • 进程本地缓存,例如HashMapGuavaCache
  • 优点:读取本地内存,没有网络开销,速度更快
  • 缺点:存储容量有限、可靠性较低、无法共享

场景:性能要求较高,缓存数据量较小

这里小编将通过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的基础实现:

  1. 小编将通过在windows中本地部署Nginx进行反向代理将前端的页面访问请求发送到Linux系统中并指定对应端口
  2. 再在linux系统中部署OpenResty服务进行监听指定端口,拦截发过来的访问请求,如果请求是查询请求,则返回OpenResty中Nginx的本地缓存
  3. 如果Nginx的本地缓存不存在对应数据,则编写lua代码和调用OpenResty提供的方法库对Redis进行数据查询操作,如果查询到数据,将数据保存到本地缓存中,并将数据返回
  4. 如果redis也没有对应缓存数据,则通过OpenResty的nginx中编写代码发起向tomcat服务端发起访问请求并由自己进行监听并发送
  5. 然后查询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节点,从而监听masterbinary 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
     */
    @Bean
    public Cache<Long, Item> itemCache() {
        return Caffeine.newBuilder()
                .maximumSize(10000)
                .initialCapacity(100)
                .build();
    }
    /**
     * 定义商品库存的本地缓存,初始容量为100,最大容量为10000
     * @return
     */
    @Bean
    public Cache<Long, ItemStock> itemStockCache() {
        return Caffeine.newBuilder().maximumSize(10000).build();
    }
}

1.2通过声明的Bean对象实现进程缓存

首先,注入声明的Cache的bean对象

    @Autowired
    private Cache<Long, Item> itemCache;
    @Autowired
    private 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]

请求头

id1001

-- 获取请求头,返回值是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.confhttp下面,添加对OpenRestyLua模块的加载,这些数据库是为了之后操作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))
        --查询redis
        resp = read_redis("127.0.0.1",6380,key)
        --判断查询结果
        if not resp then
            ngx.log(ngx.ERR, "redis 查询失败,尝试查询服务端获取数据, key:", (key or nil))
            --redis 查询失败,尝试查询服务端获取数据
            resp = read_http(path, params)
        end
        --查询成功,把数据写入本地缓存中
        item_cache:set(key, resp, expire)
    end
    
    return 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 then
        ngx.log(ngx.ERR, "放入redis连接池失败: ", err)
    end
end
--读取Redis数据的API
-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, key)
    --初始化redis
    local red = redis:new()
    --设置超时时间,建立连接时间,发送请求时间,等待响应时间
    red:set_timeout(1000,1000,1000)
    -- 获取一个连接
    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

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 {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private ItemService itemService;

    @Autowired
    private ItemStockService itemStockService;

    //JSON处理工具
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public 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: cache
  server: 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> {

    @Autowired
    private RedisHandler redisHandler;
    @Autowired
    private Cache<Long, Item> 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) {
        //删除本地缓存数据
        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容器,就解决问题了!

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

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

相关文章

Android 13.0 Launcher3定制之首页时钟小部件字体大小修改

1.前言 在13.0的系统rom产品开发中,在一些Launcher3的定制化开发中,在对于一些小屏幕的产品开发中,在首页添加时钟小部件会显得字体有点小, 所以为了整体布局美观就需要改动小部件的布局日期字体的大小来实现整体的布局美观效果,接下来来具体实现相关的功能 具体效果图: …

linux 修改主机名和用户名颜色

编译 ~/.bashrc vim ~/.bashrc 如下格式 PS1\[\e[1;31m\]\h:\[\e[0;32m\]\w \[\e[1;34m\]\u\[\e[0m\]\$ 颜色随自己喜好修改 如下使其生效 source ~/.bashrc 效果如下 Enjoy&#xff01;&#xff01;&#xff01;

数学考研错题本:查漏补缺,高效提升备考策略

考研之路漫长而艰辛&#xff0c;对于数学这一学科来说&#xff0c;错题本的建立与利用显得尤为重要&#xff0c;通过分析错题&#xff0c;我们可以查漏补缺&#xff0c;找到自己的薄弱环节&#xff0c;从而有针对性地进行复习&#xff0c;本文将详细阐述如何建立和利用数学考研…

【数据采集工具】Sqoop从入门到面试学习总结

国科大学习生活&#xff08;期末复习资料、课程大作业解析、大厂实习经验心得等&#xff09;: 文章专栏&#xff08;点击跳转&#xff09; 大数据开发学习文档&#xff08;分布式文件系统的实现&#xff0c;大数据生态圈学习文档等&#xff09;: 文章专栏&#xff08;点击跳转&…

数控机械制造工厂ERP适用范围有哪些

在当今制造业高速发展的背景下&#xff0c;企业资源计划(ERP)系统已成为提升工厂管理效率、实现生产自动化与信息化的关键工具。特别是对于数控机械制造工厂而言&#xff0c;一个合适的ERP系统能够帮助其优化生产流程、提高产品质量、降低生产成本并增强市场竞争力。 1. 生产计…

React 项目热更新失效问题的解决方案和产生的原因

背景和意义 在修复React项目热更新失效的问题时&#xff0c;经过一系列问题排查和依赖升级&#xff0c;最终成功修复了问题并为后续开发规避了类似的问题。 依赖升级 Vite版本升级 原React项目Vite版本升级到^4.4.5 Vite 4 在构建和开发服务器的性能上进行了优化&#xff…

Java--集合之vectorlinkedlisthashset结构

文章目录 0.架构图1.vector解析2.LinkedList分析2.1源码分析2.2迭代器遍历的三种方式 3.set接口的使用方法3.1基本使用说明3.2基本遍历方式3.3HashSet引入3.4数组链表模拟3.5hashset扩容机制3.6hashset源码解读3.7扩容*转成红黑树机制**我的理解 0.架构图 1.vector解析 和之前介…

15分钟学Go 第4天:Go的基本语法

第4天&#xff1a;基本语法 在这一部分&#xff0c;将讨论Go语言的基本语法&#xff0c;了解其程序结构和基础语句。这将为我们后续的学习打下坚实的基础。 1. Go语言程序结构 Go语言程序的结构相对简单&#xff0c;主要包括&#xff1a; 包声明导入语句函数语句 1.1 包声…

物联网协议:MQTT、CoAP 和 LwM2M 的比较与应用

目录标题 1.引言 &#x1f4d8;2. 物联网协议概述 &#x1f4da;3. MQTT 协议详解 &#x1f4e1;&#x1f4e1;&#x1f4e1;&#x1f4e1;3.1 协议特点3.2 工作原理3.3 应用场景 4. CoAP 协议详解 &#x1f517;4.1 协议特点4.2 工作原理4.3 应用场景 5. LwM2M 协议详解 ⚙️5…

LeetCode刷题日记之贪心算法(一)

目录 前言分发饼干摆动序列最大子数组和总结 前言 作为LeetCode刷题的过程中&#xff0c;贪心算法一直是一个经典的算法思路。在这篇文章中&#xff0c;我将记录自己刷LeetCode贪心算法题的过程&#xff0c;并逐步梳理该算法的核心逻辑&#xff0c;包括如何选择最优解、判断局…

PS证件照换底色

ps工具&#xff1a;Adobe Photoshop 2021 文章目录 1. 扣取人物2. 更换底色 1. 扣取人物 2. 更换底色

SpringSecurity使用介绍

1、SpringSecurity 1.1 SpringSecurity简介 Spring Security是基于Spring的安全框架,提供了包含认证和授权的落地方案&#xff1b;Spring Security底层充分利用了Spring IOC和AOP功能&#xff0c;为企业应用系统提供了声明式安全访问控制解决方案&#xff1b;SpringSecurity可…

数据字典是什么?和数据库、数据仓库有什么关系?

一、数据字典的定义及作用 数据字典是一种对数据的定义和描述的集合&#xff0c;它包含了数据的名称、类型、长度、取值范围、业务含义、数据来源等详细信息。 数据字典的主要作用如下&#xff1a; 1. 对于数据开发者来说&#xff0c;数据字典包含了关于数据结构和内容的清晰…

3. 单例模式唯一性问题—构造函数

1. 构造函数带来的唯一性问题指什么&#xff1f; 对于不继承MonoBehaviour的单例模式基类 我们要避免在外部 new 单例模式类对象 例如 &#xff08;完整单例模式定义在上一节&#xff09; public class Main : MonoBehaviour {void Start(){// 破坏单例模式的唯一性&#xf…

跨越距离:2024四大远程控制软件体验!

在多元化的现代生活中&#xff0c;远程控制软件已经成为我们不可或缺的助手。它们可以帮助我们实现远程办公、远程协助、远程游戏等多种功能。今天&#xff0c;我们就来为大家盘点几款热门的远程控制软件&#xff0c;包括向日葵远程控制、RayLink远程控制、Parsec和AirDroid&am…

C++笔记之静态多态和动态多态

C++笔记之静态多态和动态多态 code review! 在C++中,多态(Polymorphism)是面向对象编程的一个核心概念,允许对象以多种形式存在。多态性主要分为静态多态(Static Polymorphism)和动态多态(Dynamic Polymorphism)。下面将详细解释这两种多态及其在C++中的实现方式、优缺…

Stable Diffusion Web UI 大白话术语解释 (二)

归纳整理&#xff0c;Stable Diffusion Web UI 使用过程中&#xff0c;相关术语 ControlNet ControlNet 说简单点&#xff0c;就是你可以给 AI 一些“规则”&#xff0c;比如让它根据某些线条、结构或者骨架去画图。 这样能让 AI 画出更符合你要求的图片&#xff0c;特别适合画…

QT 实现按钮多样化

1.界面实现效果 以下是具体的项目需要用到的效果展示&#xff0c;可以根据需要&#xff0c;实例化想要的按钮。 2.简介 原理&#xff1a;使用Qt的QPropertyAnimation动画类&#xff0c;这里简单来说就是切换两个按钮样式。 请看以下结构体&#xff1a; #define MAX_LINE_C…

为什么Java中1==1为真,而128==128为假?基于享元模式的整数缓存原理分析

❃博主首页 &#xff1a; 「码到三十五」 &#xff0c;同名公众号 :「码到三十五」&#xff0c;wx号 : 「liwu0213」 ☠博主专栏 &#xff1a; <mysql高手> <elasticsearch高手> <源码解读> <java核心> <面试攻关> ♝博主的话 &#xff1a…

从零开始搭建你的DolphinScheduler分布式任务调度平台实战指南

文章目录 前言1. 安装部署DolphinScheduler1.1 启动服务 2. 登录DolphinScheduler界面3. 安装内网穿透工具4. 配置Dolphin Scheduler公网地址5. 固定DolphinScheduler公网地址 前言 本篇教程和大家分享一下DolphinScheduler的安装部署及如何实现公网远程访问&#xff0c;结合内…