java通过canal同步mysql变动数据到redis

canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议,具体可以直接去看官方文档:https://github.com/alibaba/canal ,此处使用tcp协议同步数据到redis

此处使用1.1.4的版本,通过WebUI能力来配置canal

一、需要先安装好java环境与mysql

1,java安装:https://blog.csdn.net/u012946310/article/details/81979595
2,mysql安装:https://blog.csdn.net/u012946310/article/details/81880050

mysql安装完成后,编辑 /etc/my.cnf,在[mysqld]配置下新增信息

#开启binlog
log-bin=mysql-bin
binlog-format=ROW #选择row模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复

在这里插入图片描述
保存重启mysql

service mysqld restart

二、下载canal服务端与canal-admin管理端

1,canal 服务端下载地址:https://github.com/alibaba/canal/releases/tag/canal-1.1.4
在这里插入图片描述

2,canal-admin下载:wget https://github.com/alibaba/canal/releases/download/canal-1.1.4/canal.admin-1.1.4.tar.gz
官方文档demo:https://github.com/alibaba/canal/wiki/Canal-Admin-QuickStart

三、配置canal-admin管理端

1,解压缩

mkdir canal-admin-1.1.4
tar -zxvf canal.admin-1.1.4.tar.gz -C canal-admin-1.1.4/

解压完成后,进入 canal-admin-1.1.4 目录,可以看到如下结构
在这里插入图片描述
2,移动 canal-admin-1.1.4 目录到 /usr/local 目录下

mv canal-admin-1.1.4 /usr/local/
cd /usr/local/canal-admin-1.1.4/

3,修改配置,根据自己配置修改

vi conf/application.yml
server:
  port: 8089
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

spring.datasource:
  address: 127.0.0.1:3306
  database: canal_manager
  username: canal
  password: canal
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false
  hikari:
    maximum-pool-size: 30
    minimum-idle: 1

canal:
  adminUser: admin
  adminPasswd: admin

注意:如果mysql使用的是 8.0.x 或者以上的版本需要将 driver-class-name 配置驱动更换为:com.mysql.cj.jdbc.Driver,然后将8.0以上版本的驱动 jar 包上传到 lib 目录下

在这里插入图片描述

在这里插入图片描述

4,初始化元数据库

mysql -uroot -p123456
#导入sql
source conf/canal_manager.sql

脚本执行成功后,可以看到我们数据库已经创建好canal_manager数据库了
在这里插入图片描述

初始化SQL脚本里会默认创建canal_manager的数据库,建议使用root等有超级权限的账号进行初始化 b. canal_manager.sql默认会在conf目录下

5,启动

sh bin/startup.sh

查看 admin 日志

cat logs/admin.log

在这里插入图片描述
此时代表canal-admin已经启动成功,可以通过 http://127.0.0.1:8089/ 访问,默认密码:admin/123456
在这里插入图片描述
6,关闭

sh bin/stop.sh

四、配置canal-server

1,解压缩

mkdir canal-server-1.1.4
tar -zxvf canal.deployer-1.1.4.tar.gz -C canal-server-1.1.4/

2,移动 canal-admin-1.1.4 目录到 /usr/local 目录下

mv canal-server-1.1.4 /usr/local/
cd /usr/local/canal-server-1.1.4/

3,编辑 conf/canal_local.properties

vim conf/canal_local.properties
# register ip
canal.register.ip =

# canal admin config
canal.admin.manager = 127.0.0.1:8089
canal.admin.port = 11110
canal.admin.user = admin
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
# admin auto register
canal.admin.register.auto = true
canal.admin.register.cluster =

根据自己需求更改,本机都是安装在一台机器上的,就不做修改了

4,启动

sh bin/startup.sh local

启动完成后,就能在 canal-admin 看到canal-server服务已经注册进来了
在这里插入图片描述
5,选择instance管理,新建一个当前 canal 运行服务的实例,点击选择载入模板,instance名称我这里输入和数据库名称一致springboot_canal_demo

各配置说明参考文档:https://github.com/alibaba/canal/wiki/AdminGuide
在这里插入图片描述
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 (逗号分隔)
* 注意:此过滤条件只针对row模式的数据有效(ps. mixed/statement因为不解析sql,所以无法准确提取tableName进行过滤)
*/

配置完成保存,然后启动当前实例就ok了

5、配置通过 java 将数据库变动数据同步到redis

1,安装 docker(此处只讲步骤,docker不了解的可以自行百度)

yum -y install docker

2,启动docker

service docker start

3,拉取redis镜像

docker pull redis

4,启动redis容器

docker run --name redis -p 6379:6379 -v /etc/localtime:/etc/localtime -d redis --requirepass "123456"

启动完成后可以通过 docker ps 命令查看正在运行的 docker 容器
在这里插入图片描述
接下来就可以通过 redis desktop manager 可视化工具连接到redis了,配置的连接密码为123456

5,新建springboot项目,集成 canal 客户端,同步数据库变动到redis
官方文档例子:https://github.com/alibaba/canal/wiki/ClientExample
本项目git demo:https://gitee.com/hwm0717/springboot_canal_demo

项目执行后,可以发现,每次数据库的更改,都会将数据库的变动数据同步到redis里面
在这里插入图片描述
6,项目结构
在这里插入图片描述
数据库sql

CREATE TABLE `user` (
  `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `nickname` varchar(126) DEFAULT NULL COMMENT '昵称',
  `name` varchar(50) DEFAULT NULL COMMENT '姓名',
  `phone` varchar(11) DEFAULT NULL COMMENT '手机',
  `login_name` varchar(20) DEFAULT NULL COMMENT '登陆名',
  `login_pwd` varchar(100) DEFAULT NULL COMMENT '密码',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>springboot.canal.demo</groupId>
    <artifactId>springboot-canal-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-canal-demo</name>
    <description>Spring Boot canal1.1.4 demo</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>

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

        <!-- https://hutool.cn/docs/#/ -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.4.4</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>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.properties


#服务端口号
server.port=8080

#canal配置
alibaba.canal.ip=192.168.2.196
alibaba.canal.port=11111
#instance名称
alibaba.canal.destination=springboot_canal_demo
alibaba.canal.username=canal
alibaba.canal.password=canal
#监听的数据库
alibaba.canal.subscribe=springboot_canal_demo\\..*

#redis配置
spring.redis.database=0
spring.redis.host=192.168.2.196
spring.redis.port=6379
spring.redis.password=123456
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=20
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=5
# 连接超时时间(毫秒)
spring.redis.timeout=30000

RedisConfig.java

package springboot.canal.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * RedisTemplate 配置,重写key和value的序列化
 *
 * @Classname RedisConfig
 * @Description
 * @Date 2020/5/12 12:44
 * @Created by hwm
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 配置redisTemplate
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        RedisSerializer stringSerializer = new StringRedisSerializer();

        redisTemplate.setKeySerializer(stringSerializer); // key序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化
        redisTemplate.setHashKeySerializer(stringSerializer); // Hash key序列化
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); // Hash value序列化
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

}

CanalInitBean.java

package springboot.canal.demo.canalinit;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * canal配置bean
 *
 * @description:
 * @author: dada
 * @date: 2020/10/29 10:42
 */
@ConfigurationProperties(prefix = "alibaba.canal")
@Configuration
@Data
public class CanalInitBean {

    // canal ip
    private String ip;

    // canal port
    private Integer port;

    // 实例名称
    private String destination;

    private String username;

    private String password;

    /**
     * 过滤规则:
     * mysql 数据解析关注的表,Perl正则表达式.
     * 多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\)
     * 常见例子:
     * 1.  所有表:.*   or  .*\\..*
     * 2.  canal schema下所有表: canal\\..*
     * 3.  canal下的以canal打头的表:canal\\.canal.*
     * 4.  canal schema下的一张表:canal.test1
     * 5.  多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔)
     * 注意:此过滤条件只针对row模式的数据有效(ps. mixed/statement因为不解析sql,所以无法准确提取tableName进行过滤)
     */
    private String subscribe;
}

CanalInit.java

package springboot.canal.demo.canalinit;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

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

/**
 * @description:
 * @author: dada
 * @date: 2020/10/29 10:36
 */
@Component
public class CanalInit implements ApplicationRunner {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private CanalInitBean canalInitBean;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        startCanal(canalInitBean);
    }

    private void startCanal(CanalInitBean canalInitBean) {
        // 创建链接
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(canalInitBean.getIp(), canalInitBean.getPort()), canalInitBean.getDestination(), canalInitBean.getUsername(), canalInitBean.getPassword());
        int batchSize = 1000;
        try {
            connector.connect();
//            connector.subscribe(".*\\..*");
            connector.subscribe(canalInitBean.getSubscribe());
            connector.rollback();
            while (true) {
                Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
                long batchId = message.getId();
                int size = message.getEntries().size();
                if (batchId == -1 || size == 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                } else {
                    // System.out.printf("message[batchId=%s,size=%s] \n", batchId, size);
                    printEntry(message.getEntries());
                }

                connector.ack(batchId); // 提交确认
                // connector.rollback(batchId); // 处理失败, 回滚数据
            }
        } finally {
            connector.disconnect();
        }

    }

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

            CanalEntry.RowChange rowChage = null;
            try {
                rowChage = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
                        e);
            }

            CanalEntry.EventType eventType = rowChage.getEventType();
            System.out.println(String.format("================&gt; binlog[%s:%s] , name[%s,%s] , eventType : %s",
                    entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
                    entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
                    eventType));

            // 变动的表名称
            String tableName = entry.getHeader().getTableName();
            for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {
                if (eventType == CanalEntry.EventType.DELETE) {
                    delete(rowData.getBeforeColumnsList(), tableName);

                } else if (eventType == CanalEntry.EventType.INSERT) {
                    insert(rowData.getAfterColumnsList(), tableName);

                } else {
                    System.out.println("-------&gt; before");
                    System.out.println("-------&gt; after");
                    updateAfter(rowData.getAfterColumnsList(), tableName);
                }
            }
        }
    }

    private void updateAfter(List<CanalEntry.Column> columns, String tableName) {
        // 循环将字段put到json对象里面
        insert(columns, tableName);
    }

    private void delete(List<CanalEntry.Column> columns, String tableName) {

        String key = "";
        for (CanalEntry.Column column : columns) {
            if (StrUtil.isEmpty(key) && column.getIsKey()) {
                key = column.getValue();
            }
        }

        if (StrUtil.isNotEmpty(key)) {
            redisTemplate.opsForHash().delete(tableName, key);
        }
    }

    private void insert(List<CanalEntry.Column> columns, String tableName) {

        // 循环将字段put到json对象里面
        JSONObject jsonObject = new JSONObject();
        String key = "";
        for (CanalEntry.Column column : columns) {
            System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
            jsonObject.putOpt(column.getName(), column.getValue());
            if (StrUtil.isEmpty(key) && column.getIsKey()) {
                key = column.getValue();
            }
        }
        redisTemplate.opsForHash().put(tableName, key, jsonObject);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值