数据分片剖析
# 数据分片剖析
# 核心概念
# 表概念
表是透明化数据分片的关键概念。 ShardingSphere通过提供多样化的表类型,适配不同场景下的数据分片需求。
真实表:水平拆分后数据库中真实存在的物理表。 即例如 order_0 、 order_1......。
逻辑表:被水平拆分数据库(表)的逻辑名称,例如:物理表order_0 、 order_1......的逻辑表名为
t_order
。绑定表:指分片规则一致的主表和子表。 使用绑定表进行多表关联查询时,必须使用分片键进行关联,否则会出现笛卡尔积关联或跨库关联,从而影响查询效率。 例如:
t_order
表和t_order_item
表,均按照order_id
分片,并且使用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);
在配置绑定表关系,并且使用
order_id
进行关联后,路由的 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);
广播表:指所有的分片数据源中都存在的表,表结构及其数据在每个数据库中均完全一致。 适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
# 数据节点
数据分片的最小单元,由数据源名称和真实表组成。 例:ds_0.t_order_0
。逻辑表与真实表的映射关系,可分为均匀分布和自定义分布两种形式。(这涉及到Inline行表达式的使用。后面在对Inline行表达式解析一起说)。
# 分片算法(ShardingAlgorithm)
由于分片算法和业务实现紧密相关,因此ShardingSphere并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法,目前提供4种分片算法:
- 标准分片算法:用于处理使用单一键作为分片键的
=
、IN
、BETWEEN AND
、>
、<
、>=
、<=
进行分片的场景。- 精确分片算法,PreciseShardingAlgorithm:用于处理使用单一键作为分片键的=与IN进行分片的场景。
- 范围分片算法,RangeShardingAlgorithm:用于处理使用单一键作为分片键的BETWEEN AND、>、<、>=、<=进行分片的场景。
- 复合分片算法,ComplexKeysShardingAlgorithm:用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。
- Hint分片算法,HintShardingAlgorithm:用于处理使用Hint行分片的场景。即对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段。例:内部系统,按照员工登录主键分库,而数据库中并无此字段。 SQL Hint 支持通过 Java API 和 SQL 注释(待实现)两种方式使用。这个算法是用于强制分片路由。
# 分片策略(ShardingStrategy)
包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。 真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前提供5种分片策略:
标准分片策略,StandardShardingStrategy:只支持单分片键,提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。
PreciseShardingAlgorithm是必选的,RangeShardingAlgorithm是可选的。但是SQL中使用了范围操作,如果不配置RangeShardingAlgorithm会采用全库路由扫描,效率低。
复合分片策略,ComplexShardingStrategy:支持多分片键。提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。
行表达式分片策略,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:不分片的策略。
# 分片策略配置
对于分片策略存有数据源分片策略和表分片策略两种维度,两种策略的API完全相同。
- 数据源分片策略:用于配置数据被分配的目标数据源。
- 表分片策略:用于配置数据被分配的目标表,由于表存在与数据源内,所以表分片策略是依赖数据源分片策略结果的。
# 流程剖析
ShardingSphere 3个产品的数据分片功能主要流程是完全一致的,如下图所示:
- SQL解析:SQL解析分为词法解析和语法解析。 先通过词法解析器将SQL拆分为一个个不可再分的单词。再使用语法解析器对SQL进行理解,并最终提炼出解析上下文。 Sharding-JDBC采用不同的解析器对SQL进行解析,解析器类型如下:
Implementation Class | Description |
---|---|
MySQLParserFacade | 基于 MySQL 的 SQL 解析器入口 |
PostgreSQLParserFacade | 基于 PostgreSQL 的 SQL 解析器入口 |
SQLServerParserFacade | 基于 SQLServer 的 SQL 解析器入口 |
OracleParserFacade | 基于 Oracle 的 SQL 解析器入口 |
SQL92ParserFacade | 基于 SQL92 的 SQL 解析器入口 |
OpenGaussParserFacade | 基于 openGauss 的 SQL 解析器入口 |
- 查询优化:负责合并和优化分片条件,如OR等。
- SQL路由:根据解析上下文匹配用户配置的分片策略,并生成路由路径。目前支持分片路由和广播路由。
- SQL改写:将SQL改写为在真实数据库中可以正确执行的语句。SQL改写分为正确性改写和优化改写。
- SQL执行:通过多线程执行器异步执行SQL。
- 结果归并:将多个执行结果集归并以便于通过统一的JDBC接口输出。结果归并包括流式归并、内存归并和使用装饰者模式的追加归并这几种方式。
# SQL使用规范
# 支持项
全部常用的路由至单数据节点的 SQL
路由至多数据节点的 SQL 全面支持 DML、DDL、DCL、TCL 和常用 DAL。 支持分页、去重、排序、分组、聚合、表关联(实验性支持跨库关联)等复杂查询。
SELECT select_expr [, select_expr ...] FROM table_reference [, table_reference ...] [WHERE predicates] [GROUP BY {col_name | position} [ASC | DESC], ...] [ORDER BY {col_name | position} [ASC | DESC], ...] [LIMIT {[offset,] row_count | row_count OFFSET offset}]
实验性支持特指使用 Federation 执行引擎提供支持。 该引擎处于快速开发中,用户虽基本可用,但仍需大量优化,是实验性产品。
# 不支持项(路由至多数据节点)
不支持CASE WHEN、HAVING和UNION (实验性支持)
支持分页子查询,但其他子查询有限支持,无论嵌套多少层,只能解析至第一个包含数据表的子查询,一旦在下层嵌套中再次找到包含数据表的子查询将直接抛出解析异常。
例如,以下子查询可以支持:
SELECT COUNT(*) FROM (SELECT * FROM b_order o)
以下子查询不支持:
SELECT COUNT(*) FROM (SELECT * FROM b_order o WHERE o.id IN (SELECT id FROM b_order WHERE status = ?))
简单来说,通过子查询进行非功能需求,在大部分情况下是可以支持的。比如分页、统计总数等;而通过子查询实现业务查询当前并不能支持。
由于归并的限制,子查询中包含聚合函数目前无法支持。
不支持包含schema的SQL。因为ShardingSphere的理念是像使用一个数据源一样使用多数据源,因此对SQL的访问都是在同一个逻辑schema之上。
当分片键处于运算表达式或函数中的SQL时,将采用全路由的形式获取结果。例如下面SQL,create_time为分片键:
SELECT * FROM b_order WHERE to_date(create_time, 'yyyy-mm-dd') = '2020- 05-05';
由于ShardingSphere只能通过SQL字面提取用于分片的值,因此当分片键处于运算表达式或函数中时,ShardingSphere无法提前获取分片键位于数据库中的值,从而无法计算出真正的分片值。
# 分页查询
完全支持 MySQL、PostgreSQL 和 Oracle 的分页查询,SQLServer 由于分页查询较为复杂,仅部分支持。
# 性能瓶颈
查询偏移量过大的分页会导致数据库获取数据性能低下,以 MySQL 为例:
SELECT * FROM t_order ORDER BY id LIMIT 1000000, 10
这句 SQL 会使得 MySQL 在无法利用索引的情况下跳过 1,000,000 条记录后,再获取 10 条记录,其性能可想而知。 而在分库分表的情况下(假设分为2个库),为了保证数据的正确性,SQL 会改写为:
SELECT * FROM t_order ORDER BY id LIMIT 0, 1000010
即将偏移量前的记录全部取出,并仅获取排序后的最后 10 条记录。这会在数据库本身就执行很慢的情况下,进一步加剧性能瓶颈。 因为原SQL仅需要传输 10 条记录至客户端,而改写之后的SQL则会传输 1,000,010 * 2
的记录至客户端。
# ShardingSphere 的优化
ShardingSphere 进行了 2 个方面的优化。
- 首先,采用流式处理 + 归并排序的方式来避免内存的过量占用。
- 其次,ShardingSphere 对仅落至单分片的查询进行进一步优化。
# 分页方案优化
由于 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
# Inline行表达式
Inline行表达式是可以简化数据节点和分片算法配置信息。主要是解决配置简化、配置一体化。
# 语法格式
行表达式的使用非常直观,只需要在配置中使用 ${ expression }
或 $->{ expression }
标识行表达式即可。(Groovy 的语法)
${begin..end} 表示范围区间
${[unit1, unit2, unit_x]} 表示枚举值
行表达式中如果出现连续多个 ${ expression }
或 $->{ expression }
表达式,整个表达式最终的结果将会根据每个子表达式的结果进行笛卡尔组合。例如,以下行表达式:
${['online', 'offline']}_table${1..3}
最终解释为
online_table1, online_table2, online_table3, offline_table1, offline_table2, offline_table3
# 简化数据节点配置
刚才上面提到数据节点概念有均匀分布与不均匀分布,Inline行表达式就简化了数据节点的配置:
均匀分布的数据节点:
db0
├── t_order0
└── t_order1
db1
├── t_order0
└── t_order1
# 用行表达式可以简化
db${0..1}.t_order${0..1}
不均匀分布的数据节点:
# 不均匀分布的数据节点
db0
├── t_order0
└── t_order1
db1
├── t_order2
├── t_order3
└── t_order4
# 用行表达式可以简化
db0.b_order${0..1},db1.b_order${2..4}
# 简化分片算法配置
上面提到,精确分片算法(PreciseShardingAlgorithm)与行表达式分片策略(InlineShardingStrategy)可以使用行表达式ds${id % 10}
代替编码方式配置。
# 分布式主键
ShardingSphere > 功能 > 数据分片 > 核心概念 > 分布式主键 (opens new window)
ShardingSphere不仅提供了内置的分布式主键生成器,例如UUID、SNOWFLAKE,还抽离出分布式主键生成器的接口(ShardingKeyGenerator),方便用户自行实现自定义的自增主键生成器。
# 内置主键生成器
- UUID:采用UUID.randomUUID()的方式产生分布式主键。
- SNOWFLAKE:在分片规则配置模块可配置每个表的主键生成策略,默认使用雪花算法,生成64bit的长整型数据。
# 自定义主键生成器
自定义主键类,实现
ShardingKeyGenerator
接口按SPI规范配置自定义主键类(在ShardingSphere中,很多功能实现类的加载方式是通过SPI注入的方式完成的)。
注意:在
resources
目录下新建META-INF
文件夹,再新建services
文件夹,然后新建文件的名字为org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator
,打开文件,复制自定义主键类全路径到文件中保存。自定义主键类应用配置
#对应主键字段名 spring.shardingsphere.sharding.tables.t_tablename.key-generator.column=id #对应主键类getType返回内容 spring.shardingsphere.sharding.tables.t_tablename.key-generator.type=MY_KEY
# 常见配置
这里记录YAML的配置方式,其他见请参见用户手册 (opens new window)。
rules:
- !SHARDING
tables: # 数据分片规则配置
<logic-table-name> (+): # 逻辑表名称
actualDataNodes (?): # 由数据源名 + 表名组成(参考 Inline 语法规则)
databaseStrategy (?): # 分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一
standard: # 用于单分片键的标准分片场景
shardingColumn: # 分片列名称
shardingAlgorithmName: # 分片算法名称
complex: # 用于多分片键的复合分片场景
shardingColumns: # 分片列名称,多个列以逗号分隔
shardingAlgorithmName: # 分片算法名称
hint: # Hint 分片策略
shardingAlgorithmName: # 分片算法名称
none: # 不分片
tableStrategy: # 分表策略,同分库策略
keyGenerateStrategy: # 分布式序列策略
column: # 自增列名称,缺省表示不使用自增主键生成器
keyGeneratorName: # 分布式序列算法名称
autoTables: # 自动分片表规则配置
t_order_auto: # 逻辑表名称
actualDataSources (?): # 数据源名称
shardingStrategy: # 切分策略
standard: # 用于单分片键的标准分片场景
shardingColumn: # 分片列名称
shardingAlgorithmName: # 自动分片算法名称
bindingTables (+): # 绑定表规则列表
- <logic_table_name_1, logic_table_name_2, ...>
- <logic_table_name_1, logic_table_name_2, ...>
broadcastTables (+): # 广播表规则列表
- <table-name>
- <table-name>
defaultDatabaseStrategy: # 默认数据库分片策略
defaultTableStrategy: # 默认表分片策略
defaultKeyGenerateStrategy: # 默认的分布式序列策略
defaultShardingColumn: # 默认分片列名称
# 分片算法配置
shardingAlgorithms:
<sharding-algorithm-name> (+): # 分片算法名称
type: # 分片算法类型
props: # 分片算法属性配置
# ...
# 分布式序列算法配置
keyGenerators:
<key-generate-algorithm-name> (+): # 分布式序列算法名称
type: # 分布式序列算法类型
props: # 分布式序列算法属性配置
# ...