首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…
一 .前言
这一篇开始深入学习 Mycat 的相关源码 , 以及从源码中试图分析出可定制点以及扩展的方式.
从 Git 上可以拉取对应的源码 Mycat-Server , 其核心启动类为 MycatStartup , Mycat 是国内开源 , 相对而言注释很清晰 , 这一篇主要是代码搬运和学习
Mycat 项目结构
|-- main
| |-- java
| | `-- io
| | `-- mycat
| | |-- MycatServer.java
| | |-- MycatShutdown.java
| | |-- MycatStartup.java : 启动类 , 会构建 MycatServer 进行启动
| | |-- backend : 结果返回等处理
| | |-- buffer : NIO Buffer 实现 , 仿 Netty
| | |-- cache
| | |-- catlets : Join 语句等解析
| | |-- config
| | |-- manager
| | |-- memory
| | |-- migrate : 自动扩容等
| | |-- net : NIO 连接 , 数据库连接相关类
| | |-- route : Route 规则处理
| | |-- server : TODO
| | |-- sqlengine : SQL 执行器
| | |-- statistic
| | `-- util
| `-- resources
| |-- cacheservice.properties
| |-- ehcache.xml
| |-- log4j2.xml
| |-- rule.xml : 分片规则
| |-- schema.xml : 节点规则
| |-- server.xml : Mysql 数据信息
| |-- zkconf : Zookeeper 集群配置
| | |-- rule.xml
| | |-- schema.xml
| | |-- server.xml
| `-- zkdownload
| `-- auto-sharding-long.txt
二. 源码启动和格局
Mycat 的启动类是 , 其会调用 MycatServer 进行初始化操作
public final class MycatStartup {
public static void main(String[] args) {
// S1 : 如果通过 Zookeeper 进行配置 ,此处会进行初始化
ZkConfig.getInstance().initZk();
// S2 : 获取 Home 路径
String home = SystemConfig.getHomePath();
// S3 : 获取 MycatServer 实例对象
MycatServer server = MycatServer.getInstance();
// S4 : 启动
server.startup();
}
}
2.1 核心启动类
private MycatServer() {
//读取文件配置
this.config = new MycatConfig();
//定时线程池,单线程线程池
scheduler = Executors.newSingleThreadScheduledExecutor();
//心跳调度独立出来,避免被其他任务影响
heartbeatScheduler = Executors.newSingleThreadScheduledExecutor();
//SQL记录器
this.sqlRecorder = new SQLRecorder(config.getSystem().getSqlRecordCount());
/**
* 是否在线,MyCat manager中有命令控制
* | offline | Change MyCat status to OFF |
* | online | Change MyCat status to ON |
*/
this.isOnline = new AtomicBoolean(true);
//缓存服务初始化
cacheService = new CacheService();
//路由计算初始化
routerService = new RouteService(cacheService);
// load datanode active index from properties
dnIndexProperties = loadDnIndexProps();
try {
//SQL解析器
sqlInterceptor = (SQLInterceptor) Class.forName(
config.getSystem().getSqlInterceptor()).newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
//catlet加载器
catletClassLoader = new DynaClassLoader(SystemConfig.getHomePath()
+ File.separator + "catlet", config.getSystem().getCatletClassCheckSeconds());
//记录启动时间
this.startupTime = TimeUtil.currentTimeMillis();
if (isUseZkSwitch()) {
String path = ZKUtils.getZKBasePath() + "lock/dnindex.lock";
dnindexLock = new InterProcessMutex(ZKUtils.getConnection(), path);
}
}
三. 连接流程一览
如果初期对整个源码流程不太了解 , 最简单的深入方式就是读 log , log 会为我们展现整个流程的方方面面.
3.1 获取配置
配置的加载通过 XMLSchemaLoader 进行扫描 ,通过 ConfigInitializer 进行初始化处理 , 整个配置的整体获取流程如下 :
// S1 : 构建 MycatServer 时构建 MycatConfig
- MycatServer.getInstance()
|- this.config = new MycatConfig();
// S2 : 构建 ConfigInitializer 对象
public MycatConfig() {
//读取schema.xml,rule.xml和server.xml
ConfigInitializer confInit = new ConfigInitializer(true);
//....
}
// S3 : ConfigInitializer 初始化流程
3-1 > new XMLSchemaLoader() : 初始化 Schema.xml 加载器 , 读取rule.xml和schema.xml
3-2 > new XMLConfigLoader(schemaLoader) : 读取 Server.xml 文件
3-3 > initDataHosts : 加载 dataHosts
3-4 > initDataNodes : 加载 dataNodes
3-5 > getFirewallConfig + initCobarCluster : 加载权限配置
// -------------------
// 补充 : Schema.xml 的加载 -> XMLSchemaLoader
- 准备 new XMLRuleLoader(ruleFile) , 如果未配置会使用默认配置
|- private final static String DEFAULT_XML = "/rule.xml";
|- private final static String DEFAULT_XML = "/schema.xml";
- getResourceAsStream 加载流和解析为 HashMap
|- private final Map<String, TableRuleConfig> tableRules;
|- private final Map<String, AbstractPartitionAlgorithm> functions;
// 补充 : Server 的加载 -> XMLConfigLoader
C- XMLServerLoader # load
|- XMLServerLoader.class.getResourceAsStream("/server.xml") : 获取 Resource 流
|- ConfigUtil.getDocument : 获取 Document
|- loadSystem : 加载System标签
|- loadUsers : 加载User标签
|- ClusterConfig : 加载集群配置
|- loadFirewall : 加载防火墙
3.2 建立连接
此处主要是Mycat与物理库的连接方式 , Mycat Connect 的创建起点是 PhysicalDatasource # createNewConnection , 这里会使用 ThreadPoolExecutor 进行创建 MySQLConnection , 整个连接的整体创建流程如下 :
// S1 : server.startup() 启动时对应用进行加载
- startup 中做了很多事情 , 包括初始化工厂类 , 初始化连接池 , 初始化 AIO 处理器等等
- 此环节仅关注 Mysql Connection 的连接
<---------------------------------->
// S2 : 初始化 datahost
Map<String, PhysicalDBPool> dataHosts = config.getDataHosts()
for (PhysicalDBPool node : dataHosts.values()) {
// 构建数据库对应映射索引
String index = dnIndexProperties.getProperty(node.getHostName(), "0");
// 初始化节点
node.init(Integer.parseInt(index));
// 建立心跳关联
node.startHeartbeat()
}
<---------------------------------->
// S3 : PhysicalDBPool 对 Source 进行初始化 , 此环节会初始化连接池
- ds.getConnection(this.schemas[i % schemas.length], true, getConHandler, null)
<---------------------------------->
// S4 : PhysicalDatasource # getConnection : 获取连接
- increamentCount.longValue()+totalConnectionCount : 获取最大连接数
- createNewConnection(handler, attachment, schema) : 如果连接数小于最大连接数 ,创建连接
<---------------------------------->
// S5 : 通过线程池创建连接
MycatServer.getInstance().getBusinessExecutor().execute(....)
<---------------------------------->
// S6 : 连接池创建连接 -> MySQLConnectionFactory
- MySQLConnectionFactory -> make -> new MySQLConnection -> create Socket
<---------------------------------->
// 补充 : InetSocketAddress
如果是异步处理 , 会调用 AsynchronousSocketChannel 进行 InetSocketAddress 的处理
四. 查询流程一览
上文看了 Mycat 与 物理库之间的关联关系 , 下面来看一下整个查询过程中的整体关联
4.1 Navicat 连接
Mycat 与外部的连接通过 NIOReactor 进行处理 , NIO 真的在各大框架中有非常广泛的应用 , 这里先不关注具体的实现方式
// S1 : MycatServer # startup 中对 NIO 进行初始化
- NIOReactorPool reactorPool = new NIOReactorPool
- connector = new NIOConnector
- ((NIOConnector) connector).start()
1-1 : 建立 Manager 连接 -> 9066
manager = new NIOAcceptor(DirectByteBufferPool.LOCAL_BUF_THREAD_PREX + NAME
+ "Manager", system.getBindIp(), system.getManagerPort(), mf, reactorPool);
1-2 : 建立 Server 连接 -> 8066
server = new NIOAcceptor(DirectByteBufferPool.LOCAL_BUF_THREAD_PREX + NAME
+ "Server", system.getBindIp(), system.getServerPort(), sf, reactorPool);
<---------------------------------->
// S2 : 通过 NIOReactorPool 对 NIOReactor 进行创建
for (int i = 0; i < poolSize; i++) {
NIOReactor reactor = new NIOReactor(name + "-" + i);
reactor.startup();
}
<---------------------------------->
// S3 : NIOReactor 中 死循环处理请求
public void run() {
for (;;) {
//.... 处理请求
}
}
4.2 Mycat 查询流程
MySQL 查询流程主要集中在 MySQLConnection 的父类 AbstractConnection # onReadData 中 , 以一个 select 语句来看整个流程
// 整体查询流程
- NIOReactor : NIO 处理请求
- FrontendConnection # handle : connection 建立连接
- FrontendCommandHandler # handle : command 处理命令
- ServerQueryHandler # query : 处理 Query 命令
- ServerConnection # execute : 核心命令执行
- SingleNodeHandler # execute : 单节点处理
- MySQLConnection # MySQLConnection
- MySQLConnection # synAndDoExecute
- MySQLConnection # sendQueryCmd : 发送 SQL Query 命令
查询主流程
// S1 : NIOReactor 接收请求 , 异步读取
- con.asynRead()
// S2 : 中层数据转换和处理
<---------------------------------->
// S3 : FrontendCommandHandler 对 Command 进行分发处理
public void handle(byte[] data){
// 取第四个 byte 做 Switch 判断
switch (data[4]){
// 主要基于 MySQLPacket 内的 final byte
// 可以看到 , 通过对应的 Handler 进行处理
- ServerParse.SELECT -> SelectHandler.handle
- ServerParse.SHOW -> ShowHandler.handle
- ServerParse.SET -> SetHandler.handle
- ServerParse.START -> StartHandler.handle
- ....
}
}
// 以 Select 为例
<---------------------------------->
// S4 : ServerConnection 进行 Route 和 Excute
RouteResultset = MycatServer.getInstance().getRouterservice().route(...)
session.execute(rrs, rrs.isSelectForUpdate()?ServerParse.UPDATE:type) : Session 执行查询
<---------------------------------->
// S5 : MultiNodeQueryHandler 多节点处理
for (final RouteResultsetNode node : rrs.getNodes()){
// For 循环进行处理
PhysicalDBNode dn = conf.getDataNodes().get(node.getName());
dn.getConnection(dn.getDatabase(), autocommit, node, this, node);
}
<---------------------------------->
// S6 : PhysicalDBNode # getConnection 连接池获取连接
dbPool.getRWBanlanceCon(schema,autoCommit, handler, attachment, this.database);
<---------------------------------->
// S7 : PhysicalDatasource 获取 Source 资源
BackendConnection con = this.conMap.tryTakeCon(schema, autocommit) : 从 Map 中获取已经建立好的连接
takeCon(con, handler, attachment, schema) : 绑定对应前端请求的handler
<---------------------------------->
// S8 : MySQLConnection 进行处理
xaTXID = sc.getSession2().getXaTXID()+",'"+getSchema()+"'" : 构建事务 ID
synAndDoExecute(xaTXID, rrn, sc.getCharsetIndex(), sc.getTxIsolation(),autocommit, sc.isTxReadonly(), sc.getSqlSelectLimit()) : 异步执行
<---------------------------------->
// S9 : MySQLConnection # synAndDoExecute 异步执行
- 前置 : 整个过程中包括 AutoCommit , tx 的相关处理
- 处理 : 构建 CommandPacket 用于发送 Command
- 执行 : sendQueryCmd 发送命令
整合数据
上文对数据集进行了查询 , 下面对查询的结果进行聚合
// S1 : MySQLConnectionHandler 中进行 handle 处理
// S2 : MySQLConnectionHandler # handleData 进行多节点查询
- 对 resultStatus 进行分析 , 包括三个大类以及四种方式
private static final int RESULT_STATUS_INIT = 0;
private static final int RESULT_STATUS_HEADER = 1;
private static final int RESULT_STATUS_FIELD_EOF = 2;
OkPacket > byte FIELD_COUNT = 0x00;
RequestFilePacket > byte FIELD_COUNT = (byte) 0xff;
ErrorPacket > byte FIELD_COUNT = (byte) 251;
EOFPacket > byte FIELD_COUNT = (byte) 0xfe;
// S3 : MultiNodeQueryHandler # fieldEofResponse 获取返回体
// S4 : DataMergeService # addPack 进行数据合并
- server.getBusinessExecutor().execute(this) : 将自身作为Runnable放入线程池
- run : 线程池默认调用 run 命令执行线程
- for 循环从 BlockingQueue 中弹出数据
- result.get(pack.dataNode).add(row) : 讲 Result 加入 Map
- Response 结果 : source.write(source.writeToBuffer(eof, buffer))
PS:整体过程比较复杂,初期就不花大量时间深入了 , 后续逐步的学习
总结
这一篇从 Mycat 的启动到一次完整的查询进行了学习和了解 , 在后续的文章中 , 会逐步的对其中的部分核心功能进行学习 , 同时会结合生产过程中出现的问题进行分析以及定制点的扩展
参考文档
<分布式数据库架构-基于 Mycat 中间件>