项目源码与所需资料
链接: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
就可以,但还是建议修改
- 老师说了,这里的ip不修改也是可以的,使用默认的
- 绿框圈起来的是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完结撒花~~~