[会写代码的健身爱好者成长史]之阿里canal数据实时同步-附kafka和Elasticsearch实现

目录

1.什么是canal

2.canal工作原理

3.下载安装

 4.连接配置kafka

1.打开config目录下的canal.properties文件

 2.修改相关配置

 3.mysql相关配置

3.1验证binary log日志是否开启

3.2 关于mysql如何监控多个数据库以及canal监控单个表以及多个数据库中不同表的一些设置 

3.2.1 mysql如何监控多个数据库

 3.2.2 canal 如何监控单个表或者不同数据库的不同的表

4.测试kafka

5.canal实现同步Elasticsearch

1.添加依赖

2.编写代码



1.什么是canal

canal是阿里开源针对mysql的一个数据实时同步的一个框架,可以实现无代码,简单配置即可完成数据库数据的同步到kafka,rabbitMQ等消息,增加,修改,删除

2.canal工作原理

canal的工作原理:其实就是mysql的主从复制,只不过canal不是真正的从节点,而是伪装成mysql的slave从节点,模拟mysql从节点的交互方式,给主节点发送dump请求,主节点接收到了之后,会把binary log日志推送给canal,然后canal进行解析,从而发送到存储目的地

3.下载安装

下载canal.deployer服务端

canal下载地址,下载1.14,1.15都可以,但是1.15才能支持es7.x以上的版本注意一下

这里以1.15为例,下载解压后

bin目录下有个startup.bat 双击即可打开

 4.连接配置kafka

首先要修改几个配置

1.打开config目录下的canal.properties文件

 2.修改相关配置

2.1 往下拉会看见关于kafka相关的配置

2.2 在config目录下有个example文件,点击后会看见instance.properties文件,打开配置一下kafka的主题,以下操作都是在instance.properties文件里面

 2.3如果数据库不在本地,记得修改下这里

 2.4如果没有其他的设置,这里的username/password要修改为数据库的账户和密码

 2.5这里的slaveId要打开,值不要和mysql配置的一样就好

 

 3.mysql相关配置

这里以mysql5.7.20解压版本为例

解压版本目录下是没有my.ini文件,自行创建一个就好,具体解压版要如何安装自己百度看下,这里只说一下canal相关的配置

 因为mysql默认是不开binary log日志的,所以第一步要开启binary log日志,在my.ini配置一下就好,打开my.ini,务必在[mysqld]下面添加这四行,不在mysqld下面添加不生效,server-id=1如果有可以不加

log-bin=mysql-bin # 开启Binlog 一般只需要修改这一行即可
binlog-format=ROW # 设置格式 此行可以不加 命令设置即可 详见下方拓展
binlog-do-db=test # 监控mysql中test这个库下面的所有表
server_id=1  # 配置serverID 

3.1验证binary log日志是否开启

要重启mysql不然配置的不生效,再mysql的bin路径下

D:\mysql-5.7.20-winx64\bin  输入如下命令,要用管理员的方式进cmd命令

首先应该先关闭mysql服务

net stop mysql

然后再开启mysql服务

net start mysql

然后登录mysql看下日志是否开启

mysql -uroot -p
show variables like 'log_bin';

3.2 关于mysql如何监控多个数据库以及canal监控单个表以及多个数据库中不同表的一些设置 

3.2.1 mysql如何监控多个数据库

 3.2.2 canal 如何监控单个表或者不同数据库的不同的表

前提是mysql的my.ini文件里面不要配置binlog-do-db=数据库名,如果不配置这个,mysql会把全库全表的数据上的变化发到binlog文件中,这时候在canal里面进行配置过滤一下即可

 1.打开D:\canal\conf\example 下面的instance.properties文件

监控单表

1.1 修改canal的instance.properties配置文件

1.2或者在java代码订阅数据库的时候,配置一下就好

如果无需写java代码(如:kafka,rabbitmq等消息队列)就只能修改配置文件了

ps:如果修改了配置文件,java代码可以不写,写了也没事,如果写和配置文件对应即可

如果不想改配置文件也许,不动即可,配置文件默认还是监控全库全表,在Java代码中配置一下即可

1.3 配置文件 

类型配置文件翻译写法
全库全表canal.instance.filter.regex全库全表.*\\..*
指定库全表canal.instance.filter.regex库名\..* test\..*
单表canal.instance.filter.regex库名.表名test.user
多规则组合canal.instance.filter.regex库名\..*,库名2.表名1,库名3.表名2 (逗号分隔)test\..*,test2.user1,test3.user2 (逗号分隔)

 1.4 java代码写法

全库全表canalConnector.subscribe(".*\\..*");
指定库全表canalConnector.subscribe("test\..*");
单表canalConnector.subscribe("test.user");
多规则组合canalConnector.subscribe("test\..*,test2.user1,test3.user2);

 

 

 开启之后就可以开始测试了!

4.测试kafka

首先要下载安装kafka,然后打开kafka,进入之前在canal配置文件里面配置的topic主题监控

需要在kafka的路径下输入,D:\kafka_2.11-2.4.1

打开zookeeper
.\bin\windows\zookeeper-server-start.bat config\zookeeper.properties
打开kafka
.\bin\windows\kafka-server-start.bat .\config\server.properties
进入topic
.\bin\windows\kafka-console-consumer.bat --bootstrap-server 10.88.40.156:9092 --topic topic2 --from-beginning

创建topic
.\bin\windows\kafka-topics.bat --create --topic topic3 --partitions 3 --replication-factor 1 --zookeeper 10.88.40.156:2181

在数据库添加一条数据

 去topic看下是否发送到了

 ok结束,canal数据同步到kafka结束


5.canal实现同步Elasticsearch

canal1.15才支持es7.x以上

记得修改canal的config目录下的canal.properties文件

# tcp, kafka, rocketMQ, rabbitMQ
canal.serverMode = tcp  #如果是es修改为tcp模式

1.添加依赖

        <!--注意和自己下载的Elasticsearch版本对应-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        <!-- fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
        <!--网页解析的包-->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.13.1</version>
        </dependency>
        <!--  这里依赖用的是1.1.4,1.1.5有一个包加不进去,之前以为依赖和canal的版本号不一致会出错,结果不会-->
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>1.1.4</version>
        </dependency>

2.编写代码

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.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mybatisplus.entity.Content;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;

@Component
public class CanalClient  {
    //@PostConstruct项目启动是就会加载这个方法,用这个注解可以实现此方法一直属于运行的状态,只要数据库发生增加,修改和删除就会被获取到
    @PostConstruct
    public  List<Content> canal() throws InvalidProtocolBufferException {
        List<Content> contentList = new ArrayList<>();
        //获取连接
        CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress("127.0.0.1",11111),
                "example","","");
        //连接
        canalConnector.connect();
        //订阅数据库
        canalConnector.subscribe("test.*");
        //获取数据,一次抓取100条,没有100条就全部抓完,有多少抓多少,并不会堵塞
        Message message = canalConnector.get(100);
        //获取Entry集合
        List<CanalEntry.Entry> entries = message.getEntries();
        //如果集合为空,就等待几秒
        if (!entries.isEmpty()){
            //遍历entries逐个解析
            for (CanalEntry.Entry entry : entries) {
                //1.获取表名
                String tableName = entry.getHeader().getTableName();
                //2.获取获取类型
                CanalEntry.EntryType entryType = entry.getEntryType();
                //3.获取序列化后的数据
                ByteString storeValue = entry.getStoreValue();
                //4.判断当前的数据类型是否为rowData类型
                if (CanalEntry.EntryType.ROWDATA.equals(entryType)){
                    //反序列化数据
                    CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue);
                    //获取当前事件的类型
                    CanalEntry.EventType eventType = rowChange.getEventType();
                    //获取数据集
                    List<CanalEntry.RowData> rowDataList = rowChange.getRowDatasList();
                    //遍历rowDataList数据集并打印
                    for (CanalEntry.RowData rowData : rowDataList) {
                        //修改之后的数据
                        JSONObject afterData = new JSONObject();
                        List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
                        for (CanalEntry.Column column : afterColumnsList) {
                            //name字段名,value是字段名对应的值
                            afterData.put(column.getName(),column.getValue());
                            //将JSONObject类型的数据转化为content对象
                            Content content = afterData.toJavaObject(Content.class);
                            contentList.add(content);
                        }
                        System.out.println("表名:"+tableName+",类型"+eventType+",after:"+contentList);
                    }
                }
            }
        }
        return contentList;
    }
}

连接Elasticsearch,写个config类

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @create : 2022/01/14 15:24
 */
@Configuration
public class ElasticsearchConfig {

    @Bean
    public RestHighLevelClient restHighLevelClient(){
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(
                RestClient.builder(new HttpHost("localhost",9200,"http"))
        );
        return restHighLevelClient;
    }
}

 解析网页数据,拿到数据放到数据库,从而实现数据库变化并实时同步到es索引库中

import com.mybatisplus.entity.Content;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * @create : 2022/01/17 14:11
 */
@Component
public class HtmlParseUtil {

    public List<Content> parseJD(String keyword) throws IOException {
        List<Content> contentList = new ArrayList<>();
        //获得请求
        String url = "https://search.xxxxx.com/Search?keyword="+keyword;
        //解析地址,如果30s内解析不到就会报错
        // jsoup返回的 document 对象就是 js浏览器里面的document对象
        Document parse = Jsoup.parse(new URL(url), 30000);
        //先获取id=J_goodsList的大的一个商品div
        Element elementById = parse.getElementById("J_goodsList");
        //再获取包含一个个商品的li标签
        Elements li = elementById.getElementsByTag("li");
        //循环这个li,这里的li包含的就是一个个商品,商品有价格,名称,店铺名,图片地址等等
        for (Element element : li) {
            //为了用户体验,一般大厂的网页图片都懒加载,刚开始只会加载一个默认图片,正在的图片地址在这个data-lazy-img 里面,而不是在src里面
            String img = element.getElementsByTag("img").eq(0).attr("data-lazy-img");
            String price = element.getElementsByClass("p-price").eq(0).text();
            String name = element.getElementsByClass("p-name").eq(0).text();
            String shop = element.getElementsByClass("p-shopnum").eq(0).text();
            Content content = new Content();
            content.setImg(img);
            content.setPrice(price);
            content.setTitle(name);
            content.setShop(shop);
            contentList.add(content);
        }
        return contentList;
    }

 实体类

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

/**
 * @create : 2022/01/17 14:11
 */
@Data
public class Content {
    //myBatis-plus 的id自增
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String title;
    private String price;
    private String img;
    private String shop;
}

 mapper层代码

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mybatisplus.entity.Content;
import org.springframework.stereotype.Repository;

/**
 * @create : 2022/01/11 10:51
 */
@Repository
public interface ContentMapper extends BaseMapper<Content> {

}

service代码

import com.alibaba.fastjson.JSON;
import com.mybatisplus.canal.CanalClient;
import com.mybatisplus.entity.Content;
import com.mybatisplus.mapper.ContentMapper;
import com.mybatisplus.until.HtmlParseUtil;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @create : 2022/01/17 16:19
 */
@Service
public class ContentService {

    @Autowired
    private RestHighLevelClient client;

    @Autowired
    private ContentMapper contentMapper;

    @Autowired
    private HtmlParseUtil htmlParseUtil;

    @Autowired
    private CanalClient canalClient;

    //将解析的网页数据保存到数据库中
    public void insertData(String keyword) throws IOException {
        List<Content> contentList = htmlParseUtil.parseJD(keyword);
        for (Content content : contentList) {
            contentMapper.insert(content);
        }
    }

    //将数据库中的数据更新到es索引库中
    public boolean saveES(String keyword) throws IOException {
        //网页解析存到数据库
        insertData(keyword);
        //通过canal拿到新增的数据
        List<Content> canal = canalClient.canal();
        //数据库批量插入到es索引库中
        BulkRequest bulkRequest = new BulkRequest("jd","jd_goods");
        bulkRequest.timeout();
        for (Content content : canal) {
            bulkRequest.add(new IndexRequest("jd").id(String.valueOf(content.getId())).source(JSON.toJSONString(content), XContentType.JSON));
        }
        BulkResponse bulk = client.bulk(bulkRequest, RequestOptions.DEFAULT);
        //批量插入成功返回false,所以加个非!
        return !bulk.hasFailures();
    }

    //查询数据,分页
    public List<Map<String,Object>> search(String keyword,Integer pageNo,Integer pageSize) throws IOException {
        List<Map<String,Object>> mapList = new ArrayList<>();
        //构建查询请求
        SearchRequest searchRequest = new SearchRequest();
        SearchSourceBuilder builder = new SearchSourceBuilder();
        //模糊查询
        if (keyword.matches("^[A-Za-z0-9]+$")){
            //模糊查询,主要针对于英文,如果你想搜索java,matchQuery可能查询出来,但是如果你只输入jav,那么你就没办法搜索到java
            //这时候需要用模糊查询fuzzyQuery
            builder.query(QueryBuilders.fuzzyQuery("title",keyword).fuzziness(Fuzziness.TWO));
        }else {
            //正常中英文查询
            builder.query(QueryBuilders.matchQuery("title",keyword));
        }
        //分页设置
        builder.from(pageNo);
        builder.size(pageSize);
        builder.timeout(new TimeValue(60, TimeUnit.SECONDS));
        searchRequest.source(builder);
        SearchResponse search = client.search(searchRequest, RequestOptions.DEFAULT);
        for (SearchHit hit : search.getHits().getHits()) {
            mapList.add(hit.getSourceAsMap());
        }
        return mapList;
    }

}

 controller层

import com.mybatisplus.service.ContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
 * @create : 2022/01/17 16:16
 */
@RestController
public class ContentController {

    @Autowired
    private ContentService contentService;

    //添加数据
    @GetMapping("/add/{keyword}")
    public boolean add(@PathVariable("keyword") String keyword) throws IOException {
        return contentService.saveES(keyword);
    }

    //查询数据
    @GetMapping("/search/{keyword}/{pageNo}/{pageSize}")
    public List<Map<String,Object>> search(@PathVariable("keyword") String keyword,
                                           @PathVariable("pageNo") Integer pageNo,
                                           @PathVariable("pageSize") Integer pageSize) throws IOException {
       return contentService.search(keyword, pageNo, pageSize);
    }
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个简单的示例代码,演示了如何使用Spring Boot整合Easy-ES、CanalKafka实现MySQL数据同步Elasticsearch,支持全量和增量刷新数据。 1. 添加依赖 在pom.xml中添加以下依赖: ```xml <!-- Easy-ES --> <dependency> <groupId>com.github.a2619388896</groupId> <artifactId>easy-es-spring-boot-starter</artifactId> <version>1.0.2</version> </dependency> <!-- Canal --> <dependency> <groupId>com.alibaba.otter</groupId> <artifactId>canal.client</artifactId> <version>1.1.5</version> </dependency> <!-- Kafka --> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> <version>2.6.7</version> </dependency> ``` 2. 配置文件 在application.yml中配置Easy-ES、CanalKafka: ```yaml spring: data: elasticsearch: cluster-name: elasticsearch cluster-nodes: localhost:9300 kafka: bootstrap-servers: localhost:9092 consumer: group-id: my-group auto-offset-reset: earliest properties: max.poll.interval.ms: 600000 canal: host: 127.0.0.1 port: 11111 destination: example username: canal password: canal filter: include: - .*\\..* exclude: - example\\..* ``` 3. Canal客户端 使用Canal客户端连接到Canal Server,监听MySQL的变更事件,并将变更事件发送到Kafka: ```java @Component public class CanalClient { private static final Logger logger = LoggerFactory.getLogger(CanalClient.class); @Autowired private CanalConnector canalConnector; @Autowired private KafkaTemplate<String, String> kafkaTemplate; @PostConstruct public void start() { new Thread(() -> { int batchSize = 1000; try { canalConnector.connect(); canalConnector.subscribe(".*\\..*"); canalConnector.rollback(); while (true) { Message message = canalConnector.getWithoutAck(batchSize); long batchId = message.getId(); int size = message.getEntries().size(); if (batchId == -1 || size == 0) { Thread.sleep(1000); } else { List<CanalEntry.Entry> entries = message.getEntries(); for (CanalEntry.Entry entry : entries) { if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) { CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue()); String database = entry.getHeader().getSchemaName(); String table = entry.getHeader().getTableName(); EventType eventType = rowChange.getEventType(); for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) { Map<String, Object> data = new HashMap<>(); for (CanalEntry.Column column : rowData.getAfterColumnsList()) { data.put(column.getName(), column.getValue()); } String json = new ObjectMapper().writeValueAsString(data); kafkaTemplate.send(database + "." + table, json); } } } canalConnector.ack(batchId); } } } catch (Exception e) { logger.error("Canal client error", e); } finally { canalConnector.disconnect(); } }).start(); } } ``` 4. Kafka消费者 使用Kafka消费者从Kafka中读取变更事件,并将变更事件同步Elasticsearch: ```java @Component public class KafkaConsumer { private static final Logger logger = LoggerFactory.getLogger(KafkaConsumer.class); @Autowired private ElasticsearchOperations elasticsearchOperations; @KafkaListener(topics = "${canal.filter.include[0]}") public void consume(ConsumerRecord<String, String> record) { try { String[] topicParts = record.topic().split("\\."); String indexName = topicParts[1]; String json = record.value(); Map<String, Object> data = new ObjectMapper().readValue(json, new TypeReference<>() {}); String id = (String) data.get("id"); data.remove("id"); IndexRequest indexRequest = new IndexRequest(indexName); indexRequest.id(id); indexRequest.source(data, XContentType.JSON); UpdateRequest updateRequest = new UpdateRequest(indexName, id); updateRequest.doc(data, XContentType.JSON); updateRequest.upsert(indexRequest); elasticsearchOperations.update(updateRequest); } catch (Exception e) { logger.error("Kafka consumer error", e); } } } ``` 5. 完成 现在,只要启动应用程序,就可以将MySQL中的数据同步Elasticsearch了。如果需要进行全量刷新,只需简单地从MySQL中复制数据Elasticsearch即可。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值