谷粒学院16万字笔记+1600张配图(十九)——数据同步、网关

项目源码与所需资料
链接:https://pan.baidu.com/s/1azwRyyFwXz5elhQL0BhkCA?pwd=8z59
提取码:8z59

demo19-canal数据同步、网关

1.canal应用场景

在前面的统计分析功能中,我们采取了远程服务调用获取统计数据,这样耦合度高,效率相对较低。目前我采取另一种实现方式,通过实时同步数据库表的方式实现,例如我们要统计每天注册与登录人数,我们只需把会员表同步到统计库中,实现本地统计就可以了,这样效率更高,耦合度更低,Canal就是一个很好的数据库同步工具。canal是阿里巴巴旗下的一款开源项目,纯Java开发,目前只支持MySQL的数据同步

2.准备工作

2.1分析

1.需要有一个Linux虚拟机和一个本地Windows系统

2.Linux虚拟机中需要:

  • 安装MySQL数据库
  • 创建数据库和数据表
  • 安装canal数据同步工具

3.本地Windows系统中需要:

  • 安装MySQL数据库
  • 创建数据库和数据表

其中,在Linux创建的数据库数据表与在本地Windows系统创建的数据库数据表的名称和表结构一样

3.我们想要的效果是:linux库中的数据发生变化,那么我们本地的Windows库中的数据也跟着发生变化

2.2本地Windows中MySQL建库建表

1.我们早早就创建了数据库guli,我们这里直接在数据库guli上右键创建表

在这里插入图片描述

2.我这里表名是member,并且是有三个字段

在这里插入图片描述

2.3Linux虚拟机中MySQL建库建表

1.在"[email protected]"上右键选择"创建数据库"

在这里插入图片描述

2.我们在"demo03-后台讲师管理模块"的"2.1数据库设计"的第1步创建的是什么样的数据库,这里也就创建什么样的数据库

在这里插入图片描述

3.在"demo03-后台讲师管理模块"的"2.1数据库设计"的第1步创建数据库时"数据库排序规则"选择的是默认,然后创建数据库后人家自动给guli库的排序规则是"uf8mb4_general_ci"。可是比葫芦画瓢在linux的MySQL中创建的guli库人家给的排序规则不是"uf8mb4_general_ci",我也不知道为啥,反正现在先手动将这个排序规则设置为"uf8mb4_general_ci"吧

①在linux的guli数据库上右键选择"改变数据库…"

在这里插入图片描述

②选择"uf8mb4_general_ci"的数据库排序规则并点击"保存"

在这里插入图片描述

4.在linux的guli库上右键创建表

在这里插入图片描述

5.在"2.2本地Windows中MySQL建库建表"的第2步怎么填的,这里也怎么填

在这里插入图片描述

2.4开启binlog功能

1.canal的原理是基于mysql binlog技术,所以这里一定需要开启mysql的binlog写入功能

2.使用如下命令检查是否开启binlog功能,绿框圈起来的是ON,说明已开启

show variables like 'log_bin';

在这里插入图片描述

3.如果上图绿框是OFF则表示未开启,需要修改配置文件来开启binlog(自行上网查询),修改配置文件后记得重启mysql

3.安装Canal

3.1下载canal压缩包

1.下载地址:

https://github.com/alibaba/canal/releases

2.老师用的是canal.deployer-1.1.4tar.gz版的,所以我这里下载的也是这个版本

在这里插入图片描述

3.2将canal压缩文件上传到linux

使用Xftp将上一步下载的压缩文件上传到linux虚拟机中(我这里是上传到了opt目录下)

在这里插入图片描述

3.3解压canal压缩文件

1.执行cd /opt进入到opt目录,先使用如下命令在opt目录下创建一个文件夹用于存放解压后的canal

mkdir canal

在这里插入图片描述

2.再使用如下命令将canal压缩文件解压到上一步创建的canal目录下

tar zxvf canal.deployer-1.1.4.tar.gz -C canal

在这里插入图片描述

3.4修改配置文件

1.使用如下命令来以编辑模式打开canal的配置文件instance.properties

vi /opt/canal/conf/example/instance.properties

在这里插入图片描述

2.输入i进入插入模式,第一处修改后如下图:

  • 红框圈起来的是linux的ip(填写自己linux虚拟机的ip,注意:输入自己的ip时不能使用键盘右方的数字0-9,而是需要使用键盘上方的数字0-9)
    • 老师说了,这里的ip不修改也是可以的,使用默认的127.0.0.1就可以,但还是建议修改
  • 绿框圈起来的是linux中的mysql的端口号(默认是3306)

在这里插入图片描述

3.第二处修改后如下图:

  • 红框圈起来的是用户名
  • 绿框圈起来的是用户密码
  • 用户名和密码填写在"8.9远程连接MySQL"的第2步自己设置的用户名和密码

在这里插入图片描述

4.第四处不需要修改,就使用配置文件中默认的就行:

  • 这个配置的作用是:规定让所有库和所有表都匹配

在这里插入图片描述

一些其它的匹配规则如下:

  • 所有库和所有表:.*.*\\..*

  • canal库下的所有表:canal\\..*

  • canal库下的以canal开头的表:canal\\.canal.*

  • canal库下某一张具体的表:canal.test1

  • 多规则组合使用(逗号分隔):canal\\..*,mysql.test1,mysql.test2

5.修改后先按Esc退出插入模式,然后输入:wq保存并退出

3.5启动、关闭canal数据同步工具

1.先使用如下命令进入到bin目录

cd /opt/canal/bin

在这里插入图片描述

2.然后在bin目录下使用如下命令启动canal数据同步工具

./startup.sh

在这里插入图片描述

3.如果想要关闭canal数据同步工具,则在bin目录下使用如下命令

./stop.sh

在这里插入图片描述

4.客户端代码

4.1创建子模块canal_clientedu

1.在总模块guli_parent上右键选择New–>Module…

在这里插入图片描述

2.创建一个Maven项目

在这里插入图片描述

3.填写信息后点击"Finish"

在这里插入图片描述

4.2引入依赖

在canal_clientedu模块的pom.xml中添加如下代码来引入需要的依赖(别忘了刷新maven)

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

    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>commons-dbutils</groupId>
        <artifactId>commons-dbutils</artifactId>
    </dependency>

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

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

在这里插入图片描述

4.3配置application.properties

创建配置文件application.properties并编写配置

# 服务端口
server.port=10000
# 服务名
spring.application.name=canal-client

# 环境设置:dev、test、prod
spring.profiles.active=dev

# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root

在这里插入图片描述

4.4创建启动类

在canal_clientedu模块的java包下创建包com.atguigu.canal,然后在canal包下创建启动类CanalApplication

@SpringBootApplication
public class CanalApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(CanalApplication.class, args);
    }
}

在这里插入图片描述

4.5编写canal客户端类

在canal包下创建包client,然后在client包下创建客户端类CanalClient(代码是固定代码,不要求能手敲,只要能看懂,会修改就行了【我看不懂,目前也没打算看懂】)

package com.atguigu.canal.client;

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.InvalidProtocolBufferException;
import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.net.InetSocketAddress;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

@Component
public class CanalClient {
    
    

    //sql队列
    private Queue<String> SQL_QUEUE = new ConcurrentLinkedQueue<>();

    @Resource
    private DataSource dataSource;

    /**
     * canal入库方法
     */
    public void run() {
    
    

        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.44.132",
                11111), "example", "", "");
        int batchSize = 1000;
        try {
    
    
            connector.connect();
            connector.subscribe(".*\\..*");
            connector.rollback();
            try {
    
    
                while (true) {
    
    
                    //尝试从master那边拉去数据batchSize条记录,有多少取多少
                    Message message = connector.getWithoutAck(batchSize);
                    long batchId = message.getId();
                    int size = message.getEntries().size();
                    if (batchId == -1 || size == 0) {
    
    
                        Thread.sleep(1000);
                    } else {
    
    
                        dataHandle(message.getEntries());
                    }
                    connector.ack(batchId);

                    //当队列里面堆积的sql大于一定数值的时候就模拟执行
                    if (SQL_QUEUE.size() >= 1) {
    
    
                        executeQueueSql();
                    }
                }
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } catch (InvalidProtocolBufferException e) {
    
    
                e.printStackTrace();
            }
        } finally {
    
    
            connector.disconnect();
        }
    }

    /**
     * 模拟执行队列里面的sql语句
     */
    public void executeQueueSql() {
    
    
        int size = SQL_QUEUE.size();
        for (int i = 0; i < size; i++) {
    
    
            String sql = SQL_QUEUE.poll();
            System.out.println("[sql]----> " + sql);

            this.execute(sql.toString());
        }
    }

    /**
     * 数据处理
     *
     * @param entrys
     */
    private void dataHandle(List<Entry> entrys) throws InvalidProtocolBufferException {
    
    
        for (Entry entry : entrys) {
    
    
            if (EntryType.ROWDATA == entry.getEntryType()) {
    
    
                RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
                EventType eventType = rowChange.getEventType();
                if (eventType == EventType.DELETE) {
    
    
                    saveDeleteSql(entry);
                } else if (eventType == EventType.UPDATE) {
    
    
                    saveUpdateSql(entry);
                } else if (eventType == EventType.INSERT) {
    
    
                    saveInsertSql(entry);
                }
            }
        }
    }

    /**
     * 保存更新语句
     *
     * @param entry
     */
    private void saveUpdateSql(Entry entry) {
    
    
        try {
    
    
            RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
            List<RowData> rowDatasList = rowChange.getRowDatasList();
            for (RowData rowData : rowDatasList) {
    
    
                List<Column> newColumnList = rowData.getAfterColumnsList();
                StringBuffer sql = new StringBuffer("update " + entry.getHeader().getTableName() + " set ");
                for (int i = 0; i < newColumnList.size(); i++) {
    
    
                    sql.append(" " + newColumnList.get(i).getName()
                            + " = '" + newColumnList.get(i).getValue() + "'");
                    if (i != newColumnList.size() - 1) {
    
    
                        sql.append(",");
                    }
                }
                sql.append(" where ");
                List<Column> oldColumnList = rowData.getBeforeColumnsList();
                for (Column column : oldColumnList) {
    
    
                    if (column.getIsKey()) {
    
    
                        //暂时只支持单一主键
                        sql.append(column.getName() + "=" + column.getValue());
                        break;
                    }
                }
                SQL_QUEUE.add(sql.toString());
            }
        } catch (InvalidProtocolBufferException e) {
    
    
            e.printStackTrace();
        }
    }

    /**
     * 保存删除语句
     *
     * @param entry
     */
    private void saveDeleteSql(Entry entry) {
    
    
        try {
    
    
            RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
            List<RowData> rowDatasList = rowChange.getRowDatasList();
            for (RowData rowData : rowDatasList) {
    
    
                List<Column> columnList = rowData.getBeforeColumnsList();
                StringBuffer sql = new StringBuffer("delete from " + entry.getHeader().getTableName() + " where ");
                for (Column column : columnList) {
    
    
                    if (column.getIsKey()) {
    
    
                        //暂时只支持单一主键
                        sql.append(column.getName() + "=" + column.getValue());
                        break;
                    }
                }
                SQL_QUEUE.add(sql.toString());
            }
        } catch (InvalidProtocolBufferException e) {
    
    
            e.printStackTrace();
        }
    }

    /**
     * 保存插入语句
     *
     * @param entry
     */
    private void saveInsertSql(Entry entry) {
    
    
        try {
    
    
            RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
            List<RowData> rowDatasList = rowChange.getRowDatasList();
            for (RowData rowData : rowDatasList) {
    
    
                List<Column> columnList = rowData.getAfterColumnsList();
                StringBuffer sql = new StringBuffer("insert into " + entry.getHeader().getTableName() + " (");
                for (int i = 0; i < columnList.size(); i++) {
    
    
                    sql.append(columnList.get(i).getName());
                    if (i != columnList.size() - 1) {
    
    
                        sql.append(",");
                    }
                }
                sql.append(") VALUES (");
                for (int i = 0; i < columnList.size(); i++) {
    
    
                    sql.append("'" + columnList.get(i).getValue() + "'");
                    if (i != columnList.size() - 1) {
    
    
                        sql.append(",");
                    }
                }
                sql.append(")");
                SQL_QUEUE.add(sql.toString());
            }
        } catch (InvalidProtocolBufferException e) {
    
    
            e.printStackTrace();
        }
    }

    /**
     * 入库
     * @param sql
     */
    public void execute(String sql) {
    
    
        Connection con = null;
        try {
    
    
            if(null == sql) return;
            con = dataSource.getConnection();
            QueryRunner qr = new QueryRunner();
            int row = qr.execute(con, sql);
            System.out.println("update: "+ row);
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            DbUtils.closeQuietly(con);
        }
    }
}

代码修改的地方:

  • 第35行ip地址填写自己linux虚拟机的ip

4.6修改启动类

我们想要本地库能够和远程(linux虚拟机)库做同步,远程库中加数据了那本地库就需要加数据,远程库中修改数据了那本地库也就需要修改数据。想要实现这个目的就要保证我们的程序一直处于监控状态,一直监控着远程库中的变化。在"4.5编写canal客户端类"编写的类中的run方法是用来监控远程库并且做相关操作的,所以我么需要让该方法一直处于运行状态,那就需要修改启动类:

1.让启动类实现接口CommandLineRunner

在这里插入图片描述

2.在启动类中添加如下代码

@Resource
private CanalClient canalClient;
@Override
public void run(String... strings) throws Exception {
    
    
    //项目启动,执行canal客户端监听
    canalClient.run();
}

在这里插入图片描述

  • 重写了CommandLineRunner接口的run方法,CommandLineRunner接口的run方法的作用是:只要程序还在执行,就会一直执行重写后的CommandLineRunner接口的run方法中的代码

4.7测试

1.后端服务只需要启动canal_clientedu即可,并且在linux中启动canal

2.使用SQLyog给linux中的库插入数据

INSERT INTO member VALUES(1,'lucy',20)

在这里插入图片描述

3.可以看到本地库的member表也跟着插入了一条数据,说明我们数据同步成功

在这里插入图片描述

5.网关

5.1网关概念

1.在客户端和服务端中间的一面墙,可以起到的作用有很多,比如:请求转发、负载均衡、权限控制…

2.老师说了,网关服务也需要在注册中心进行注册(不是很理解)

3.客户端访问时会先通过网关,网关中会做这些处理:先在Gateway Handler Mapping中做路径匹配,如果可以匹配上,就进入Gateway Web Handler开始执行,然后接着进入Filter过滤器,我们可以在过滤器中实现权限管理、跨域…

在这里插入图片描述

4.Spring Cloud Gateway中几个重要的概念:

  • 路由:路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter组成。如果断言路由为真,则说明请求的URL和配置匹配
  • 断言:Java8中的断言函数。说白了就是声明匹配规则,比如用户现在访问eduservice,就让它跳转到8001端口
  • 过滤器:对请求和响应进行修改,使得可以实现权限管理、跨域、统一异常处理…

5.2创建子子模块api_gateway

1.先创建子模块infrastructure:

①在总项目guli_parent上右键选择New–>Module…

在这里插入图片描述

②创建一个Maven工程

在这里插入图片描述

③填写信息后点击"Finish"

在这里插入图片描述

④因为该子模块下面还有子模块,所以要在infrastructure模块的pom.xml中添加如下代码,表示将这个子工程改为pom类型(别忘了刷新maven)

<packaging>pom</packaging>

在这里插入图片描述

⑤因为我们不要在infrastructure模块中写代码,所以将该模块下的src目录删掉

在这里插入图片描述

2.在infrastructure模块上右键选择New–>Module…

在这里插入图片描述

3.创建一个Maven工程

在这里插入图片描述

4.填写信息后点击"Finish"

在这里插入图片描述

5.3添加依赖

在api_gateway模块的pom.xml中添加依赖(别忘了刷新maven)

<dependencies>
    <dependency>
        <groupId>com.atguigu</groupId>
        <artifactId>common_utils</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <!--gson-->
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
    </dependency>

    <!--服务调用-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

在这里插入图片描述

5.4创建启动类

在api_gateway模块的java包下创建包com.atguigu.gateway,然后在gateway包下创建启动类ApiGatewayApplication

@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

在这里插入图片描述

因为网关服务需要在nacos中注册,所以需要给启动类添加注解@EnableDiscoveryClient

5.5配置application.properties

创建配置文件application.properties并编写配置

# 服务端口
server.port=8222
# 服务名
spring.application.name=service-gateway
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

#使用服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true

#设置路由id
spring.cloud.gateway.routes[0].id=service-acl
#设置路由的uri
spring.cloud.gateway.routes[0].uri=lb://service-acl
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[0].predicates= Path=/*/acl/**

#配置service-cms服务
#设置路由id(可以随便起,但建议写服务名字)
spring.cloud.gateway.routes[1].id=service-cms
#设置路由的uri    lib://在nacos注册的服务名称
spring.cloud.gateway.routes[1].uri=lb://service-cms
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[1].predicates= Path=/educms/**

spring.cloud.gateway.routes[2].id=service-edu
spring.cloud.gateway.routes[2].uri=lb://service-edu
spring.cloud.gateway.routes[2].predicates= Path=/eduservice/**

spring.cloud.gateway.routes[3].id=service-msm
spring.cloud.gateway.routes[3].uri=lb://service-msm
spring.cloud.gateway.routes[3].predicates= Path=/edumsm/**

spring.cloud.gateway.routes[4].id=service-order
spring.cloud.gateway.routes[4].uri=lb://service-order
spring.cloud.gateway.routes[4].predicates= Path=/eduorder/**

spring.cloud.gateway.routes[5].id=service-oss
spring.cloud.gateway.routes[5].uri=lb://service-oss
spring.cloud.gateway.routes[5].predicates= Path=/eduoss/**

spring.cloud.gateway.routes[6].id=service-statistics
spring.cloud.gateway.routes[6].uri=lb://service-statistics
spring.cloud.gateway.routes[6].predicates= Path=/staservice/**

#配置service-ucenter服务
spring.cloud.gateway.routes[7].id=service-ucenter
spring.cloud.gateway.routes[7].uri=lb://service-ucenter
spring.cloud.gateway.routes[7].predicates= Path=/educenter/**

spring.cloud.gateway.routes[8].id=service-vod
spring.cloud.gateway.routes[8].uri=lb://service-vod
spring.cloud.gateway.routes[8].predicates= Path=/eduvod/**

第9行的spring.cloud.gateway.discovery.locator.enabled=true表示使用Gateway的服务发现实现调用(和使用nginx实现请求转发不同的是:nginx是通过路径进行匹配,而使用Gateway时是通过服务发现进行匹配【个人理解,不知道对不对】)

5.6测试

1.启动nacos,启动后端项目中的service_edu和api_gateway

2.在地址栏输入http://localhost:8222/eduservice/subject/getAllSubject可以看到数据,说明我们配置成功

在这里插入图片描述

5.7实现负载均衡

Gateway已经给我们封装了负载均衡,我们什么也不用配置,就能实现负载均衡:假如我把service_edu服务做了集群(实现的功能一样,服务名相同,只是端口号不同),将这个服务放到两台服务器上,端口号分别是8101、8102,当客户端访问eduservice时,Gateway会根据一定规则(轮询、权重、最少请求时间)来决定将这个请求分配给8101还是8102,这就是负载均衡

在这里插入图片描述

5.8实现跨域、权限管理、异常处理

1.都是固定代码,不用敲,这部分的代码放到了资料中

在这里插入图片描述

2.将上图的三个文件夹复制粘贴到api_gateway模块的gateway包下

在这里插入图片描述

  • CorsConfig类是一个配置类,添加了一个插件,使得让所有请求都实现跨域
  • AuthGlobalFilter的作用:规定了:哪些请求可以访问、哪些请求不可以访问、当访问失败了会输出什么值

3.因为我们已经在CorsConfig类中实现了跨域,所以需要将service模块下的所有的跨域注解@CrossOrigin都注释掉或删掉,否则会报错(但实际上,因为我一点也看不懂CorsConfig、AuthGlobalFilter、ErrorHandlerConfig、JsonExceptionHandler这四个类,不敢乱用,所以我将这四个类全部注释掉了,使用的请求转发仍是使用nginx来实现)

4.将前端项目vue-admin-1010的config目录下的dev.env.js文件的地址中的9001改为网关的端口号8222

在这里插入图片描述

5.将前端项目vue-front-1010的utils目录下的request.js文件的地址中的9001改为网关的端口号8222

在这里插入图片描述

看评论说后面的权限管理全是cv,老师赶时间,也没细说具体怎么实现的,起初我还想着听一遍混个眼熟,直到我看完网关,我意识到了,老师是真的没时间了,全是cv,关键我水平不够,一点也不讲解的话我根本理解不了,所以后面的权限管理暂且不跟了,这个项目到这里先告一段落吧,肝面经去了。

2022.07.272022.09.22完结撒花~~~

猜你喜欢

转载自blog.csdn.net/maxiangyu_/article/details/127033142