基于Canal的Mysql&Redis数据同步实现

11 篇文章 1 订阅
2 篇文章 0 订阅

1.Canal简介

我们在做mysqlredis的数据同步时,往往采用的是代码层实现,或者通过spring-cache等缓存框架。但是仍然有某些场景,比如说原项目无源码,或者不能进行二开时,就需要独立的第三方来实现数据同步。

我们需要一种无代码入侵式的数据同步,完全由第三方组件管理。这就需要借助canal来实现mysqlredis的数据同步

canal是阿里巴巴旗下的一款开源项目,纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL(也支持mariaDB

其工作原理主要是模仿MySQL复制

  • canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
  • mysql master收到dump请求,开始推送binary logslave(也就是canal
  • canal解析binary log对象(原始为byte流

在这里插入图片描述

Canal架构如下:

  • eventParser (数据源接入,模拟slave协议和master进行交互,协议解析)
  • eventSink (Parser和Store链接器,进行数据过滤,加工,分发的工作)
  • eventStore (数据存储)
  • metaManager (增量订阅&消费信息管理器)
  • server代表一个canal运行实例,对应于一个jvm
  • instance对应于一个数据队列 (1个server对应1…n个instance)

instance模块如下图所示:
在这里插入图片描述

大致的解析过程如下:

  • parse解析MySQL的Bin log,然后将数据放入到sink中
  • sink对数据进行过滤,加工,分发
  • storesink中读取解析好的数据存储起来
  • 然后自己用设计代码将store中的数据同步写入Redis中就可以了
  • 其中parse/sink是框架封装好的,我们做的是store的数据读取那一步

在这里插入图片描述

canal分成服务端deployer和客户端adapter,我们可以部署多个,同时为了方便管理还提供了一个管理端admin
在这里插入图片描述

因为目前canal还不能直接通过配置就实现对redis的数据同步,因此我们需要自定义一下canal客户端,通过服务端将数据同步到客户端后,由客户端自定义操作同步到redis

在这里插入图片描述

2.Canal安装

canal支持多种安装方式,本文提供了windows环境下和docker环境下的安装方式,详情请移步:《Canal安装教程》

在此基础上,本文相关配置如下:

  • mysql
    • 端口号:3307
    • ip:192.168.0.104
    • 待匹配表:canal_demo/tb_user
    • 在这里插入图片描述

SQL:

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
  `user_id` int NOT NULL AUTO_INCREMENT,
  `user_name` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  `age` int DEFAULT NULL,
  `sex` char(2) COLLATE utf8_bin DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin;

对应的instance.properties如下:
在这里插入图片描述

  • canal
    在canal-server目录下新建一个redis目录,复制examlpe的所有内容到redis中
    • windows环境下
      在这里插入图片描述

    • docker环境下
      在这里插入图片描述

重新启动canal-server服务即可

进入 canal-admin 的控制台,如果你的配置正确,server 列表里会自动出现启动:
在这里插入图片描述

在 instance 管理中新建一个 instance,注意名字要和刚才复制的文件夹名字对应:
在这里插入图片描述

在这里插入图片描述

过几秒钟以后,如果你的配置正确,instance 列表里,如果列表里的状态是停止,可以在操作里手动启动
在这里插入图片描述

3.SpringBoot集成Canal实现数据同步

为了节约文章篇幅,本文只贴出核心代码,完整代码可在文末进行查看或下载

3.1 添加maven依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.72</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>1.1.4</version>
        </dependency>

由于我们需要对数据库操作来观察Redis中的变化,建议大家集成一下Mybatis或Mybatis-Plus实际操作一下:

3.2 添加配置

#redis配置
# Redis服务器地址
spring.redis.host=192.168.0.104
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# Redis数据库索引(默认为0)
spring.redis.database=0
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0
spring.redis.lettuce.shutdown-timeout=0

3.3 添加Redis操作的工具类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;

@Service
public class RedisUtils {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 写入缓存 * @param key * @param value * @return
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 写入缓存设置时效时间 * @param key * @param value * @return
     */
    public boolean setEx(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 判断缓存中是否有对应的value * @param key * @return
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 读取缓存 * @param key * @return
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }

    /**
     * 删除对应的value * @param key
     */
    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;

    }
}


3.4 新建 canal 客户端

新建一个canal 客户端,并且依赖 ApplicationRunner,在 Spring 容器启动完成后开启守护线程同步任务(注意 import 时选择 canal 包下的类):

import com.alibaba.fastjson.JSONObject;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import com.example.demo.util.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;
import java.util.List;

@Slf4j
@Component
public class CanalClient implements ApplicationRunner {
    @Autowired
    RedisUtils redisUtils;

    private static final String TABLE_NAME = "tb_user";
    private static final String PRIMARY_KEY = "user_id";
    private static final String SEPARATOR = ":";

    private static final String CANAL_SERVER_HOST = "192.168.0.104";
    private static final int CANAL_SERVER_PORT = 11111;
    private static final String CANAL_INSTANCE = "redis";
    private static final String USERNAME = "canal";
    private static final String PASSWORD = "canal";

    @Override
    public void run(ApplicationArguments args) throws Exception {
        this.initCanal();
    }

    public void initCanal() {
        // 创建链接
        CanalConnector connector = CanalConnectors.newSingleConnector(
                new InetSocketAddress(CANAL_SERVER_HOST, CANAL_SERVER_PORT),
                CANAL_INSTANCE, USERNAME, PASSWORD);
        int batchSize = 1000;
        try {
            log.info("启动 canal 数据同步...");
            connector.connect();
            connector.subscribe(".*\\..*");
            connector.rollback();
            while (true) {
                // 获取指定数量的数据
                Message message = connector.getWithoutAck(batchSize);
                long batchId = message.getId();
                int size = message.getEntries().size();
                if (batchId == -1 || size == 0) {
                    try {
                        // 时间间隔1000毫秒
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    syncEntry(message.getEntries());
                }
                connector.ack(batchId); // 提交确认
                // connector.rollback(batchId); // 处理失败, 回滚数据
            }
        } finally {
            connector.disconnect();
        }
    }

    private void syncEntry(List<CanalEntry.Entry> entrys) {
        for (CanalEntry.Entry entry : entrys) {
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN
                    || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
                continue;
            }

            CanalEntry.RowChange rowChange;
            try {
                rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("ERROR data:" + entry.toString(), e);
            }

            CanalEntry.EventType eventType = rowChange.getEventType();
            log.info("================> binlog[{}:{}] , name[{},{}] , eventType : {}",
                    entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
                    entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
                    eventType);

            String tableName = entry.getHeader().getTableName();
            if (!TABLE_NAME.equalsIgnoreCase(tableName)) continue;
            for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                if (eventType == CanalEntry.EventType.INSERT) {
                    printColumn(rowData.getAfterColumnsList());
                    redisInsert(tableName, rowData.getAfterColumnsList());
                } else if (eventType == CanalEntry.EventType.UPDATE) {
                    printColumn(rowData.getAfterColumnsList());
                    redisUpdate(tableName, rowData.getAfterColumnsList());
                } else if (eventType == CanalEntry.EventType.DELETE) {
                    printColumn(rowData.getBeforeColumnsList());
                    redisDelete(tableName, rowData.getBeforeColumnsList());
                }
            }
        }
    }

    private void redisInsert(String tableName, List<CanalEntry.Column> columns) {
        JSONObject json = new JSONObject();
        for (CanalEntry.Column column : columns) {
            json.put(column.getName(), column.getValue());
        }
        for (CanalEntry.Column column : columns) {
            if (PRIMARY_KEY.equalsIgnoreCase(column.getName())) {
                String key = tableName + SEPARATOR + column.getValue();
                redisUtils.set(key, json.toJSONString());
                log.info("redis数据同步新增,key:" + key);
                break;
            }
        }
    }

    private void redisUpdate(String tableName, List<CanalEntry.Column> columns) {
        JSONObject json = new JSONObject();
        for (CanalEntry.Column column : columns) {
            json.put(column.getName(), column.getValue());
        }
        for (CanalEntry.Column column : columns) {
            if (PRIMARY_KEY.equalsIgnoreCase(column.getName())) {
                String key = tableName + SEPARATOR + column.getValue();
                redisUtils.set(key, json.toJSONString());
                log.info("redis数据同步更新,key:" + key);
                break;
            }
        }
    }

    private void redisDelete(String tableName, List<CanalEntry.Column> columns) {
        for (CanalEntry.Column column : columns) {
            if (PRIMARY_KEY.equalsIgnoreCase(column.getName())) {
                String key = tableName + SEPARATOR + column.getValue();
                redisUtils.remove(key);
                log.info("redis数据同步删除,key:" + key);
                break;
            }
        }
    }

    private void printColumn(List<CanalEntry.Column> columns) {
        for (CanalEntry.Column column : columns) {
            log.info(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
        }
    }

}

3.5 编写接口测试

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
public class TbUserController {
    @Autowired
    ITbUserService tbUserService;

    @PostMapping("insert")
    public Object insert(@RequestBody TbUser tbUser) {
        tbUserService.save(tbUser);
        return "数据插入完成";
    }

    @PostMapping("update")
    public Object update(@RequestBody TbUser tbUser) {
        tbUserService.updateById(tbUser);
        return "数据更新完成";
    }

    @PostMapping("delete")
    public Object delete(@RequestParam(value = "id") Integer id) {
        tbUserService.removeById(id);
        return "数据删除完成";
    }
}

3.6 测试


  • 在这里插入图片描述

在查看redis中是否也跟着插入
在这里插入图片描述


  • 在这里插入图片描述

可以看到redis中的数据也跟着同步变化
在这里插入图片描述


  • 在这里插入图片描述

点击刷新按钮,发现已经没有这个key了
在这里插入图片描述

4.总结

Canal 的好处在于对业务代码没有侵入,因为是基于监听 binlog 日志去进行同步数据的。实时性也能做到准实时,其实是很多企业一种比较常见的数据同步的方案。
以上只是一个测试的案例。

Canal 根据偏移量增量同步 MySQL 的 binlog,可以为每个 instance 配置路由规则,只同步部分内容,业务代码也可以自行修改,不仅仅同步到 Redis,也可以同步到其他存储介质中,不仅仅同步相同数据,可以自定义数据模型结构进行转换。

上述代码中,我们得到 binlog 同步数据对象 proto,格式主要如下:
在这里插入图片描述

由此可见,可以根据 DDL(data definition language) 数据定义语言, DML(data manipulation language) 数据操纵语言 区分自己不同的操作,DML 操作也能得到具体数据库名表名以及字段名和数据变更前变更后的各种详细数据信息,我们可以选择性地结合业务来同步数据。

当然这仅仅只是入门,实际项目中一般配置 MQ 模式,配合 RocketMQ 或者 Kafka,Canal 会把数据发送到 MQ 的 topic 中,然后通过消息队列的消费者进行处理。Canal 的部署也是支持集群的,需要配合 ZooKeeper 进行集群管理。

项目地址:https://gitee.com/ninesuntec/canal.git

  • 0
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: 可以回答这个问题。canal中间件可以实现redismysql数据同步,它可以监控mysql数据库的binlog日志,将数据变更同步redis中。同时,canal也支持将mysql数据同步到其他数据库,如elasticsearch等。 ### 回答2: Canal中间件是一种用于实现数据数据同步的工具。它可以实现RedisMySQL之间的数据同步。 首先,我们需要设置Canal中间件与RedisMySQL的连接。通过配置Canal中间件的连接参数,使其能够连接到RedisMySQL数据库。可以设置监控的表和字段,以确定需要同步数据范围。 当MySQL数据库中的数据发生变化时,Canal中间件会自动捕获这些变化并生成相应的Binlog日志。通过监听MySQL数据库的Binlog日志,Canal中间件能够实时获取更新的数据。 接下来,Canal中间件将获取到的数据进行处理,将变化的数据转换成Redis可以接受的数据格式,并将其写入到Redis数据库中。这样就实现MySQL数据变化的同步Redis数据库的功能。 通过Canal中间件,我们可以实现MySQLRedis之间的双向数据同步。当MySQL中的数据发生改变时,Canal中间件会将变化的数据同步Redis数据库中。同样地,当Redis中的数据发生改变时,Canal中间件也能够捕获这些变化并同步MySQL数据库中。 这种数据同步的方式可以提高应用程序的性能和可靠性。通过将热点数据存储到Redis中,可以提高读取性能。同时,由于Canal中间件的实时同步机制,可以保证数据的一致性。 总结起来,Canal中间件可以实现RedisMySQL之间的数据同步。它通过捕获MySQL数据库的Binlog日志,并将变化的数据转换成Redis可以接受的格式,实现数据同步。这种方式可以提高应用程序的性能和可靠性。 ### 回答3: Canal中间件是一个开源的数据同步工具,用于将数据库中的数据同步到其他数据源。在实现RedisMySQL数据同步的场景下,可以通过以下步骤实现数据同步: 第一步,安装配置Canal中间件。首先需要在服务器上安装Canal中间件,并配置Canal的相关参数,如数据源的地址、端口号、用户名和密码等。 第二步,配置Canal中间件连接MySQL数据库。需要在Canal配置文件中添加MySQL数据源的相关信息,包括MySQL服务器的地址、端口号、用户名和密码等。 第三步,配置Canal中间件连接Redis数据库。同样需要在Canal配置文件中添加Redis数据源的相关信息,包括Redis服务器的地址、端口号、密码等。 第四步,创建数据库表数据的监听和同步。通过创建Canal的实例,并指定需要监听的数据库和表,可以实现对特定表数据的监听和同步。 第五步,通过Canal中间件将MySQL数据同步Redis。当MySQL数据库中的数据发生变化时,Canal中间件会将变更的数据解析并转发给Redis数据库,实现数据同步。 通过以上步骤,即可实现RedisMySQL数据同步。Canal中间件作为数据同步的桥梁,能够实时监听MySQL数据库的变化,并将变更的数据同步Redis,确保数据的一致性和及时性。 需要注意的是,在配置Canal中间件时,需要确保CanalMySQLRedis之间的网络连接正常,并且对应的用户有足够的权限来进行数据操作。同时,还需要进行相关的性能测试和优化,以确保数据同步的效率和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZNineSun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值