架构原理
官方文档:概览 :: ShardingSphere
核心概念
逻辑表
水平拆分的数据库(表)的相同逻辑和数据结构表的总称。例:订单数据根据主键尾数拆分为10张表,分别是t_order_0
到t_order_9
,他们的逻辑表名为t_order
。
真实表
在分片的数据库中真实存在的物理表。即上个示例中的t_order_0
到t_order_9
。
数据节点
数据分片的最小单元。由数据源名称和数据表组成,例:ds_0.t_order_0
。
绑定表
指分片规则一致的主表和子表。例如:t_order
表和t_order_item
表,均按照order_id
分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。举例说明,如果SQL为:
SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
在不配置绑定表关系时,假设分片键order_id
将数值10路由至第0片,将数值11路由至第1片,那么路由后的SQL应该为4条,它们呈现为笛卡尔积:
SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
在配置绑定表关系后,路由的SQL应该为2条:
SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
其中t_order
在FROM的最左侧,ShardingSphere将会以它作为整个绑定表的主表。 所有路由计算将会只使用主表的策略,那么t_order_item
表的分片计算将会使用t_order
的条件。故绑定表之间的分区键要完全相同。
分片策略
ShardingSphere认为对于分片策略存有两种维度:
- 数据源分片策略(DatabaseShardingStrategy):数据被分配的目标数据源
- 表分片策略(TableShardingStrategy):数据被分配的目标表
两种分片策略API完全相同,但是表分片策略是依赖于数据源分片策略的(即:先分库然后才有分表)
分片算法
Sharding-JDBC提供了5种分片策略。由于分片算法和业务实现紧密相关,因此Sharding-JDBC并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。
- 精确分片算法
对应PreciseShardingAlgorithm,用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用。
- 范围分片算法
对应RangeShardingAlgorithm,用于处理使用单一键作为分片键的BETWEEN AND、>、<、>=、<=进行分片的场景。需要配合StandardShardingStrategy使用。
- 复合分片算法
对应ComplexKeysShardingAlgorithm,用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合ComplexShardingStrategy使用。
- Hint分片算法
对应HintShardingAlgorithm,用于处理使用Hint行分片的场景。需要配合HintShardingStrategy使用。
分页原理
select * from ORDER_XX where orderId = ? limit 0 ,10
如果查询条件没有orderId, 那么最终执行的sql就是3条(假设每页10条):
select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 0 ,10 ; select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 0 ,10 ; select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 0 ,10 ;
注意在有多个分表的情况下, 每个表都取前10条数据出来(一共30条), 然后再排序取前10条, 这样的逻辑是不对的. sharding-jdbc给了个例子, 如果下图:
图中的例子中,想要取得两个表中共同的按照分数排序的第2条和第3条数据,应该是95和90。 由于执行的SQL只能从每个表中获取第2条和第3条数据,即从t_score_0表中获取的是90和80;从t_score_1表中获取的是85和75。 因此进行结果归并时,只能从获取的90,80,85和75之中进行归并,那么结果归并无论怎么实现,都不可能获得正确的结果.
那怎么办呢?
sharding-jdbc的做法就改写我们的sql, 先查出来所有的数据, 再做归并排序
例如查询第2页时
原sql是: select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 10 ,10 ; select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 10 ,10 ; select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 10 ,10 ; 会被改写成: select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 0 ,20 ; select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 0 ,20 ; select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 0 ,20 ;
查询第3页时
原sql是: select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 20 ,10 ; select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 20 ,10 ; select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 20 ,10 ; 会被改写成: select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 0 ,30 ; select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 0 ,30 ; select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 0 ,30 ;
当然, 大家肯定会觉得这样处理性能会很差, 其实事实上也的确是, 不过sharing-jdbc是在这个基础上做了优化的,就是上面提到的"归并",大概的逻辑就是先查出所有页的数据, 然后通过流式处理跳过前面的页,只取最终需要的页,最终达到分页的目的
配置实战
Sharding-JDBC重写了mybatis里配置sql的路由规则,只要按照常规的方法在xml文件里书写sql就可以自动路由的对应的表,但是查询时最好要指定分片键,否则会出现扫描所有表的情况,在数据量很庞大的情况下不可取。
引入依赖
<!--mybatis--> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>${sharding.jdbc.version}</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>${tk.mybatis.version}</version> </dependency>
数据源配置 (按照月份进行分表)
spring: shardingsphere: datasource: names: two-project two-project: driver-class-name: com.mysql.jdbc.Driver filters: stat,wall,log4j poolPreparedStatements: true stat-view-servlet: enabled: true reset-enable: false url-pattern: /druid/* testOnBorrow: false testOnReturn: false testWhileIdle: true type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://127.0.0.1:3306/sharding-jdbc?useSSL=false&characterEncoding=utf-8&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true username: root password: 123456.. validationQuery: SELECT 1 FROM DUAL web-stat-filter: enabled: true exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' props: sql: show: false sharding: tables: order: actual-data-nodes: two-project.t_order_2020_0$->{8..9},two-project.t_order_2020_$->{10..12},two-project.t_order_$->{2021..2099}_0$->{1..9},two-project.t_order_$->{2021..2099}_$->{10..12} table-strategy: standard: sharding-column: id precise-algorithm-class-name: com.baoquan.sharding.TimeShardingTableAlgorithm range-algorithm-class-name: com.baoquan.sharding.TimeRangeShardingAlgorithm
mapper文件
<insert id="createOrder" parameterType="com.baoquan.dataobject.po.Order"> insert into `order` (id,user_id, order_id,create_time) values (#{id},#{userId}, #{orderId}, #{createTime}) </insert>
分片算法
package com.baoquan.sharding; import com.baoquan.constants.CommonContants; import com.baoquan.util.ParaseShardingKeyTool; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue; import java.util.Collection; public class TimeShardingTableAlgorithm implements PreciseShardingAlgorithm<Long> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) { String logicTable = new StringBuffer().append(shardingValue.getLogicTableName()) .append(CommonContants.SPLIT).append(ParaseShardingKeyTool.getYearAndMonth((shardingValue.getValue().toString()))).toString(); //获取实际表名 String targetTable = null; for (String availableTargetName : availableTargetNames) { if (availableTargetName.contains(logicTable)) { targetTable = availableTargetName; } } return targetTable; } }
package com.baoquan.sharding; import com.google.common.collect.Range; import com.baoquan.constants.CommonContants; import com.baoquan.util.ParaseShardingKeyTool; import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; import java.util.Collection; import java.util.LinkedHashSet; public class TimeRangeShardingAlgorithm implements RangeShardingAlgorithm<String> { private DateTimeFormatter dateformat = DateTimeFormatter.ofPattern(CommonContants.yyyy_MM); private DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder() .appendPattern(CommonContants.yyyy_MM) .parseDefaulting(ChronoField.DAY_OF_MONTH, 1) .toFormatter(); @Override public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<String> shardingValue) { Collection<String> result = new LinkedHashSet<String>(); Range<String> shardingKey = shardingValue.getValueRange(); String startShardingKey = shardingKey.lowerEndpoint(); String endShardingKey = shardingKey.upperEndpoint(); String startTimeString = ParaseShardingKeyTool.getYearAndMonth(startShardingKey); String endTimeString = ParaseShardingKeyTool.getYearAndMonth(endShardingKey); LocalDate startLocalDate = LocalDate.parse(startTimeString, DATE_FORMAT); LocalDate endLocalDate = LocalDate.parse(endTimeString, DATE_FORMAT); while (startLocalDate.compareTo(endLocalDate) != 0) { StringBuffer tableName = new StringBuffer(); tableName.append(shardingValue.getLogicTableName()) .append(CommonContants.SPLIT).append(dateformat.format(startLocalDate)); result.add(tableName.toString()); startLocalDate = startLocalDate.plusMonths(1); } result.add(shardingValue.getLogicTableName() + CommonContants.SPLIT + dateformat.format(startLocalDate)); //获取实际表名 Collection<String> result2 = new LinkedHashSet<String>(); for (String availableTargetName : availableTargetNames) { for (String logicTable : result) { if (availableTargetName.contains(logicTable)) { result2.add(availableTargetName); } } } return result2; } }