54.多级缓存

目录

一、传统缓存的问题、多级缓存方案。

二、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。

六、多级缓存总结。


一、传统缓存的问题、多级缓存方案。


二、JVM进程缓存。

1)进程缓存和远程缓存。

在Java中,进程缓存和缓存也是两个不同的概念。

  1. 进程缓存:在Java中,进程缓存通常指JVM的堆内存,它是Java虚拟机为每个Java进程分配的内存空间。Java进程可以使用堆内存来存储对象、数组等数据结构,以及执行方法时所需的局部变量、方法参数等。Java程序可以通过调整JVM的参数来控制堆内存的大小,从而影响程序的性能和内存占用。

  2. 远程缓存:不存放在程序里面,而是放到远程缓存软件。

缓存又分进程内缓存和远程缓存两种:远程缓存如redis、memcached等,还有本地(进程内)缓存如ehcache、GuavaCache、Caffeine等。

需要注意的是,Caffeine是一个进程级别的缓存,它只在单个Java进程内生效。

2)导入商品案例。

为了演示多级缓存,我们先导入一个商品管理的案例,其中包含商品的CRUD功能。我们将来会给查询商品添加多级缓存。

1.安装MySQL

后期做数据同步需要用到MySQL的主从功能,所以需要大家在虚拟机中,利用Docker来运行一个MySQL容器。

1.1.准备目录

为了方便后期配置MySQL,我们先准备两个目录,用于挂载容器的数据和配置文件目录:

复制代码
# 进入/tmp目录
cd /tmp
# 创建文件夹
mkdir mysql
# 进入mysql目录
cd mysql

1.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.25

1.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=1000

1.4.重启

配置修改后,必须重启容器:

复制代码
docker restart mysql

2.导入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业务集群要部署的地方:

完整内容如下:

Lua 复制代码
#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;
        }
    }
}

3)初识Caffeine(就是在springboot学过的注解方式的cache)。

这里是使用代码方式写的。

使用案例:

java 复制代码
public class CaffeineTest {
    /*
      基本用法测试
     */
    @Test
    void testBasicOps() throws UnsupportedEncodingException {
        // 创建缓存对象
        Cache<String, String> 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<String, String> 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<String, String> 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
    }
}

4)实现进程缓存。

加载Cache成为Bean:

java 复制代码
@Configuration
public class CaffeineConfig {
    @Bean
    public Cache<Long, Item> itemCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)//初始化100个key容量
                .maximumSize(10_000)//上限是10000个key容量
                .build();
    }

    @Bean
    public Cache<Long, ItemStock> itemStockCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)//初始化100个key容量
                .maximumSize(10_000)//上限是10000个key容量
                .build();
    }
}

使用Caffeine缓存:

java 复制代码
@RestController
@RequestMapping("item")
public class ItemController {
    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    @Autowired
    private Cache<Long,Item> itemCache;
    @Autowired
    private Cache<Long,ItemStock> 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里面的进程缓存:

三、Lua语法入门。

1)初识Lua。

CentOS中自带Lua环境。

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。官网:https://www.lua.org/

简单写一个lua脚本:

2)数据类型、变量和循环。



lua中字符串拼接是使用..连接的,如local str = 'hello' .. 'world' #打印出来是helloworld

3)函数、条件控制。


四、多级缓存。

1)安装OpenResty。

官方网站: https://openresty.org/cn/

1.安装

首先你的Linux虚拟机必须联网

1)安装开发库

首先要安装OpenResty的依赖开发库,执行命令:

复制代码
yum install -y pcre-devel openssl-devel gcc --skip-broken

2)安装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 openresty

4)安装opm工具

opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。

如果你想安装命令行工具 opm,那么可以像下面这样安装 openresty-opm 包:

复制代码
yum install -y openresty-opm

5)目录结构

默认情况下,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:$PATH

NGINX_HOME:后面是OpenResty安装目录下的nginx的目录

然后让配置生效:

复制代码
source /etc/profile

2.启动和运行

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文件,以下内容覆盖原本内容:

bash 复制代码
#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模块:

bash 复制代码
#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块     
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  

common.lua(这个是写一个函数,方便后面调用,可根据自己需求编写)

bash 复制代码
-- 封装函数,发送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:

bash 复制代码
-- 关闭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:

bash 复制代码
-- 查询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; 

2)OpenResty快速入门。

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也要重新加载,否则的话是访问失败(还是原来的样子,没有变化)的。

3)请求参数处理。

~:波浪线表示后面跟着正则表达式匹配。

案例:

修改openResty中的nginx的nginx.conf文件。

修改openResty中的nginx目录下的lua目录下的item.lua文件。

都改完后执行nginx -s reload重新加载,然后访问。

4)查询Tomcat。

适用于所有虚拟机连接windows系统的便捷方式:虚拟机的IP地址前三个数字不变,第四个数字替换为1,、就一定能得到wdows地址。(前提是windows防火墙关闭)

例如:

虚拟机IP地址:192.168.203.129

连接windows系统使用:192.168.203.1
lua文件的语句结束不用";",但我发现使用了";"也没有报错,要使用英文分号。

将函数导出:意思就是加载这个模块(类似java中的导包)的文件可以使用该函数。这里的发送请求会被反向代理拦截,然后发到指定IP地址。

5)Tomcat集群的负载均衡。

在Nginx中,使用 hash $request_uri; 可以实现基于请求URI的负载均衡策略。这个策略会根据请求的URI对后端服务器进行哈希计算,并将同一URI的请求始终分发到同一台后端服务器上。$ 符号表示引用变量的开始。在这种上下文中,$request_uri代表了请求的URI变量。

计算请求路径的哈希值,根据哈希值取余tomcat服务器数量,保证同一个请求路径只会发给同一个tomcat处理,保证进程缓存的可用性。

操作如下:

6)Redis缓存预热。

初始化redis缓存:

java 复制代码
@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<Item> 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<ItemStock> 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);
        }
    }
}

7)查询Redis缓存。

common.lua文件:

java 复制代码
-- 引入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文件:

java 复制代码
-- 导入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日志,一般会告诉你因何错位)

8)Nginx本地缓存。

在 Nginx 中,"worker" 是指工作进程(worker process)。Nginx 的主进程负责管理整个服务器,而工作进程则负责处理实际的客户端请求。每个工作进程相互独立,它们可以同时处理多个客户端连接和请求。

linux的openResty里的nginx的nginx.conf文件:

item.lua文件:

java 复制代码
-- 导入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))

五、缓存同步策略。

1)数据同步策略。

使用MQ还是有一些代码侵入。

我们使用下面这种:下面这种代码侵入更少。

2)安装Canal。

2.1)初识Canal。

2.2)安装和配置Canal

下面我们就开启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 mysql

2.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 

3)监听Canal。

Canal框架 概念: canal用java开发的基于数据库增量日志解析,提供增量数据订阅&消费的中间件。目前,canal主要支持了MySQL的binlog解析,解析完成后才利用canal client 用来处理获得的相关数据。

Canal 是阿里巴巴开源的数据库变更数据抓取和同步框架,用于监听数据库的变更,并将这些变更事件传输到消息中间件或者其他存储介质中。

RedisHandler implements InitializingBean类:

java 复制代码
@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<Item> 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<ItemStock> 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<Item>类:

java 复制代码
@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) {
        //删除数据到jvm进程缓存
        itemCache.invalidate(item.getId());
        //删除数据到redis
        redisHandler.deleteItemById(item.getId());
    }
}

六、多级缓存总结。

相关推荐
minihuabei43 分钟前
linux centos 安装redis
linux·redis·centos
monkey_meng3 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
想要打 Acm 的小周同学呀3 小时前
LRU缓存算法
java·算法·缓存
hlsd#3 小时前
go 集成go-redis 缓存操作
redis·缓存·golang
镰刀出海3 小时前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试
奶糖趣多多5 小时前
Redis知识点
数据库·redis·缓存
CoderIsArt6 小时前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存
ketil2711 小时前
Redis - String 字符串
数据库·redis·缓存
王佑辉13 小时前
【redis】延迟双删策略
redis