数据库系列——基于Canal实现MySQL增量数据同步

环境准备:

1、redis (默认端口6379)

2、zookeeper (默认端口2181)

3、Kafka (默认端口9092)

4、Canal (默认端口 11111)

5、MySQL (默认端口 3306

本文Github代码地址https://github.com/cheriduk/spring-boot-integration-template

Canal介绍:引用官方介绍


canal 是阿里巴巴 MySQL 数据库 Binlog 的增量订阅&消费组件。

名称:canal [kə'næl]
译意: 水道/管道/沟渠
语言: 纯java开发
定位: 基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL

早期,阿里巴巴 B2B 公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于 trigger 的方式获取增量 变更,不过从 2010 年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务,从此开 启了一段新纪元。ps. 目前内部使用的同步,已经支持 MySQL 8.x 和 Oracle 部分版本的日志解析

基于日志增量订阅&消费支持的业务:

  1. 数据库镜像

  2. 数据库实时备份

  3. 多级索引 (卖家和买家各自分库索引)

  4. search build (elastic search)

  5. 业务cache刷新(redis)

  6. 价格变化等重要业务消息

Canal 工作原理:

原理相对比较简单:

  1. canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议

  2. mysql master收到dump请求,开始推送binary log给slave(也就是canal)

  3. canal解析binary log对象(原始为byte流)

以上为官方介绍

canal 链接: https://pan.baidu.com/s/1HIT4b30BtXrkHym-w4peww 提取码: ar6c


 实现项目开发中如何使用呢?

实际项目我们是配置MQ模式,配合RocketMQ或者Kafka,canal会把数据发送到MQ的topic中,然后通过消息队列的消费者进行消费处理

这篇文章演示部署Canal,配合使用Kafka,同步数据到Redis

通过架构图,我们很清晰就知道要用到的组件:MySQL、Canal、Kafka、ZooKeeper、Redis

MySQL搭建大家应该都会,ZooKeeper、Redis这些网上也有很多资料参考

主要说下Kafka搭建

首先在官网下载安装包:
 

解压,打开/config/server.properties配置文件,修改日志目录

首先启动ZooKeeper,我用的是3.4.13版本:

 
接着再启动Kafka,在Kafka的bin目录下打开cmd,输入命令:

kafka-server-start.bat ../../config/server.properties

我们可以通过ZooInspector看到ZooKeeper上注册了Kafka相关的配置信息:

然后需要创建一个队列,用于接收canal传送过来的数据,使用命令:

kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic canaltopic

 创建的队列名是canaltopic

在这里插入图片描述

配置Cannal Server

canal官网下载相关安装包:

找到canal.deployer-1.1.4/conf目录下的canal.properties配置文件:

# tcp, kafka, RocketMQ 这里选择kafka模式
canal.serverMode = kafka
# 解析器的线程数,打开此配置,不打开则会出现阻塞或者不进行解析的情况
canal.instance.parser.parallelThreadSize = 16
# 配置MQ的服务地址,这里配置的是kafka对应的地址和端口
canal.mq.servers = 127.0.0.1:9092
# 配置instance,在conf目录下要有example同名的目录,可以配置多个
canal.destinations = example

然后配置instance,找到/conf/example/instance.properties配置文件:

## mysql serverId , v1.0.26+ will autoGen(自动生成,不需配置)
# canal.instance.mysql.slaveId=0

# position info
canal.instance.master.address=127.0.0.1:3306
# 在Mysql执行 SHOW MASTER STATUS;查看当前数据库的binlog
canal.instance.master.journal.name=mysql-bin.000006
canal.instance.master.position=4596
# 账号密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=Canal@****
canal.instance.connectionCharset = UTF-8
#MQ队列名称
canal.mq.topic=canaltopic
#单队列模式的分区下标
canal.mq.partition=0

数据库配置,创建授权账户

 canal的原理是模拟自己为mysql slave,所以这里一定需要做为mysql slave的相关权限 

CREATE USER canal IDENTIFIED BY 'canal';    
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';  
-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;  
FLUSH PRIVILEGES; 

     针对已有的账户可通过grants查询权限:

show grants for 'canal' 

配置完成后,就可以启动canal了。

测试验证

这时可以打开kafka的消费者窗口,测试一下kafka是否收到消息。

kafka-console-consumer.bat --bootstrap-server 127.0.0.1:9092 --from-beginning --topic canaltopic

控制台会出现乱码的话,需要临时设置一下编码

在cmd命令行执行前切换到UTF-8编码即可,使用命令行:chcp 65001

在MySQL数据库操作数据,然后观察Kafka这边变化情况;

返回的这一串字符对应的数据结构

官方有说明:

https://github.com/alibaba/canal/wiki/ClientAPI#%E6%95%B0%E6%8D%AE%E5%AF%B9%E8%B1%A1%E6%A0%BC%E5%BC%8F%E7%AE%80%E5%8D%95%E4%BB%8B%E7%BB%8Dentryprotocolproto 

我使用的是最新的版本,官网文档给出的数据格式可能没及时更新,有些不同

启动Redis,把数据同步到Redis。

环境都搭建好了以后,下面编写Redis客户端代码

首先引入Kafka和Redis的maven依赖:

 <dependencies>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </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.28</version>
            <scope>compile</scope>
        </dependency>
</dependencies>

在application.yml文件配置:

spring:  
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    password: 123456

编写操作Redis的工具类:
 

@Component
public class RedisClient {

    /**
     * 获取redis模版
     */
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 设置redis的key-value
     */
    public void setString(String key, String value) {
        setString(key, value, null);
    }

    /**
     * 设置redis的key-value,带过期时间
     */
    public void setString(String key, String value, Long timeOut) {
        stringRedisTemplate.opsForValue().set(key, value);
        if (timeOut != null) {
            stringRedisTemplate.expire(key, timeOut, TimeUnit.SECONDS);
        }
    }

    /**
     * 获取redis中key对应的值
     */
    public String getString(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    /**
     * 删除redis中key对应的值
     */
    public Boolean deleteKey(String key) {
        return stringRedisTemplate.delete(key);
    }
}

创建MQ消费者进行数据同步

在application.yml配置文件加上kafka的配置信息:

spring:
  kafka:
  	# Kafka服务地址
    bootstrap-servers: 127.0.0.1:9092
    consumer:
      # 指定一个默认的组名
      group-id: consumer-group1
      #序列化反序列化
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringDeserializer
      value-serializer: org.apache.kafka.common.serialization.StringDeserializer
      # 批量抓取
      batch-size: 65536
      # 缓存容量
      buffer-memory: 524288

可以创建一个CanalBean对象进行接收 

public class CanalBean {
    //数据
    private List<Student> data;
    //数据库名称
    private String database;
    private long es;
    //递增,从1开始
    private int id;
    //是否是DDL语句
    private boolean isDdl;
    //表结构的字段类型
    private MysqlType mysqlType;
    //UPDATE语句,旧数据
    private String old;
    //主键名称
    private List<String> pkNames;
    //sql语句
    private String sql;
    private SqlType sqlType;
    //表名
    private String table;
    private long ts;
    //(新增)INSERT、(更新)UPDATE、(删除)DELETE、(删除表)ERASE等等
    private String type;
    //getter、setter方法
}
public class MysqlType {
    private String id;
    private String commodity_name;
    private String commodity_price;
    private String number;
    private String description;
    //getter、setter方法
}
public class SqlType {
    private int id;
    private int commodity_name;
    private int commodity_price;
    private int number;
    private int description;
}

创建业务测试表对应的Bean用于测试使用

@Data // lombok插件依赖
public class Student implements Serializable {
    private Long id;

    private String name;

    private Integer age;

    private static final long serialVersionUID = 1L;
}

最后就可以创建一个消费者CanalConsumer进行消费

package com.gary.sync.consumer;

import com.alibaba.fastjson.JSONObject;
import com.gary.sync.model.CanalBean;
import com.gary.sync.model.Student;
import com.gary.sync.model.TbCommodityInfo;
import com.gary.sync.redis.RedisClient;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

@Component
public class CanalConsumer {
    //日志记录
    private static Logger log = LoggerFactory.getLogger(CanalConsumer.class);
    //redis操作工具类
    @Resource
    private RedisClient redisClient;
    //监听的队列名称为:canaltopic
    @KafkaListener(topics = "canaltopic")
    public void receive(ConsumerRecord<?, ?> consumer) {
        String value = (String) consumer.value();
        log.info("topic名称:{},key:{},分区位置:{},下标:{},value:{}", consumer.topic(), consumer.key(),consumer.partition(), consumer.offset(), value);
        //转换为javaBean
        CanalBean canalBean = JSONObject.parseObject(value, CanalBean.class);
        //获取是否是DDL语句
        boolean isDdl = canalBean.getIsDdl();
        //获取类型
        String type = canalBean.getType();
        //不是DDL语句
        if (!isDdl) {
            List<Student> students = canalBean.getData();
            //过期时间
            long TIME_OUT = 600L;
            if ("INSERT".equals(type)) {
                //新增语句
                for (Student student : students) {
                    Long id = student.getId();
                    //新增到redis中,过期时间是10分钟
                    redisClient.setString(String.valueOf(id), JSONObject.toJSONString(student), TIME_OUT);
                }
            } else if ("UPDATE".equals(type)) {
                //更新语句
                for (Student student : students) {
                    Long id = student.getId();
                    //更新到redis中,过期时间是10分钟
                    redisClient.setString(String.valueOf(id), JSONObject.toJSONString(student), TIME_OUT);
                }
            } else {
                //删除语句
                for (Student student : students) {
                    Long id = student.getId();
                    //从redis中删除
                    redisClient.deleteKey(String.valueOf(id));
                }
            }
        }
    }
}

测试MySQL与Redis数据同步

依次启动-zookeeper-》kafka-》canal-》redis

zookeeper

kafka 

canal 

redis

测试数据准备:

先在MySQL创建表

DROP TABLE IF EXISTS `student`;

CREATE TABLE `student` (
  `id` int NOT NULL,
  `name` varchar(25) DEFAULT NULL,
  `age` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

然后启动项目 

 接着新增一条数据:

INSERT INTO `test`.`student` (`id`, `name`, `age`) 
VALUES
  ('777', '测试', '123') ;

student表查到新增的数据:

Redis也查到了对应的数据,证明同步成功!

 

使用场景:

  1. canal只能同步增量数据。

  2. 不是实时同步,是准实时同步。

增量同步,实现性不太强的场景

 

 

猜你喜欢

转载自blog.csdn.net/Coder_Boy_/article/details/111055381