【源码解析】Sharding-Jdbc的执行过程(一)
一、ShardingContext
在Sharding-Jdbc中,我们其实需要抓住一个核心类,也就是ShardingContext,分片上下文,里面定义了下面几个内容:
@RequiredArgsConstructor
@Getter
public final class ShardingContext {
//分片规则
private final ShardingRule shardingRule;
//数据库类型
private final DatabaseType databaseType;
//执行引擎
private final ExecutorEngine executorEngine;
//是否要在log文件中展示sql语句
private final boolean showSQL;
}
里面的几个参数很简单易懂,但是里面包含的内容也是整个中间件的核心内容。
二、preparedStatement
这块对应于代码中的jdbc/core/statement,里面包括两部分,一部分是statement,一部分是prepareStatement。考虑到性能问题(prepareStatement可以进行缓存)和代码的优雅性(变量的设置),以及考虑到通常JDBC使用的都是prepareStatement,所以我们着重看下prepareStatement的两部分,即MasterSlavePreparedStatement和ShardingPreparedStatement。
2.1 MasterSlavePreparedStatement
2.1.1 获取connection
支持读写分离。这块可以先看一些他的构造方法,构造方法中一般都有这样的方法:
connection.getConnections(sqlStatement.getType())
这块其实是根据sql的类型,来获取不同的连接
- 如果是DDL类型,会获取所有的连接,包括master和slave,也就是说对于表结构的修改,会修改包括master和slave
- 如果是DML类型,也就是写操作,获取的是master的连接
- 最后,如果是DQL类型,也就是读操作,是根据读写分离的策略获取某个slave连接
后面两种是有本地缓存的,可以避免每次都进行connect构建,提高效率。
当然,这个sqlType是怎么来的?这就需要用到sql解析的模块了。
SQLStatement sqlStatement = new SQLJudgeEngine(sql).judge();
这块先提一下,后续再详细讲解。
2.1.2 执行sql
主要有三个方法,
- executeQuery
public ResultSet executeQuery() throws SQLException {
Preconditions.checkArgument(1 == routedStatements.size(), "Cannot support executeQuery for DDL");
return routedStatements.iterator().next().executeQuery();
}
首先说明下,这里的routedStatements是待执行sql的一个集合,这块首先确保只有一个查询语句。然后直接调用PreparedStatement.executeQuery()方法,比较直观。
- executeUpdate
public int executeUpdate() throws SQLException {
int result = 0;
for (PreparedStatement each : routedStatements) {
result += each.executeUpdate();
}
return result;
}
这块逻辑与上面一致,不过不一样的是,可以传入多个update的语句,然后顺序执行,最后调用的是PreparedStatement.executeUpdate()方法。
- execute
public boolean execute() throws SQLException {
boolean result = false;
for (PreparedStatement each : routedStatements) {
result = each.execute();
}
return result;
}
最后就是一些其他的方法的执行了。
总的来说,读写分离这块的内容比较简单,首先sql语句不要怎么变化,再者数据库连接connection基本上都是确定的,所以不需要路由什么的,可以直接运行。
2.2 ShardingPreparedStatement
下面我们看下分库分表的情况下,来执行我们的sql,这块就稍微有些复杂了。
2.2.1 构造方法
首先看一下他的构造方法,构造方法中有一些很奇怪的常量,比如TYPE_FORWARD_ONLY、CONCUR_READ_ONLY等等,这块可以参考这篇博客。其实就是在读写过程中的指针的方向等内容。
2.2.2 sql执行
2.2.2.1 查询
查询的逻辑大概如下:
- sql路由
- sql改写
- sql执行
- 结果合并
下面看下代码:
public ResultSet executeQuery() throws SQLException {
ResultSet result;
try {
Collection<PreparedStatementUnit> preparedStatementUnits = route();
List<ResultSet> resultSets = new PreparedStatementExecutor(
getConnection().getShardingContext().getExecutorEngine(), routeResult.getSqlStatement().getType(), preparedStatementUnits, getParameters()).executeQuery();
result = new ShardingResultSet(resultSets, new MergeEngine(resultSets, (SelectStatement) routeResult.getSqlStatement()).merge());
} finally {
clearBatch();
}
currentResultSet = result;
return result;
}
首先我们需要看几个类:
- PreparedStatementUnit
- SQLExecutionUnit:包括dataSource和sql
- PreparedStatement
这个类,会用于最终我们到具体的数据库上执行sql,调用route方法,其实就是路由到具体的服务器上面。
private Collection<PreparedStatementUnit> route() throws SQLException {
Collection<PreparedStatementUnit> result = new LinkedList<>();
routeResult = routingEngine.route(getParameters());
for (SQLExecutionUnit each : routeResult.getExecutionUnits()) {
SQLType sqlType = routeResult.getSqlStatement().getType();
Collection<PreparedStatement> preparedStatements;
if (SQLType.DDL == sqlType) {
preparedStatements = generatePreparedStatementForDDL(each);
} else {
preparedStatements = Collections.singletonList(generatePreparedStatement(each));
}
routedStatements.addAll(preparedStatements);
for (PreparedStatement preparedStatement : preparedStatements) {
replaySetParameter(preparedStatement);
result.add(new PreparedStatementUnit(each, preparedStatement));
}
}
return result;
}
这里面有个routeResult,其实就是根据入参路由到的数据库列表。这边有一个路由引擎,看下这边是怎么路由的:
public SQLRouteResult route(final List<Object> parameters) {
if (null == sqlStatement) {
sqlStatement = sqlRouter.parse(logicSQL, parameters.size());
}
return sqlRouter.route(logicSQL, parameters, sqlStatement);
}
这边有几个参数,
- logicSQL:表示逻辑上的sql,因为业务使用时,写的sql其实是逻辑上的sql,用到的数据库和表也是逻辑库和逻辑表
- parameters:传入的参数
- sqlStatement:最终的sql语句
首先解析sql,然后路由。这边的解析还是老套路,但是如果解析出来的sql是insert,而且配置了自动生成key,那么会调用自动生成key的方法,生成key,放到对应的column下。解析这块后续再分析。下面我们看下路由:
@Override
public SQLRouteResult route(final String logicSQL, final List<Object> parameters, final SQLStatement sqlStatement) {
SQLRouteResult result = new SQLRouteResult(sqlStatement);
if (sqlStatement instanceof InsertStatement && null != ((InsertStatement) sqlStatement).getGeneratedKey()) {
processGeneratedKey(parameters, (InsertStatement) sqlStatement, result);
}
//路由
RoutingResult routingResult = route(parameters, sqlStatement);
//重写sql
SQLRewriteEngine rewriteEngine = new SQLRewriteEngine(shardingRule, logicSQL, databaseType, sqlStatement);
boolean isSingleRouting = routingResult.isSingleRouting();
if (sqlStatement instanceof SelectStatement && null != ((SelectStatement) sqlStatement).getLimit()) {
processLimit(parameters, (SelectStatement) sqlStatement, isSingleRouting);
}
SQLBuilder sqlBuilder = rewriteEngine.rewrite(!isSingleRouting);
if (routingResult instanceof CartesianRoutingResult) {
for (CartesianDataSource cartesianDataSource : ((CartesianRoutingResult) routingResult).getRoutingDataSources()) {
for (CartesianTableReference cartesianTableReference : cartesianDataSource.getRoutingTableReferences()) {
result.getExecutionUnits().add(new SQLExecutionUnit(cartesianDataSource.getDataSource(), rewriteEngine.generateSQL(cartesianTableReference, sqlBuilder)));
}
}
} else {
for (TableUnit each : routingResult.getTableUnits().getTableUnits()) {
result.getExecutionUnits().add(new SQLExecutionUnit(each.getDataSourceName(), rewriteEngine.generateSQL(each, sqlBuilder)));
}
}
if (showSQL) {
SQLLogger.logSQL(logicSQL, sqlStatement, result.getExecutionUnits(), parameters);
}
return result;
}
先看路由:
private RoutingResult route(final List<Object> parameters, final SQLStatement sqlStatement) {
Collection<String> tableNames = sqlStatement.getTables().getTableNames();
RoutingEngine routingEngine;
//不涉及到具体的表,全路由
if (tableNames.isEmpty()) {
routingEngine = new DatabaseAllRoutingEngine(shardingRule.getDataSourceMap());
} else if (1 == tableNames.size() || shardingRule.isAllBindingTables(tableNames) || shardingRule.isAllInDefaultDataSource(tableNames)) {
routingEngine = new SimpleRoutingEngine(shardingRule, parameters, tableNames.iterator().next(), sqlStatement);
} else {
// TODO config for cartesian set
routingEngine = new ComplexRoutingEngine(shardingRule, parameters, tableNames, sqlStatement);
}
return routingEngine.route();
}
这里涉及到一个bindingTable的概念,概念如下:指在任何场景下分片规则均一致的主表和子表。例:订单表和订单项表,均按照订单ID分片,则此两张表互为BindingTable关系。BindingTable关系的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
这里最终返回的结果RoutingResult类似如下:
这里面有几种路由类,后续再详细分析。
获取到数据库表的路由信息后,就到了sql改写的过程。这里也有一个sql改写的引擎:++SQLRewriteEngine++。中间插入了一个判断,routingResult.isSingleRouting(),判断是否路由之后的DB只有一个。这里有个小的优化:如果是select语句,用到了limit,而且最终落到了单片上,那么sql语句是不会被重写的,通过rewrite方法也可以看出来,是和isSingleRouting取反的。
public SQLBuilder rewrite(final boolean isRewriteLimit) {
SQLBuilder result = new SQLBuilder();
if (sqlTokens.isEmpty()) {
result.appendLiterals(originalSQL);
return result;
}
int count = 0;
sortByBeginPosition();
for (SQLToken each : sqlTokens) {
if (0 == count) {
result.appendLiterals(originalSQL.substring(0, each.getBeginPosition()));
}
if (each instanceof TableToken) {
appendTableToken(result, (TableToken) each, count, sqlTokens);
} else if (each instanceof ItemsToken) {
appendItemsToken(result, (ItemsToken) each, count, sqlTokens);
} else if (each instanceof RowCountToken) {
appendLimitRowCount(result, (RowCountToken) each, count, sqlTokens, isRewriteLimit);
} else if (each instanceof OffsetToken) {
appendLimitOffsetToken(result, (OffsetToken) each, count, sqlTokens, isRewriteLimit);
} else if (each instanceof OrderByToken) {
appendOrderByToken(result, count, sqlTokens);
}
count++;
}
return result;
}
执行完成这步之后,sql语句重写基本完成。如果路由结果涉及到笛卡尔积,还需要对sql进行进一步的重写,因为涉及到关联表。下面涉及到真正转化sql这部分,使用的方法是:rewriteEngine.generateSQL,涉及到sql中的逻辑库表替换为实际的库表,形成SQLExecutionUnit,添加到待执行的sql列表中,最终得到SQLRouteResult。至此,sql路由完成。回到ShardingPreparedStatement的route方法。遍历routeResult,形成预执行的statement集合。
再回到ShardingPreparedStatement的executeQuery()方法,route()得到了Collection,下面就到了真正要执行sql的步骤。
【源码解析】Sharding-Jdbc的执行过程(一)的更多相关文章
- Flink 源码解析 —— JobManager 处理 SubmitJob 的过程
JobManager 处理 SubmitJob https://t.zsxq.com/3JQJMzZ 博客 1.Flink 从0到1学习 -- Apache Flink 介绍 2.Flink 从0到1 ...
- Flink 源码解析 —— TaskManager 处理 SubmitJob 的过程
TaskManager 处理 SubmitJob 的过程 https://t.zsxq.com/eu7mQZj 博客 1.Flink 从0到1学习 -- Apache Flink 介绍 2.Flink ...
- Netty 源码解析(九): connect 过程和 bind 过程分析
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第九篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...
- Mybatis源码解析1—— JDBC
在之前的文章中,我为大家介绍了 Mybatis 的详细用法,算是基础教程. 详细链接:Mybatis 基础教程 言归正传,只懂基础可不行,接下来将给大家带来高阶的源码解析教程,从浅入深,通过源码解析, ...
- Fabric1.4源码解析:客户端创建通道过程
在使用Fabric创建通道的时候,通常我们执行一条命令完成,这篇文章就解析一下执行这条命令后Fabric源码中执行的流程. peer channel create -o orderer.example ...
- Netty源码解析 -- ChannelPipeline机制与读写过程
本文继续阅读Netty源码,解析ChannelPipeline事件传播原理,以及Netty读写过程. 源码分析基于Netty 4.1 ChannelPipeline Netty中的ChannelPip ...
- Netty源码解析 -- ChannelOutboundBuffer实现与Flush过程
前面文章说了,ChannelHandlerContext#write只是将数据缓存到ChannelOutboundBuffer,等到ChannelHandlerContext#flush时,再将Cha ...
- 【转】aiohttp 源码解析之 request 的处理过程
[转自 太阳尚远的博客:http://blog.yeqianfeng.me/2016/04/01/python-yield-expression/] 使用过 python 的 aiohttp 第三方库 ...
- MyBatis源码解析(一)——执行流程
原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/6603926.html 一.MyBatis简介 MyBatis框架是一种轻量级的ORM框架, ...
随机推荐
- maven常见配置
maven surefire plugin 默认执行失败后,不会继续执行,需要在</configuration>中设置参数 <testFailureIgnore>true< ...
- 升级Centos 7/6内核版本到4.12.4的方法
一.查看那系统内核版本 二.升级内核 三.修改grub中默认的内核版本 四.重启系统并查看系统内核 公司打算上Docker服务,目前需要安装运行环境,Docker新的功能除了需要Centos 7系统之 ...
- Day4_迭代器
迭代器: 是一个重复的过程,每一次重复都是基于上一次的结果而来. 可迭代对象: 凡是对象有_iter_方法(对象._iter_),该对象就是可迭代对象. 字符串,列表,元组,字典等都是可迭代对象. 可 ...
- Day2_and_Day3 文件操作
文件修改操作: 文件的修改操作:文件并没有修改操作,实际是将一个编写的新文件覆盖了原有的文件 替换文件中的某个内容: with open('old.txt','r',encoding='utf-8') ...
- mongodb备份还原脚本
同步 echo off ,%-%date:~,%-%date:~,% set bak_dir=mongo_na_world_svn2win_%time_dir% set fromdb=db_erago ...
- 新一代大数据处理引擎 Apache Flink
https://www.ibm.com/developerworks/cn/opensource/os-cn-apache-flink/index.html 大数据计算引擎的发展 这几年大数据的飞速发 ...
- Android 众多的布局属性详解
http://www.open-open.com/lib/view/open1328686184311.html Android功能强大,界面华丽,但是众多的布局属性就害苦了开发者,下面这篇文章结合了 ...
- Centos7下安装MySql
1.安装MariaDB 安装命令 yum -y install mariadb mariadb-server 安装完成MariaDB,首先启动MariaDB systemctl start maria ...
- __new__()方法的使用和实例化
Python中__new__()方法的使用和实例化 1 2 new()是在新式类中新出现的方法,它作用在构造方法init()建造实例之前,可以这么理解,在Python 中存在于类里面的构造方法init ...
- 一张图搞定OAuth2.0
1.引言 本篇文章是介绍OAuth2.0中最经典最常用的一种授权模式:授权码模式 非常简单的一件事情,网上一堆神乎其神的讲解,让我不得不写一篇文章来终结它们. 一项新的技术,无非就是了解它是什么,为什 ...