org.apache.shardingsphere
sharding-jdbc-core
${latest.release.version}
注意: 请将${latest.release.version}
更改为实际的版本号。
Sharding-JDBCk可以通过Java、YAML、Spring命名空间和Spring Boot Starter四种方式配置,开发者可根据场景选择适合的配置方式。
Java配置
数据分片 ShardingDataSourceFactory
数据分片的数据源创建工厂
名称 | 数据类型 | 说明 |
---|---|---|
dataSourceMap | Map |
数据源配置 |
shardingRuleConfig | ShardingRuleConfiguration | 数据分片配置规则 |
props (?) | Properties | 属性配置 |
分片规则配置对象
名称 | 数据类型 | 说明 |
---|---|---|
tableRuleConfigs | Collection | 分片规则列表 |
bindingTableGroups (?) | Collection | 绑定表规则列表 |
broadcastTables (?) | Collection | 广播表规则列表 |
defaultDataSourceName (?) | String | 未配置分片规则的表将通过默认数据源定位 |
defaultDatabaseShardingStrategyConfig (?) | ShardingStrategyConfiguration | 默认分库策略 |
defaultTableShardingStrategyConfig (?) | ShardingStrategyConfiguration | 默认分表策略 |
defaultKeyGeneratorConfig (?) | KeyGeneratorConfiguration | 默认自增列值生成器配置,缺省将使用org.apache.shardingsphere.core.keygen.generator.impl.SnowflakeKeyGenerator |
masterSlaveRuleConfigs (?) | Collection | 读写分离规则,缺省表示不使用读写分离 |
TableRuleConfiguration
表封片规则配置对象
名称 | 数据类型 | 说明 |
---|---|---|
logicTable | String | 逻辑表名称 |
actualDataNodes (?) | String | 由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况 |
databaseShardingStrategyConfig (?) | ShardingStrategyConfiguration | 分库策略,缺省表示使用默认分库策略 |
tableShardingStrategyConfig (?) | ShardingStrategyConfiguration | 分表策略,缺省表示使用默认分表策略 |
keyGeneratorConfig (?) | KeyGeneratorConfiguration | 自增列值生成器配置,缺省表示使用默认自增主键生成器 |
encryptorConfiguration (?) | EncryptorConfiguration | 加解密生成器配置 |
StandardShardingStrategyConfiguration
ShardingStrategyConfiguration的实现类,用于单分片键的标准分片场景
名称 | 数据类型 | 说明 |
---|---|---|
shardingColumn | String | 分片列名称 |
preciseShardingAlgorithm | PreciseShardingAlgorithm | 精确分片算法,用于=和IN |
rangeShardingAlgorithm (?) | RangeShardingAlgorithm | 范围分片算法,用于BETWEEN |
ComplexShardingStrategyConfiguration
ShardingStrategyConfiguration的实现类,用于多分片键的复合分片场景
名称 | 数据类型 | 说明 |
---|---|---|
shardingColumns | String | 分片列名称,多个列以逗号分隔 |
shardingAlgorithm | ComplexKeysShardingAlgorithm | 复合分片算法 |
InlineShardingStrategyConfiguration
ShardingStrategyConfiguration的实现类,用于配置行表达式分片策略
名称 | 数据类型 | 说明 |
---|---|---|
shardingColumn | String | 分片列名称 |
algorithmExpression | String | 分片算法行表达式,需符合groovy语法,详情请参考 |
HintShardingStrategyConfiguration
ShardingStrategyConfiguration的实现类,用于配置Hint方式分片策略
名称 | 数据类型 | 说明 |
---|---|---|
shardingAlgorithm | HintShardingAlgorithm | Hint分片算法 |
NoneShardingStrategyConfiguration
ShardingStrategyConfiguration的实现类,用于配置不分片的策略
KeyGeneratorConfiguration
名称 | 数据类型 | 说明 |
---|---|---|
column | String | 自增列名称 |
type | String | 自增列值生成器类型,可自定义或选择内置类型:SNOWFLAKE/UUID |
props | Properties | 自增列值生成器的相关属性配置 |
Properties
属性配置项,可以为以下自增列值生成器的属性
SNOWFLAKE
名称 | 数据类型 | 说明 |
---|---|---|
worker.id (?) | long | 工作机器唯一id,默认为0 |
max.tolerate.time.difference.milliseconds (?) | long | 最大容忍时钟回退时间,单位:毫秒。默认为10毫秒 |
max.vibration.offset (?) | int | 最大抖动上限值,范围[0, 4096),默认为1。注:若使用此算法生成值作分片值,建议配置此属性。此算法在不同毫秒内所生成的key取模2^n (2^n一般为分库或分表数) 之后结果总为0或1。为防止上述分片问题,建议将此属性值配置为(2^n)-1 |
EncryptRuleConfiguration
名称 | 数据类型 | 说明 |
---|---|---|
encryptors | Map |
加解密器配置列表,可自定义或选择内置类型:MD5/AES |
tables | Map |
加密表配置列表 |
EncryptorRuleConfiguration
名称 | 数据类型 | 说明 |
---|---|---|
type | String | 加解密器类型,可自定义或选择内置类型:MD5/AES |
properties | Properties | 属性配置, 注意:使用AES加密器,需要配置AES加密器的KEY属性:aes.key.value |
EncryptTableRuleConfiguration
名称 | 数据类型 | 说明 |
---|---|---|
tables | Map |
加密列配置列表 |
EncryptColumnRuleConfiguration
名称 | 数据类型 | 说明 |
---|---|---|
plainColumn | String | 存储明文的字段 |
cipherColumn | String | 存储密文的字段 |
assistedQueryColumn | String | 辅助查询字段,针对ShardingQueryAssistedEncryptor类型的加解密器进行辅助查询 |
encryptor | String | 加解密器名字 |
Properties
属性配置项,可以为以下属性
名称 | 数据类型 | 说明 |
---|---|---|
sql.show (?) | boolean | 是否开启SQL显示,默认值: false |
executor.size (?) | int | 工作线程数量,默认值: CPU核数 |
max.connections.size.per.query (?) | int | 每个物理数据库为每次查询分配的最大连接数量。默认值: 1 |
check.table.metadata.enabled (?) | boolean | 是否在启动时检查分表元数据一致性,默认值: false |
query.with.cipher.column (?) | boolean | 当存在明文列时,是否使用密文列查询,默认值: true |
allow.range.query.with.inline.sharding (?) | boolean | 当使用inline分表策略时,是否允许范围查询,默认值: false |
Yaml配置
数据分片
dataSources: #数据源配置,可配置多个data_source_name
: # `!!`表示实例化该类
driverClassName: #数据库驱动类名
url: #数据库url连接
username: #数据库用户名
password: #数据库密码
# ... 数据库连接池的其它属性
shardingRule:
tables: #数据分片规则配置,可配置多个logic_table_name
: #逻辑表名称
actualDataNodes: #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
databaseStrategy: #分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一
standard: #用于单分片键的标准分片场景
shardingColumn: #分片列名称
preciseAlgorithmClassName: #精确分片算法类名称,用于=和IN。。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器
rangeAlgorithmClassName: #范围分片算法类名称,用于BETWEEN,可选。。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器
complex: #用于多分片键的复合分片场景
shardingColumns: #分片列名称,多个列以逗号分隔
algorithmClassName: #复合分片算法类名称。该类需实现ComplexKeysShardingAlgorithm接口并提供无参数的构造器
inline: #行表达式分片策略
shardingColumn: #分片列名称
algorithmInlineExpression: #分片算法行表达式,需符合groovy语法
hint: #Hint分片策略
algorithmClassName: #Hint分片算法类名称。该类需实现HintShardingAlgorithm接口并提供无参数的构造器
none: #不分片
tableStrategy: #分表策略,同分库策略
keyGenerator:
column: #自增列名称,缺省表示不使用自增主键生成器
type: #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
: 属性名称
bindingTables: #绑定表规则列表
-
-
-
broadcastTables: #广播表规则列表
- table_name1
- table_name2
- table_name_x
defaultDataSourceName: #未配置分片规则的表将通过默认数据源定位
defaultDatabaseStrategy: #默认数据库分片策略,同分库策略
defaultTableStrategy: #默认表分片策略,同分库策略
defaultKeyGenerator: #默认的主键生成算法 如果没有设置,默认为SNOWFLAKE算法
type: #默认自增列值生成器类型,缺省将使用org.apache.shardingsphere.core.keygen.generator.impl.SnowflakeKeyGenerator。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
props:
: #自增列值生成器属性配置, 比如SNOWFLAKE算法的worker.id与max.tolerate.time.difference.milliseconds
masterSlaveRules: #读写分离规则,详见读写分离部分
: #数据源名称,需要与真实数据源匹配,可配置多个data_source_name
masterDataSourceName: #详见读写分离部分
slaveDataSourceNames: #详见读写分离部分
loadBalanceAlgorithmType: #详见读写分离部分
props: #读写分离负载算法的属性配置
: #属性值
props: #属性配置
sql.show: #是否开启SQL显示,默认值: false
executor.size: #工作线程数量,默认值: CPU核数
max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false
Spring Boot配置
配置项
数据分片
spring.shardingsphere.datasource.names= #数据源名称,多数据源以逗号分隔
spring.shardingsphere.datasource..type= #数据库连接池类名称
spring.shardingsphere.datasource..driver-class-name= #数据库驱动类名
spring.shardingsphere.datasource..url= #数据库url连接
spring.shardingsphere.datasource..username= #数据库用户名
spring.shardingsphere.datasource..password= #数据库密码
spring.shardingsphere.datasource..xxx= #数据库连接池的其它属性
spring.shardingsphere.sharding.tables..actual-data-nodes= #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
#分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一
#用于单分片键的标准分片场景
spring.shardingsphere.sharding.tables..database-strategy.standard.sharding-column= #分片列名称
spring.shardingsphere.sharding.tables..database-strategy.standard.precise-algorithm-class-name= #精确分片算法类名称,用于=和IN。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器
spring.shardingsphere.sharding.tables..database-strategy.standard.range-algorithm-class-name= #范围分片算法类名称,用于BETWEEN,可选。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器
#用于多分片键的复合分片场景
spring.shardingsphere.sharding.tables..database-strategy.complex.sharding-columns= #分片列名称,多个列以逗号分隔
spring.shardingsphere.sharding.tables..database-strategy.complex.algorithm-class-name= #复合分片算法类名称。该类需实现ComplexKeysShardingAlgorithm接口并提供无参数的构造器
#行表达式分片策略
spring.shardingsphere.sharding.tables..database-strategy.inline.sharding-column= #分片列名称
spring.shardingsphere.sharding.tables..database-strategy.inline.algorithm-expression= #分片算法行表达式,需符合groovy语法
#Hint分片策略
spring.shardingsphere.sharding.tables..database-strategy.hint.algorithm-class-name= #Hint分片算法类名称。该类需实现HintShardingAlgorithm接口并提供无参数的构造器
#分表策略,同分库策略
spring.shardingsphere.sharding.tables..table-strategy.xxx= #省略
spring.shardingsphere.sharding.tables..key-generator.column= #自增列名称,缺省表示不使用自增主键生成器
spring.shardingsphere.sharding.tables..key-generator.type= #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
spring.shardingsphere.sharding.tables..key-generator.props.= #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
spring.shardingsphere.sharding.binding-tables[0]= #绑定表规则列表
spring.shardingsphere.sharding.binding-tables[1]= #绑定表规则列表
spring.shardingsphere.sharding.binding-tables[x]= #绑定表规则列表
spring.shardingsphere.sharding.broadcast-tables[0]= #广播表规则列表
spring.shardingsphere.sharding.broadcast-tables[1]= #广播表规则列表
spring.shardingsphere.sharding.broadcast-tables[x]= #广播表规则列表
spring.shardingsphere.sharding.default-data-source-name= #未配置分片规则的表将通过默认数据源定位
spring.shardingsphere.sharding.default-database-strategy.xxx= #默认数据库分片策略,同分库策略
spring.shardingsphere.sharding.default-table-strategy.xxx= #默认表分片策略,同分表策略
spring.shardingsphere.sharding.default-key-generator.type= #默认自增列值生成器类型,缺省将使用org.apache.shardingsphere.core.keygen.generator.impl.SnowflakeKeyGenerator。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
spring.shardingsphere.sharding.default-key-generator.props.= #自增列值生成器属性配置, 比如SNOWFLAKE算法的worker.id与max.tolerate.time.difference.milliseconds
spring.shardingsphere.sharding.master-slave-rules..master-data-source-name= #详见读写分离部分
spring.shardingsphere.sharding.master-slave-rules..slave-data-source-names[0]= #详见读写分离部分
spring.shardingsphere.sharding.master-slave-rules..slave-data-source-names[1]= #详见读写分离部分
spring.shardingsphere.sharding.master-slave-rules..slave-data-source-names[x]= #详见读写分离部分
spring.shardingsphere.sharding.master-slave-rules..load-balance-algorithm-class-name= #详见读写分离部分
spring.shardingsphere.sharding.master-slave-rules..load-balance-algorithm-type= #详见读写分离部分
spring.shardingsphere.props.sql.show= #是否开启SQL显示,默认值: false
spring.shardingsphere.props.executor.size= #工作线程数量,默认值: CPU核数
核心概念
SQL
逻辑表
水平拆分的数据库(表)的相同逻辑和数据结构表的总称。例:订单数据根据主键尾数拆分为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);
广播表
指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表
分片
分片键
用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:将订单表中的订单主键的尾数取模分片, 则订单主键为分片字段。 SQL中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,ShardingSphere也支持根据多个字段进行分片
分片算法
通过分片算法将数据分片,支持通过=
、>=
、<=
、>
、<
、BETWEEN
和IN
分片。分片算法需要应用方开发者自行实现,可实现的灵活度非常高。
目前提供4种分片算法。由于分片算法和业务实现紧密相关,因此并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。
精确分片算法
对应PreciseShardingAlgorithm,用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用
范围分片算法
对应RangeShardingAlgorithm,用于处理使用单一键作为分片键的BETWEEN AND、>、<、>=、<=进行分片的场景。需要配合StandardShardingStrategy使用
复合分片算法
对应ComplexKeysShardingAlgorithm,用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合ComplexShardingStrategy使用
Hint分片算法
对应HintShardingAlgorithm,用于处理使用Hint行分片的场景。需要配合HintShardingStrategy使用
分片策略
标准分片策略
对应StandardShardingStrategy。提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND, >, <, >=, <=分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理
复合分片策略
对应ComplexShardingStrategy。复合分片策略。提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度
行表达式分片策略
对应InlineShardingStrategy。使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->{u_id % 8}
表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0
到t_user_7
Hint分片策略
对应HintShardingStrategy。通过Hint指定分片值而非从SQL中提取分片值的方式进行分片的策略
不分片策略
对应NoneShardingStrategy。不分片的策略
SQL Hint
对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段。例:内部系统,按照员工登录主键分库,而数据库中并无此字段。SQL Hint支持通过Java API和SQL注释(待实现)两种方式使用
解析引擎
抽象语法树
解析过程分为词法解析和语法解析。 词法解析器用于将SQL拆解为不可再分的原子符号,称为Token。并根据不同数据库方言所提供的字典,将其归类为关键字,表达式,字面量和操作符。 再使用语法解析器将SQL转换为抽象语法树
例如,以下SQL:
SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18
解析之后的为抽象语法树见下图:
最后,通过对抽象语法树的遍历去提炼分片所需的上下文,并标记有可能需要改写的位置。 供分片使用的解析上下文包含查询选择项(Select Items)、表信息(Table)、分片条件(Sharding Condition)、自增主键信息(Auto increment Primary Key)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit、Rownum、Top)。 SQL的一次解析过程是不可逆的,一个个Token的按SQL原本的顺序依次进行解析,性能很高。 考虑到各种数据库SQL方言的异同,在解析模块提供了各类数据库的SQL方言字典
SQL解析引擎
SQL解析作为分库分表类产品的核心,其性能和兼容性是最重要的衡量指标。 ShardingSphere的SQL解析器经历了3代产品的更新迭代。
第一代SQL解析器为了追求性能与快速实现,在1.4.x之前的版本使用Druid作为SQL解析器。经实际测试,它的性能远超其它解析器。
第二代SQL解析器从1.5.x版本开始,ShardingSphere采用完全自研的SQL解析引擎。 由于目的不同,ShardingSphere并不需要将SQL转为一颗完全的抽象语法树,也无需通过访问器模式进行二次遍历。它采用对SQL半理解
的方式,仅提炼数据分片需要关注的上下文,因此SQL解析的性能和兼容性得到了进一步的提高。
第三代SQL解析器则从3.0.x版本开始,ShardingSphere尝试使用ANTLR作为SQL解析的引擎,并计划根据DDL -> TCL -> DAL –> DCL -> DML –>DQL
这个顺序,依次替换原有的解析引擎,目前仍处于替换迭代中。 使用ANTLR的原因是希望ShardingSphere的解析引擎能够更好的对SQL进行兼容。对于复杂的表达式、递归、子查询等语句,虽然ShardingSphere的分片核心并不关注,但是会影响对于SQL理解的友好度。 经过实例测试,ANTLR解析SQL的性能比自研的SQL解析引擎慢3-10倍左右。为了弥补这一差距,ShardingSphere将使用PreparedStatement
的SQL解析的语法树放入缓存。 因此建议采用PreparedStatement
这种SQL预编译的方式提升性能。
第三代SQL解析引擎的整体结构划分如下图所示:
路由引擎
分片路由
用于根据分片键进行路由的场景,又细分为直接路由、标准路由和笛卡尔积路由这3种类型
直接路由
满足直接路由的条件相对苛刻,它需要通过Hint(使用HintAPI直接指定路由至库表)方式分片,并且是只分库不分表的前提下,则可以避免SQL解析和之后的结果归并。 因此它的兼容性最好,可以执行包括子查询、自定义函数等复杂情况的任意SQL。直接路由还可以用于分片键不在SQL中的场景。例如,设置用于数据库分片的键为3
hintManager.setDatabaseShardingValue(3);
假如路由算法为value % 2
,当一个逻辑库t_order
对应2个真实库t_order_0
和t_order_1
时,路由后SQL将在t_order_1
上执行。下方是使用API的代码样例:
String sql = "SELECT * FROM t_order";
try (
HintManager hintManager = HintManager.getInstance();
Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
hintManager.setDatabaseShardingValue(3);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
//...
}
}
}
标准路由
标准路由是ShardingSphere最为推荐使用的分片方式,它的适用范围是不包含关联查询或仅包含绑定表之间关联查询的SQL。 当分片运算符是等于号时,路由结果将落入单库(表),当分片运算符是BETWEEN或IN时,则路由结果不一定落入唯一的库(表),因此一条逻辑SQL最终可能被拆分为多条用于执行的真实SQL。 举例说明,如果按照order_id
的奇数和偶数进行数据分片,一个单表查询的SQL如下:
SELECT * FROM t_order WHERE order_id IN (1, 2);
那么路由的结果应为:
SELECT * FROM t_order_0 WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 WHERE order_id IN (1, 2);
笛卡尔路由
笛卡尔路由是最复杂的情况,它无法根据绑定表的关系定位分片规则,因此非绑定表之间的关联查询需要拆解为笛卡尔积组合执行。 如果上个示例中的SQL并未配置绑定表关系,那么路由的结果应为:
SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
笛卡尔路由查询性能较低,需谨慎使用
广播路由
对于不携带分片键的SQL,则采取广播路由的方式。根据SQL类型又可以划分为全库表路由、全库路由、全实例路由、单播路由和阻断路由这5种类型
全库表路由
全库表路由用于处理对数据库中与其逻辑表相关的所有真实表的操作,主要包括不带分片键的DQL和DML,以及DDL等。例如
SELECT * FROM t_order WHERE good_prority IN (1, 10);
则会遍历所有数据库中的所有表,逐一匹配逻辑表和真实表名,能够匹配得上则执行。路由后成为
SELECT * FROM t_order_0 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_1 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_2 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_3 WHERE good_prority IN (1, 10);
全库路由
全库路由用于处理对数据库的操作,包括用于库设置的SET类型的数据库管理命令,以及TCL这样的事务控制语句。 在这种情况下,会根据逻辑库的名字遍历所有符合名字匹配的真实库,并在真实库中执行该命令,例如:
SET autocommit=0;
在t_order
中执行,t_order
有2个真实库。则实际会在t_order_0
和t_order_1
上都执行这个命令
全实例路由
全实例路由用于DCL操作,授权语句针对的是数据库的实例。无论一个实例中包含多少个Schema,每个数据库的实例只执行一次。例如:
CREATE USER [email protected] identified BY '123';
这个命令将在所有的真实数据库实例中执行,以确保customer用户可以访问每一个实例
单播路由
单播路由用于获取某一真实表信息的场景,它仅需要从任意库中的任意真实表中获取数据即可。例如:
DESCRIBE t_order;
t_order的两个真实表t_order_0,t_order_1的描述结构相同,所以这个命令在任意真实表上选择执行一次
阻断路由
阻断路由用于屏蔽SQL对数据库的操作,例如:
USE order_db;
这个命令不会在真实数据库中执行,因为ShardingSphere采用的是逻辑Schema的方式,无需将切换数据库Schema的命令发送至数据库中。
路由引擎的整体结构划分如下图:
改写引擎
正确性改写
在包含分表的场景中,需要将分表配置中的逻辑表名称改写为路由之后所获取的真实表名称。仅分库则不需要表名称的改写。除此之外,还包括补列和分页信息修正等内容
标识符改写
需要改写的标识符包括表名称、索引名称以及Schema名称。
表名称改写是指将找到逻辑表在原始SQL中的位置,并将其改写为真实表的过程。表名称改写是一个典型的需要对SQL进行解析的场景。 从一个最简单的例子开始,若逻辑SQL为:
SELECT order_id FROM t_order WHERE order_id=1;
假设该SQL配置分片键order_id,并且order_id=1的情况,将路由至分片表1。那么改写之后的SQL应该为:
SELECT order_id FROM t_order_1 WHERE order_id=1;
在这种最简单的SQL场景中,是否将SQL解析为抽象语法树似乎无关紧要,只要通过字符串查找和替换就可以达到SQL改写的效果。 但是下面的场景,就无法仅仅通过字符串的查找替换来正确的改写SQL了:
SELECT order_id FROM t_order WHERE order_id=1 AND remarks=' t_order xxx';
正确改写的SQL应该是
SELECT order_id FROM t_order_1 WHERE order_id=1 AND remarks=' t_order xxx';
补列
需要在查询语句中补列通常由两种情况导致。 第一种情况是ShardingSphere需要在结果归并时获取相应数据,但该数据并未能通过查询的SQL返回。 这种情况主要是针对GROUP BY和ORDER BY。结果归并时,需要根据GROUP BY
和ORDER BY
的字段项进行分组和排序,但如果原始SQL的选择项中若并未包含分组项或排序项,则需要对原始SQL进行改写。 先看一下原始SQL中带有结果归并所需信息的场景:
SELECT order_id, user_id FROM t_order ORDER BY user_id;
由于使用user_id进行排序,在结果归并中需要能够获取到user_id的数据,而上面的SQL是能够获取到user_id数据的,因此无需补列。
如果选择项中不包含结果归并时所需的列,则需要进行补列,如以下SQL:
SELECT order_id FROM t_order ORDER BY user_id;
由于原始SQL中并不包含需要在结果归并中需要获取的user_id,因此需要对SQL进行补列改写。补列之后的SQL是:
SELECT order_id, user_id AS ORDER_BY_DERIVED_0 FROM t_order ORDER BY user_id;
分页修正
从多个数据库获取分页数据与单数据库的场景是不同的。 假设每10条数据为一页,取第2页数据。在分片环境下获取LIMIT 10, 10,归并之后再根据排序条件取出前10条数据是不正确的。 举例说明,若SQL为:
SELECT score FROM t_score ORDER BY score DESC LIMIT 1, 2;
下图展示了不进行SQL的改写的分页执行结果:
通过图中所示,想要取得两个表中共同的按照分数排序的第2条和第3条数据,应该是95
和90
。 由于执行的SQL只能从每个表中获取第2条和第3条数据,即从t_score_0表中获取的是90
和80
;从t_score_0表中获取的是85
和75
。 因此进行结果归并时,只能从获取的90
,80
,85
和75
之中进行归并,那么结果归并无论怎么实现,都不可能获得正确的结果。
正确的做法是将分页条件改写为LIMIT 0, 3
,取出所有前两页数据,再结合排序条件计算出正确的数据。 下图展示了进行SQL改写之后的分页执行结果。
越获取偏移量位置靠后数据,使用LIMIT分页方式的效率就越低。 有很多方法可以避免使用LIMIT进行分页。比如构建行记录数量与行偏移量的二级索引,或使用上次分页数据结尾ID作为下次查询条件的分页方式等。
分页信息修正时,如果使用占位符的方式书写SQL,则只需要改写参数列表即可,无需改写SQL本身
批量拆分
在使用批量插入的SQL时,如果插入的数据是跨分片的,那么需要对SQL进行改写来防止将多余的数据写入到数据库中。 插入操作与查询操作的不同之处在于,查询语句中即使用了不存在于当前分片的分片键,也不会对数据产生影响;而插入操作则必须将多余的分片键删除。
INSERT INTO t_order (order_id, xxx) VALUES (1, 'xxx'), (2, 'xxx'), (3, 'xxx');
假设数据库仍然是按照order_id的奇偶值分为两片的,仅将这条SQL中的表名进行修改,然后发送至数据库完成SQL的执行 ,则两个分片都会写入相同的记录。 虽然只有符合分片查询条件的数据才能够被查询语句取出,但存在冗余数据的实现方案并不合理。因此需要将SQL改写为:
INSERT INTO t_order_0 (order_id, xxx) VALUES (2, 'xxx');
INSERT INTO t_order_1 (order_id, xxx) VALUES (1, 'xxx'), (3, 'xxx');
SQL
由于SQL语法灵活复杂,分布式数据库和单机数据库的查询场景又不完全相同,难免有和单机数据库不兼容的SQL出现。
本文详细罗列出已明确可支持的SQL种类以及已明确不支持的SQL种类,尽量让使用者避免踩坑。
示例
支持的SQL
SQL | 必要条件 |
---|---|
SELECT * FROM tbl_name | |
SELECT * FROM tbl_name WHERE (col1 = ? or col2 = ?) and col3 = ? | |
SELECT * FROM tbl_name WHERE col1 = ? ORDER BY col2 DESC LIMIT ? | |
SELECT COUNT(*), SUM(col1), MIN(col1), MAX(col1), AVG(col1) FROM tbl_name WHERE col1 = ? | |
SELECT COUNT(col1) FROM tbl_name WHERE col2 = ? GROUP BY col1 ORDER BY col3 DESC LIMIT ?, ? | |
INSERT INTO tbl_name (col1, col2,…) VALUES (?, ?, ….) | |
INSERT INTO tbl_name VALUES (?, ?,….) | |
INSERT INTO tbl_name (col1, col2, …) VALUES (?, ?, ….), (?, ?, ….) | |
UPDATE tbl_name SET col1 = ? WHERE col2 = ? | |
DELETE FROM tbl_name WHERE col1 = ? | |
CREATE TABLE tbl_name (col1 int, …) | |
ALTER TABLE tbl_name ADD col1 varchar(10) | |
DROP TABLE tbl_name | |
TRUNCATE TABLE tbl_name | |
CREATE INDEX idx_name ON tbl_name | |
DROP INDEX idx_name ON tbl_name | |
DROP INDEX idx_name | |
SELECT DISTINCT * FROM tbl_name WHERE col1 = ? | |
SELECT COUNT(DISTINCT col1) FROM tbl_name |
不支持的SQL
SQL | 不支持原因 |
---|---|
INSERT INTO tbl_name (col1, col2, …) VALUES(1+2, ?, …) | VALUES语句不支持运算表达式 |
INSERT INTO tbl_name (col1, col2, …) SELECT col1, col2, … FROM tbl_name WHERE col3 = ? | INSERT … SELECT |
SELECT COUNT(col1) as count_alias FROM tbl_name GROUP BY col1 HAVING count_alias > ? | HAVING |
SELECT * FROM tbl_name1 UNION SELECT * FROM tbl_name2 | UNION |
SELECT * FROM tbl_name1 UNION ALL SELECT * FROM tbl_name2 | UNION ALL |
SELECT * FROM ds.tbl_name1 | 包含schema |
SELECT SUM(DISTINCT col1), SUM(col1) FROM tbl_name | 详见DISTINCT支持情况详细说明 |
SELECT * FROM tbl_name WHERE to_date(create_time, ‘yyyy-mm-dd’) = ? | 会导致全路由 |
DISTINCT支持情况详细说明
SQL | |
---|---|
SELECT DISTINCT * FROM tbl_name WHERE col1 = ? | |
SELECT DISTINCT col1 FROM tbl_name | |
SELECT DISTINCT col1, col2, col3 FROM tbl_name | |
SELECT DISTINCT col1 FROM tbl_name ORDER BY col1 | |
SELECT DISTINCT col1 FROM tbl_name ORDER BY col2 | |
SELECT DISTINCT(col1) FROM tbl_name | |
SELECT AVG(DISTINCT col1) FROM tbl_name | |
SELECT SUM(DISTINCT col1) FROM tbl_name | |
SELECT COUNT(DISTINCT col1) FROM tbl_name | |
SELECT COUNT(DISTINCT col1) FROM tbl_name GROUP BY col1 | |
SELECT COUNT(DISTINCT col1 + col2) FROM tbl_name | |
SELECT COUNT(DISTINCT col1), SUM(DISTINCT col1) FROM tbl_name | |
SELECT COUNT(DISTINCT col1), col1 FROM tbl_name GROUP BY col1 | |
SELECT col1, COUNT(DISTINCT col1) FROM tbl_name GROUP BY col1 |
不支持的SQL
SQL | 不支持原因 |
---|---|
SELECT SUM(DISTINCT col1), SUM(col1) FROM tbl_name | 同时使用普通聚合函数和DISTINCT聚合函数 |
分页
完全支持MySQL的分页查询
分页性能
性能瓶颈
查询偏移量过大的分页会导致数据库获取数据性能低下,以MySQL为例:
SELECT * FROM t_order ORDER BY id LIMIT 1000000, 10
这句SQL会使得MySQL在无法利用索引的情况下跳过1000000条记录后,再获取10条记录,其性能可想而知。 而在分库分表的情况下(假设分为2个库),为了保证数据的正确性,SQL会改写为:
SELECT * FROM t_order ORDER BY id LIMIT 0, 1000010
即将偏移量前的记录全部取出,并仅获取排序后的最后10条记录。这会在数据库本身就执行很慢的情况下,进一步加剧性能瓶颈。 因为原SQL仅需要传输10条记录至客户端,而改写之后的SQL则会传输1,000,010 * 2
的记录至客户端
ShardingSphere的优化
ShardingSphere进行了2个方面的优化。
首先,采用流式处理 + 归并排序的方式来避免内存的过量占用。由于SQL改写不可避免的占用了额外的带宽,但并不会导致内存暴涨。 与直觉不同,大多数人认为ShardingSphere会将1,000,010 * 2
记录全部加载至内存,进而占用大量内存而导致内存溢出。 但由于每个结果集的记录是有序的,因此ShardingSphere每次比较仅获取各个分片的当前结果集记录,驻留在内存中的记录仅为当前路由到的分片的结果集的当前游标指向而已。 对于本身即有序的待排序对象,归并排序的时间复杂度仅为O(n)
,性能损耗很小。
其次,ShardingSphere对仅落至单分片的查询进行进一步优化。 落至单分片查询的请求并不需要改写SQL也可以保证记录的正确性,因此在此种情况下,ShardingSphere并未进行SQL改写,从而达到节省带宽的目的。
分页方案优化
由于LIMIT并不能通过索引查询数据,因此如果可以保证ID的连续性,通过ID进行分页是比较好的解决方案:
SELECT * FROM t_order WHERE id > 100000 AND id <= 100010 ORDER BY id
或通过记录上次查询结果的最后一条记录的ID进行下一页的查询:
SELECT * FROM t_order WHERE id > 100000 LIMIT 10
分页子查询
MySQL和PostgreSQL都支持LIMIT分页,无需子查询:
SELECT * FROM t_order o ORDER BY id LIMIT ? OFFSET ?
自增ID,根据ID范围进行分表(左闭右开)
规则案例
优点
缺点
A库ID=userId % 库数量 2
表ID userId /库数量 2 % 表数据4
1.8
1.8
1.8
2.5.5
3.4.0
1.18.16
4.1.1
4.12
1.1.16
true
org.springframework.boot
spring-boot-starter-web
${spring.boot.version}
org.springframework.boot
spring-boot-starter-test
${spring.boot.version}
test
com.baomidou
mybatis-plus-boot-starter
${mybatisplus.boot.starter.version}
mysql
mysql-connector-java
8.0.27
org.projectlombok
lombok
${lombok.version}
org.apache.shardingsphere
sharding-jdbc-spring-boot-starter
${sharding-jdbc.version}
junit
junit
${junit.version}
org.springframework.boot
spring-boot-maven-plugin
${spring.boot.version}
true
true
分库分表
数据库
shop_order_0
shop_order_1
SQL脚本
CREATE TABLE `product_order_0` (
`id` bigint NOT NULL AUTO_INCREMENT,
`out_trade_no` varchar(64) DEFAULT NULL COMMENT '订单唯一标识',
`state` varchar(11) DEFAULT NULL COMMENT 'NEW 未支付订单,PAY已经支付订单,CANCEL超时取消订单',
`create_time` datetime DEFAULT NULL COMMENT '订单生成时间',
`pay_amount` decimal(16,2) DEFAULT NULL COMMENT '订单实际支付价格',
`nickname` varchar(64) DEFAULT NULL COMMENT '昵称',
`user_id` bigint DEFAULT NULL COMMENT '用户id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `ad_config` (
`id` bigint unsigned NOT NULL COMMENT '主键id',
`config_key` varchar(1024) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '配置key',
`config_value` varchar(1024) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '配置value',
`type` varchar(128) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '类型',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
#绑定表
CREATE TABLE `product_order_item_0` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`product_order_id` bigint DEFAULT NULL COMMENT '订单号',
`product_id` bigint DEFAULT NULL COMMENT '产品id',
`product_name` varchar(128) DEFAULT NULL COMMENT '商品名称',
`buy_num` int DEFAULT NULL COMMENT '购买数量',
`user_id` bigint DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
实体模型类
//数据库实体类
public interface ProductOrderMapper extends BaseMapper {
}
public interface ProductOrderItemMapper extends BaseMapper {
}
public interface AdConfigMapper extends BaseMapper {
}
@Data
@TableName("product_order")
@EqualsAndHashCode(callSuper = false)
public class ProductOrderDO {
private Long id;
private String outTradeNo;
private String state;
private Date createTime;
private Double payAmount;
private String nickname;
private Long userId;
}
@Data
@TableName("product_order_item")
@EqualsAndHashCode(callSuper = false)
public class ProductOrderItemDO {
private Long id;
private Long productOrderId;
private Long productId;
private String productName;
private Integer buyNum;
private Long userId;
}
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ad_config")
public class AdConfigDO {
private Integer id;
private String configKey;
private String configValue;
private String type;
}
配置文件配置
# 打印执行的数据库以及语句
spring.shardingsphere.props.sql.show=true
# 数据源 db0
spring.shardingsphere.datasource.names=ds0,ds1
# 第一个数据库
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://192.168.2.133:3306/shop_order_0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456
# 第二个数据库
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mysql://192.168.2.133:3306/shop_order_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=123456
#配置workId
spring.shardingsphere.sharding.tables.product_order.key-generator.props.worker.id=1
#配置广播表
spring.shardingsphere.sharding.broadcast-tables=ad_config
spring.shardingsphere.sharding.tables.ad_config.key-generator.column=id
spring.shardingsphere.sharding.tables.ad_config.key-generator.type=SNOWFLAKE
#配置【默认分库策略】
#spring.shardingsphere.sharding.default-database-strategy.inline.sharding-column=user_id
#spring.shardingsphere.sharding.default-database-strategy.inline.algorithm-expression=ds$->{user_id % 2 }
#配置分库规则
#spring.shardingsphere.sharding.tables.product_order.database-strategy.inline.sharding-column=user_id
#spring.shardingsphere.sharding.tables.product_order.database-strategy.inline.algorithm-expression=ds$->{user_id % 2 }
#id生成策略
spring.shardingsphere.sharding.tables.product_order.key-generator.column=id
spring.shardingsphere.sharding.tables.product_order.key-generator.type=SNOWFLAKE
# 指定product_order表的数据分布情况,配置数据节点,行表达式标识符使用 ${...} 或 $->{...},
# 但前者与 Spring 本身的文件占位符冲突,所以在 Spring 环境中建议使用 $->{...}
#spring.shardingsphere.sharding.tables.product_order.actual-data-nodes=ds0.product_order_$->{0..1}
#spring.shardingsphere.sharding.tables.product_order.actual-data-nodes=ds$->{0..1}.product_order_$->{0..1}
# 指定product_order表的分片策略,分片策略包括【分片键和分片算法】
#spring.shardingsphere.sharding.tables.product_order.table-strategy.inline.sharding-column=id
#spring.shardingsphere.sharding.tables.product_order.table-strategy.inline.algorithm-expression=product_order_$->{id % 2}
# 指定product_order_item表的分片策略,分片策略包括【分片键和分片算法】
#spring.shardingsphere.sharding.tables.product_order_item.actual-data-nodes=ds$->{0..1}.product_order_item_$->{0..1}
#spring.shardingsphere.sharding.tables.product_order_item.table-strategy.inline.sharding-column=product_order_id
#spring.shardingsphere.sharding.tables.product_order_item.table-strategy.inline.algorithm-expression=product_order_item_$->{product_order_id % 2}
#配置绑定表
#spring.shardingsphere.sharding.binding‐tables[0]=product_order,product_order_item
#精准分片-水平分表
# 指定product_order表的数据分布情况,配置数据节点,在 Spring 环境中建议使用 $->{...}
spring.shardingsphere.sharding.tables.product_order.actual-data-nodes=ds$->{0..1}.product_order_$->{0..1}
#spring.shardingsphere.sharding.tables.product_order.actual-data-nodes=ds0.product_order_$->{0..1}
单库下⼀般使⽤Mysql⾃增ID, 但是分库分表后,会造成不同分⽚
上的数据表主键会重复
需求
业界常⽤ID解决⽅案
分布式ID⽣成器Snowflake需要注意的问题
配置实操
#配置workId
spring.shardingsphere.sharding.tables.product_order.key-generator.props.worker.id=1
方式实现一
订单id使⽤MybatisPlus的配置
@TableId(value = "id", type = IdType.ASSIGN_ID)
默认实现类为DefaultIdentifierGenerator雪花算法
方式实现二
使⽤Sharding-Jdbc配置⽂件,注释DO类⾥⾯的id分配策略
#id生成策略
spring.shardingsphere.sharding.tables.product_order.key-generator.column=id
spring.shardingsphere.sharding.tables.product_order.key-generator.type=SNOWFLAKE
workerId动态指定
动态制定workerId
/**
* sharding-jdbc 使用雪花算法生成主键时,workId配置
*
*/
@Configuration
@Slf4j
public class WorkIdConfig {
/**
* 设置workerIdValue的值
*/
static {
int workId = 0;
try {
workId = getWorkId();
} catch (Exception e) {
log.error("生成workId发生异常.", e);
}
System.setProperty("workerIdValue", String.valueOf(workId));
}
/**
* 根据机器名称生成workId
* @return
*/
private static int getWorkId() {
String hostAddress = System.getenv("HOSTNAME");
log.info("============= hostAddress: {} =============", hostAddress);
int[] ints = StringUtils.toCodePoints(hostAddress);
int sum = 0;
for (int b : ints) {
sum += b;
}
int workId = (sum % 32);
log.info("============== workId:{} =============", workId);
return workId;
}
}
配置文件配置:
spring.shardingsphere.sharding.tables.product_order.key-generator.props.worker.id=${workerIdValue}
什么是⼴播表
注意点
配置实战
ad_config表
配置文件
#配置广播表
spring.shardingsphere.sharding.broadcast-tables=ad_config
spring.shardingsphere.sharding.tables.ad_config.key-generator.column=id
spring.shardingsphere.sharding.tables.ad_config.key-generator.type=SNOWFLAKE
什么是绑定表
分库分表配置
实战代码配置
#配置绑定表
#错误配置,因为带有空格所以会导致绑定关系失败
spring.shardingsphere.sharding.binding‐tables[0] = product_order,product_order_item
#正确配置绑定表实现,逻辑表两边不能带有空格
spring.shardingsphere.sharding.binding-tables[0] =product_order,product_order_item
问题:分库分表带来的分布式事务问题
常见分布式事务处理方案