shardingjdbc原理分析
业务场景
垂直拆分:
垂直拆分主要是将系统按照业务的维度将系统进行分库/表,如下图所示:
网上购物系统按照业务的维度拆分成3个独立的库,可以减少表与表之间的IO问题
水平拆分:
主要是根据分片算法将一个库/表拆分成多个库/表,如下图所示
将各个子系统按照不同的分片规则进行拆分,具体的拆分规则需要针对不同的业务场景进行区分,水平拆分用于解决单表数据量过大的问题
shardingjdbc主要用于分库分表的业务场景,是一个分布式的数据库中间件。这里引用官方文档
http://shardingjdbc.io/index_zh.html
特点:
-支持分布式
-快速入手、侵入性小、简单方便
-能支持特性
原理
使用shardingjdbc很容易 eg:引入pom文件
<dependency>
<groupId>io.shardingjdbc</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>2.0.0.M2</version>
</dependency>
配置对应的分片策略
<sharding:standard-strategy id="databaseShardingStrategy" sharding-column="user_id"
precise-algorithm-class="io.shardingjdbc.example.spring.namespace.mybatis.algorithm.PreciseModuloDatabaseShardingAlgorithm"/>
<sharding:standard-strategy id="tableShardingStrategy" sharding-column="order_id"
precise-algorithm-class="io.shardingjdbc.example.spring.namespace.mybatis.algorithm.PreciseModuloTableShardingAlgorithm"/>
<sharding:data-source id="shardingDataSource">
<sharding:sharding-rule data-source-names="demo_ds_0, demo_ds_1">
<sharding:table-rules>
<sharding:table-rule logic-table="t_order"
actual-data-nodes="demo_ds_${0..1}.t_order_${0..1}"
database-strategy-ref="databaseShardingStrategy"
table-strategy-ref="tableShardingStrategy"
generate-key-column="order_id" />
<sharding:table-rule logic-table="t_order_item"
actual-data-nodes="demo_ds_${0..1}.t_order_item_${0..1}"
database-strategy-ref="databaseShardingStrategy"
table-strategy-ref="tableShardingStrategy"
generate-key-column="order_item_id" />
</sharding:table-rules>
</sharding:sharding-rule>
<sharding:props>
<prop key="sql.show">true</prop>
</sharding:props>
</sharding:data-source>
详细请参考官网demo>https://github.com/shardingjdbc/sharding-jdbc-example
架构图.png
shardingjdbc在1.5版本前使用druid的sql解析引擎,在新版本中使用自主开发的sql解析引擎
根据上图 我们将sql引擎分为
1.sql解析
2.sql改写
3.sql路由
4.sql执行
5.结果归并
sql解析
sql解析主要分为lexer和parse
lexer
lexer是通过lexerEngineFactory进行将sql语句进行语法分析
eg:SELECT * FROM T_ORDER WHERE ORDER_ID=order.id
通过Tokenizer 分词器 将sql语句进行分词,生成Token,每个token里含有 type literals endPosition,调试结果如下:
token.literals==SELECT
token.literals==*
token.literals==FROM
token.literals==T_ORDER
token.literals==WHERE
token.literals==ORDER_ID
token.literals== =
token.literals==order.id
lexer类图,针对不同的数据库,使用不同的lexer规则
lexer类图.png
parse
parse主要是通过ParseEnginFactory将sql语句进一步处理
eg:insert into t_order(user_id,status) values(1,'ok')
执行解析时时序图如下:
InsertParseFactory.parse()
方法对应时序图如下InsertParseFactory.parse.png
代码调试结果如下
调试信息1.png
parse时将sql语句表示出表名是t_order,列名时user_id,status,同时针对sql Type=DML,将分片id order_id添加到sqlToken中,为下一步做准备
//ParsingSQLRouter.java
public SQLStatement parse(final String logicSQL, final int parametersSize) {
SQLParsingEngine parsingEngine = new SQLParsingEngine(databaseType, logicSQL, shardingRule);
SQLStatement result = parsingEngine.parse();
if (result instanceof InsertStatement) {
((InsertStatement) result).appendGenerateKeyToken(shardingRule, parametersSize);
}
return result;
}
代码appendGenerateKeyToken用于为分片键order_id添加占位符?调试结果如下,注意对比2张图的标红的调试信息:
调试信息2.png
sql路由
Sql路由,主要是通过sql解析的上下文 + sql分片键 +sql分片算法来决定 sql语句应该落在那个数据源上的那张表上,如果有bindingTable关系,查询时就方便多了不用做笛卡尔积查询
sql路由时序图如下,:
主要做了几件事:
1.生成分片键值(主键),通过雪花算法进行生成.
2.获取对应的路由规则(上图中xml配置的规则)
3.路由数据库 根据自定义路由算法进行
4.路由表
5.返回路由结果(sql中是逻辑表)
6.sql重写
7.返回路由结果(sql中真实表)
下图是shardingPreparedStatement类图
shardingPreparedStatement类图.png
这是5返回路由结果如下图:
逻辑表路由结果.png
下图是6sql重写的时序图
sql重写时序图.png
1.sql分段
2.sql替换
信息调试如下:
分段
image.png
表名替换
信息调试3.png
7.返回路由结果调试信息如下
信息调试4.png
sql执行
sql执行时序图如下:
分为以下几步
1.根据数据源获取连接对象(数据源就是通过xml解析后的SpringShardingDataSource)
2.记录日志
3.执行sql
4.删除日志
shardingConnection类图如下
shardingConnection.png
1个数据源对应一个执行引擎一个执行引擎对应一个执行服务即一个线程池默认大小为8 可设置
对照图.png
sql执行中第一个sql语句为同步,从第二个开始为异步,代码如下
ListenableFuture<List<T>> restFutures = asyncExecute(sqlType, Lists.newArrayList(iterator), parameterSets, executeCallback);
T firstOutput;
List<T> restOutputs;
try {
firstOutput = syncExecute(sqlType, firstInput, parameterSets, executeCallback);
restOutputs = restFutures.get();
//CHECKSTYLE:OFF
} catch (final Exception ex) {
//CHECKSTYLE:ON
event.setException(ex);
event.setEventExecutionType(EventExecutionType.EXECUTE_FAILURE);
EventBusInstance.getInstance().post(event);
ExecutorExceptionHandler.handleException(ex);
return null;
}
event.setEventExecutionType(EventExecutionType.EXECUTE_SUCCESS);
EventBusInstance.getInstance().post(event);
//todo:监听器
结果归并
数据源 | 第一步结果 | 第二步结果 |
---|
sql结果归并是从将各个分片上的结果集进行merge,然后在输出
eg:SELECT i.* FROM t_order o, t_order_item i WHERE o.order_id = i.order_id order by o.order_id LIMIT 10, 5
时序图如下:
我们会发现路由引擎变成了ComplexRoutingEngine引擎,其实底层实现也是SimpleRoutingEgine.java
结果归并做了如下几件事
1.按照逻辑表的维度将不同数据源的实际表拍平
2.按照逻辑表关联关系对实际表做笛卡尔积
上述2步可以通过下图说明:
数据源 | 第一步结果 | 第二步结果 |
---|
上图我们看到t_order和t_order_items是bindingTable关系,所以做笛卡尔积时并不是44 = 16结果 而是 22+2*2 =8个结果,查询最多返回8个resultSet结果集
结果集合并类图如下:
返回结果集类图.png
ShardingResultSet.java 持有ResultMerger.java的引用
返回结果集merge类图.png
在进行merge时使用了通过创建优先级队列来存储resultSet的引用 并通过优先级队列对引用的第一个元素进行排序,来达到合并的效果。
eg:假设resultSet结果集返回5个,值1,值2,值3是resultSet的结果集合,空间关系这里只展示了3个,按照limit4,3我们至少每个结果集需要请求7个值
orderby merge举例.png
最左侧展示的每次出队和入队的元素,中间展示的初始化时队列的元素,和每一次出队和入队后的队列元素。不难看出前4个元素是a,b,c,d.我们要返回的分页结果集是 f,g,k.同时,中间的队列大小也反映了ressultSet集合大小。
这里优先级队列做了个排序,类似一个堆结构,每次只取最小堆,然后将最小堆对应resultSet.next()获取的元素放入堆中,进行重新排序产生最小堆,下次在去最小堆,以此类推。
柔性事务
柔性事务执行
其对应的流程图如下:
执行流程图.png
更详细的使用建议参考官方文档.> http://shardingjdbc.io/docs/02-guide/transaction/
shardingjdbc总结
1.从上面的类图中可以看出,shardingjdbc主要通过重写sql规范来实现分库分表的。
2.分布式主键 数据分布不均匀
Shardingjdbc使用了snowflake算法 如下
shardingjdbc官网解释
snowflake算法的最后4位是在同一毫秒内的访问递增值。因此,如果毫秒内并发度不高,最后4位为零的几率则很大。因此并发度不高的应用生成偶数主键的几率会更高。
解释如下:
12bit序列号
0000 0000 0000
1111 1111 1111
==>20+21+22+…+211 = 212-2+20=4096-2+1=4095 0-->4095 共4096个数字
shardingjdbc性能指标> http://shardingjdbc.io/docs/02-guide/apm/
不支持的sql> http://shardingjdbc.io/docs/01-start/sql-supported/
与其他框架对比>http://shardingjdbc.io/docs/00-overview/intro/