ShardingSphere实践——数据分片
Posted wzy0623
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ShardingSphere实践——数据分片相关的知识,希望对你有一定的参考价值。
目录
一、功能详解
1. 背景
传统的将数据集中存储至单一节点的解决方案,在性能、可用性和运维成本这三方面已经难于满足海量数据的场景。
从性能方面来说,由于关系型数据库大多采用B+树类型的索引,在数据量超过阈值的情况下,索引深度的增加也将使得磁盘访问的IO次数增加,进而导致查询性能的下降。同时,高并发访问请求也使得集中式数据库成为系统的最大瓶颈。
从可用性的方面来讲,服务化的无状态性,能够达到较小成本的随意扩容,这必然导致系统的最终压力都落在数据库之上。而单一的数据节点,或者简单的主从架构,已经越来越难以承担。数据库的可用性,已成为整个系统的关键。
从运维成本方面考虑,当一个数据库实例中的数据达到阈值以上,对于DBA的运维压力就会增大。数据备份和恢复的时间成本都将随着数据量的大小而愈发不可控。一般来讲,单一数据库实例数据量的阈值在1TB之内,是比较合理的范围。
在传统的关系型数据库无法满足互联网场景需要的情况下,将数据存储至原生支持分布式的NoSQL的尝试越来越多。但NoSQL对SQL的不兼容性以及生态圈的不完善,使得它们在与关系型数据库的博弈中始终无法完成致命一击,而关系型数据库的地位却依然不可撼动。
数据分片指按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中,以达到提升性能以及可用性的效果。数据分片的有效手段是对关系型数据库进行分库和分表。分库和分表均可以有效地避免由数据量超过可承受阈值而产生的查询瓶颈。除此之外,分库还能够用于有效地分散对数据库单点的访问量;分表虽然无法缓解数据库压力,但却能够提供尽量将分布式事务转化为本地事务的可能,一旦涉及到跨库的更新操作,分布式事务往往会使问题变得复杂。使用多主多从的分片方式,可以有效避免数据单点,从而提升数据架构的可用性。
通过分库和分表进行数据拆分,使得各个表的数据量保持在阈值以下,以及对流量进行疏导,是应对高并发访问和海量数据系统的有效手段。
数据分片的拆分方式又分为垂直分片和水平分片。按照业务拆分的方式称为垂直分片,又称为纵向拆分,它的核心理念是专库专用。在拆分之前,一个数据库由多个数据表构成,每个表对应着不同的业务。而拆分之后,则是按照业务将表进行归类,分布到不同的数据库中,从而将压力分散至不同的数据库。下图展示了根据业务需要,将用户表和订单表垂直分片到不同的数据库的方案。
垂直分片往往需要对架构和设计进行调整。通常来讲,是来不及应对互联网业务需求快速变化的。而且,它也并无法真正解决单点瓶颈。垂直拆分可以缓解数据量和访问量带来的问题,但无法根治。如果垂直拆分之后,表中的数据量依然超过单节点所能承载的阈值,则需要水平分片来进一步处理。
水平分片又称为横向拆分。相对于垂直分片,它不再将数据根据业务逻辑分类,而是通过某个字段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个分片仅包含数据的一部分。例如:根据主键分片,偶数主键的记录放入0库(或表),奇数主键的记录放入1库(或表),如下图所示。
水平分片从理论上突破了单机数据量处理的瓶颈,并且扩展相对自由,是数据分片的标准解决方案。
虽然数据分片解决了性能、可用性以及单点备份恢复等问题,但分布式的架构在获得了收益的同时,也引入了新的问题。面对如此散乱的分片之后的数据,应用开发工程师和数据库管理员对数据库的操作变得异常繁重就是其中的重要挑战之一。他们需要知道数据需要从哪个具体的数据库的子表中获取。
另一个挑战则是,能够正确地运行在单节点数据库中的SQL,在分片之后的数据库中并不一定能够正确运行。例如,分表导致表名称的修改,或者分页、排序、聚合分组等操作的不正确处理。
跨库事务也是分布式的数据库集群要面对的棘手事情。合理采用分表,可以在降低单表数据量的情况下,尽量使用本地事务,善于使用同库不同表可有效避免分布式事务带来的麻烦。在不能避免跨库事务的场景,有些业务仍然需要保持事务的一致性。而基于XA的分布式事务由于在并发度高的场景中性能无法满足需要,并未被互联网巨头大规模使用,他们大多采用最终一致性的柔性事务代替强一致事务。
尽量透明化分库分表所带来的影响,让使用方尽量像使用一个数据库一样使用水平分片之后的数据库集群,是ShardingSphere数据分片模块的主要设计目标。
2. 核心概念
本节主要介绍ShardingSphere数据分片的核心概念。
(1)表
表是透明化数据分片的关键概念。ShardingSphere通过提供多样化的表类型,适配不同场景下的数据分片需求。
- 逻辑表
相同结构的水平拆分数据库(表)的逻辑名称,是SQL中表的逻辑标识。例:订单数据根据主键尾数拆分为10张表,分别是t_order_0到t_order_9,他们的逻辑表名为t_order。
- 真实表
在水平拆分的数据库中真实存在的物理表,如上个示例中的t_order_0 到t_order_9。
- 绑定表
指分片规则一致的一组分片表。使用绑定表进行多表关联查询时,如果不使用分片键进行关联,会导致笛卡尔积关联或跨库关联,从而影响查询效率。例如: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);
其中t_order表由于指定了分片条件,ShardingSphere将会以它作为整个绑定表的主表(关联查询中出现的第一个表)。所有路由计算将会只使用主表的策略,那么t_order_item表的分片计算将会使用t_order的条件。
- 广播表
指所有的分片数据源中都存在的表,表结构及其数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
- 单表
指所有的分片数据源中仅唯一存在的表。适用于数据量不大且无需分片的表。
(2)数据节点
数据分片的最小单元,由数据源名称和真实表组成。例:ds_0.t_order_0。逻辑表与真实表的映射关系,可分为均匀分布和自定义分布两种形式。
均匀分布指数据表在每个数据源内呈现均匀分布的态势,例如:
db0
├── t_order0
└── t_order1
db1
├── t_order0
└── t_order1
数据节点的配置如下:
db0.t_order0, db0.t_order1, db1.t_order0, db1.t_order1
自定义分布指数据表呈现有特定规则的分布,例如:
db0
├── t_order0
└── t_order1
db1
├── t_order2
├── t_order3
└── t_order4
数据节点的配置如下:
db0.t_order0, db0.t_order1, db1.t_order2, db1.t_order3, db1.t_order4
(3)分片
- 分片键
用于将数据库(表)水平拆分的数据库字段。例:将订单表中的订单主键取模分片,则订单主键为分片字段。SQL中如果无分片字段,将执行全路由,性能较差。除了对单分片字段的支持,ShardingSphere也支持根据多个字段进行分片。
- 分片算法
用于将数据分片的算法,支持 =、>=、<=、>、<、BETWEEN和IN进行分片。 分片算法可由开发者自行实现,也可使用ShardingSphere内置的分片算法语法糖,灵活度非常高。
- 自动化分片算法
分片算法语法糖,用于便捷地托管所有数据节点,使用者无需关注真实表的物理分布。包括取模、哈希、范围、时间等常用分片算法的实现。
- 自定义分片算法
提供接口让应用开发者自行实现与业务实现紧密相关的分片算法,并允许使用者自行管理真实表的物理分布。自定义分片算法又分为:标准分片算法、复合分片算法和Hint分片算法。
标准分片算法用于处理使用单一键作为分片键的 =、IN、BETWEEN AND、>、<、>=、<=进行分片的场景。复合分片算法用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。Hint分片算法用于处理使用Hint行分片的场景。
- 分片策略
包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。
(4)行表达式
配置的简化与一体化是行表达式所希望解决的两个主要问题。在繁琐的数据分片规则配置中,随着数据节点的增多,大量的重复配置使得配置本身不易被维护。通过行表达式可以有效地简化数据节点配置工作量。
对于常见的分片算法,使用Java代码实现并不有助于配置的统一管理。通过行表达式书写分片算法,可以有效地将规则配置一同存放,更加易于浏览与存储。
行表达式的使用非常直观,只需要在配置中使用 $ expression 或 $-> expression 标识行表达式即可。目前支持数据节点和分片算法这两个部分的配置。行表达式的内容使用的是Groovy的语法,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
对于均匀分布的数据节点,如果数据结构如下:
db0
├── t_order0
└── t_order1
db1
├── t_order0
└── t_order1
用行表达式可以简化为:db$0..1.t_order$0..1 或者 db$->0..1.t_order$->0..1
对于自定义的数据节点,如果数据结构如下:
db0
├── t_order0
└── t_order1
db1
├── t_order2
├── t_order3
└── t_order4
用行表达式可以简化为:db0.t_order$0..1,db1.t_order$2..4 或者 db0.t_order$->0..1,db1.t_order$->2..4
对于有前缀的数据节点,也可以通过行表达式灵活配置,如果数据结构如下:
db0
├── t_order_00
├── t_order_01
├── t_order_02
├── t_order_03
├── t_order_04
├── t_order_05
├── t_order_06
├── t_order_07
├── t_order_08
├── t_order_09
├── t_order_10
├── t_order_11
├── t_order_12
├── t_order_13
├── t_order_14
├── t_order_15
├── t_order_16
├── t_order_17
├── t_order_18
├── t_order_19
└── t_order_20
db1
├── t_order_00
├── t_order_01
├── t_order_02
├── t_order_03
├── t_order_04
├── t_order_05
├── t_order_06
├── t_order_07
├── t_order_08
├── t_order_09
├── t_order_10
├── t_order_11
├── t_order_12
├── t_order_13
├── t_order_14
├── t_order_15
├── t_order_16
├── t_order_17
├── t_order_18
├── t_order_19
└── t_order_20
可以使用分开配置的方式,先配置包含前缀的数据节点,再配置不含前缀的数据节点,再利用行表达式笛卡尔积的特性,自动组合即可。上面的示例,用行表达式可以简化为:db$0..1.t_order_0$0..9, db$0..1.t_order_$10..20 或者 db$0..1.t_order_0$0..9, db$0..1.t_order_$10..20
对于只有一个分片键的使用 = 和 IN 进行分片的SQL,可以使用行表达式代替编码方式配置。行表达式内部的表达式本质上是一段Groovy代码,可以根据分片键进行计算的方式,返回相应的真实数据源或真实表名称。例如:分为 10 个库,尾数为 0 的路由到后缀为 0 的数据源,尾数为 1 的路由到后缀为 1 的数据源,以此类推。用于表示分片算法的行表达式为:ds$id % 10 或者 ds$->id % 10。
(5)分布式主键
传统数据库软件开发中,主键自动生成技术是基本需求。而各个数据库对于该需求也提供了相应的支持,比如mysql的自增键,Oracle的自增序列等。
数据分片后,不同数据节点生成全局唯一主键是非常棘手的问题。同一个逻辑表内的不同实际表之间的自增键由于无法互相感知而产生重复主键。虽然可通过约束自增主键初始值和步长的方式避免碰撞,但需引入额外的运维规则,使解决方案缺乏完整性和可扩展性。
目前有许多第三方解决方案可以完美解决这个问题,如UUID等依靠特定算法自生成不重复键,或者通过引入主键生成服务等。为了方便用户使用、满足不同用户不同使用场景的需求,ShardingSphere不仅提供了内置的分布式主键生成器,例如UUID、SNOWFLAKE,还抽离出分布式主键生成器的接口,方便用户自行实现自定义的自增主键生成器。
内置的主键生成器包括:UUID采用UUID.randomUUID()的方式产生分布式主键;NanoID生成长度为21的字符串分布式主键;SNOWFLAKE使用雪花算法(snowflake)生成64bit的长整型数据。
在分片规则配置模块可配置每个表的主键生成策略,默认使用SNOWFLAKE。雪花算法是由Twitter公布的分布式主键生成算法,它能够保证不同进程主键的不重复性,以及相同进程主键的有序性。
在同一个进程中,它首先是通过时间位保证不重复,如果时间相同则是通过序列位保证。同时由于时间位是单调递增的,且各个服务器如果大体做了时间同步,那么生成的主键在分布式环境可以认为是总体有序的,这就保证了对索引字段的插入的高效性。例如MySQL的Innodb存储引擎的主键。
使用雪花算法生成的主键,二进制表示形式包含四部分,从高位到低位分表为:1bit符号位、41bit时间戳位、10bit工作进程位以及12bit序列号位。
- 符号位
预留的符号位,恒为零。
- 时间戳位(41bit)
41bit的时间戳可以容纳的毫秒数是 2 的 41 次幂,约等于 69.73 年(Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L))。ShardingSphere的雪花算法的时间纪元从 2016年11月1日 零点开始,可以使用到 2086 年,相信能满足绝大部分系统的要求。
- 工作进程位(10bit)
该标志在 Java 进程内是唯一的,如果是分布式应用部署应保证每个工作进程的 id 是不同的。该值默认为0,可通过属性设置。
- 序列号位(12bit)
该序列号位用来在同一个毫秒内生成不同的ID。如果在这个毫秒内生成的数量超过4096(2 的 12 次幂),那么生成器会等待到下个毫秒继续生成。
雪花算法主键的详细结构见下图。
服务器时钟回拨会导致产生重复序列,因此默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,默认分布式主键生成器会等待时钟同步到最后一次主键生成的时间后再继续工作。最大容忍的时钟回拨毫秒数的默认值为0,可通过属性设置。
3. 使用规范
虽然ShardingSphere希望能够完全兼容所有的SQL以及单机数据库,但分布式为数据库带来了更加复杂的场景。ShardingSphere希望能够优先解决海量数据OLTP的问题,OLAP的相关支持,会一点一点的逐渐完善。
(1)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]
select_expr:
* |
[DISTINCT] COLUMN_NAME [AS] [alias] |
(MAX | MIN | SUM | AVG)(COLUMN_NAME | alias) [AS] [alias] |
COUNT(* | COLUMN_NAME | alias) [AS] [alias]
table_reference:
tbl_name [AS] alias] [index_hint_list]
| table_reference ([INNER] | LEFT|RIGHT [OUTER]) JOIN table_factor [JOIN ON conditional_expr | USING (column_list)]
子查询和外层查询同时指定分片键,且分片键的值保持一致时,由内核提供稳定支持。例如:
SELECT * FROM (SELECT * FROM t_order WHERE order_id = 1) o WHERE o.order_id = 1;
用于分页的子查询,由内核提供稳定支持。例如:
SELECT * FROM (SELECT row_.*, rownum rownum_ FROM (SELECT * FROM t_order) row_ WHERE rownum <= ?) WHERE rownum > ?;
当分片键处于运算表达式中时,无法通过SQL字面提取用于分片的值,将导致全路由。例如,假设create_time为分片键:
SELECT * FROM t_order WHERE to_date(create_time, 'yyyy-mm-dd') = '2019-01-01';
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 ?, ? | |
SELECT DISTINCT * FROM tbl_name WHERE col1 = ? | |
SELECT COUNT(DISTINCT col1), SUM(DISTINCT col1) FROM tbl_name | |
(SELECT * FROM tbl_name) | |
SELECT * FROM (SELECT * FROM tbl_name WHERE col1 = ?) o WHERE o.col1 = ? | 子查询和外层查询在同一分片后的数据节点 |
INSERT INTO tbl_name (col1, col2,…) VALUES (?, ?, ….) | |
INSERT INTO tbl_name VALUES (?, ?,….) | |
INSERT INTO tbl_name (col1, col2, …) VALUES(1 + 2, ?, …) | |
INSERT INTO tbl_name (col1, col2, …) VALUES (?, ?, ….), (?, ?, ….) | |
INSERT INTO tbl_name (col1, col2, …) SELECT col1, col2, … FROM tbl_name WHERE col3 = ? | INSERT 表和 SELECT 表相同表或绑定表 |
REPLACE INTO tbl_name (col1, col2, …) SELECT col1, col2, … FROM tbl_name WHERE col3 = ? | REPLACE 表和 SELECT 表相同表或绑定表 |
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 |
慢SQL | 原因 |
SELECT * FROM tbl_name WHERE to_date(create_time, ‘yyyy-mm-dd’) = ? | 分片键在运算表达式中,导致全路由 |
- 实验性支持
实验性支持特指使用Federation执行引擎提供支持。该引擎处于快速开发中,用户虽基本可用,但仍需大量优化,是实验性产品。
子查询和外层查询未同时指定分片键,或分片键的值不一致时,由Federation执行引擎提供支持。例如:
SELECT * FROM (SELECT * FROM t_order) o;
SELECT * FROM (SELECT * FROM t_order) o WHERE o.order_id = 1;
SELECT * FROM (SELECT * FROM t_order WHERE order_id = 1) o;
SELECT * FROM (SELECT * FROM t_order WHERE order_id = 1) o WHERE o.order_id = 2;
当关联查询中的多个表分布在不同的数据库实例上时,由Federation执行引擎提供支持。假设t_order和t_order_item是多数据节点的分片表,并且未配置绑定表规则,t_user和t_user_role是分布在不同的数据库实例上的单表,那么Federation执行引擎能够支持如下常用的关联查询:
SELECT * FROM t_order o INNER JOIN t_order_item i ON o.order_id = i.order_id WHERE o.order_id = 1;
SELECT * FROM t_order o INNER JOIN t_user u ON o.user_id = u.user_id WHERE o.user_id = 1;
SELECT * FROM t_order o LEFT JOIN t_user_role r ON o.user_id = r.user_id WHERE o.user_id = 1;
SELECT * FROM t_order_item i LEFT JOIN t_user u ON i.user_id = u.user_id WHERE i.user_id = 1;
SELECT * FROM t_order_item i RIGHT JOIN t_user_role r ON i.user_id = r.user_id WHERE i.user_id = 1;
SELECT * FROM t_user u RIGHT JOIN t_user_role r ON u.user_id = r.user_id WHERE u.user_id = 1;
SQL示例:
实验性支持的SQL | 必要条件 |
SELECT * FROM (SELECT * FROM tbl_name) o | |
SELECT * FROM (SELECT * FROM tbl_name) o WHERE o.col1 = ? | |
SELECT * FROM (SELECT * FROM tbl_name WHERE col1 = ?) o | |
SELECT * FROM (SELECT * FROM tbl_name WHERE col1 = ?) o WHERE o.col1 = ? | 子查询和外层查询不在同一分片后的数据节点 |
SELECT (SELECT MAX(col1) FROM tbl_name) a, col2 from tbl_name | |
SELECT SUM(DISTINCT col1), SUM(col1) FROM tbl_name | |
SELECT col1, SUM(col2) FROM tbl_name GROUP BY col1 HAVING SUM(col2) > ? | |
SELECT col1, col2 FROM tbl_name UNION SELECT col1, col2 FROM tbl_name | |
SELECT col1, col2 FROM tbl_name UNION ALL SELECT col1, col2 FROM tbl_name |
- 不支持
以下CASE WHEN语句不支持:CASE WHEN 中包含子查询;CASE WHEN 中使用逻辑表名(请使用表别名)。
SQL示例:
不支持的SQL | 原因 | 解决方案 |
INSERT INTO tbl_name (col1, col2, …) SELECT * FROM tbl_name WHERE col3 = ? | SELECT 子句不支持 * 和内置分布式主键生成器 | 无 |
REPLACE INTO tbl_name (col1, col2, …) SELECT * FROM tbl_name WHERE col3 = ? | SELECT 子句不支持 * 和内置分布式主键生成器 | 无 |
SELECT MAX(tbl_name.col1) FROM tbl_name | 查询列是函数表达式时,查询列前不能使用表名 | 使用表别名 |
(2)分页
完全支持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 个方面的优化。
首先,采用流式处理 + 归并排序的方式来避免内存的过量占用。由于SQL改写不可避免的占用了额外的带宽,但并不会导致内存暴涨。与直觉不同,大多数人认为ShardingSphere会将 1,000,010 * 2 记录全部加载至内存,进而占用大量内存而导致内存溢出。但由于每个结果集的记录是有序的,因此ShardingSphere每次比较仅获取各个分片的当前结果集记录,驻留在内存中的记录仅为当前路由到的分片的结果集的当前游标指向而已。按归并思想合并 m 个长度为 n 的已排序数组,时间复杂度为 O(mn(log m)),一般分片数量 m 都较小,可以认为时间复杂度为 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
- 分页子查询
Oracle和SQLServer的分页都需要通过子查询来处理,ShardingSphere支持分页相关的子查询。
Oracle 支持使用 rownum 进行分页:
SELECT * FROM (SELECT row_.*, rownum rownum_ FROM (SELECT o.order_id as order_id FROM t_order o JOIN t_order_item i ON o.order_id = i.order_id) row_ WHERE rownum <= ?) WHERE rownum > ?
目前不支持 rownum + BETWEEN 的分页方式。
SQLServer 支持使用 TOP + ROW_NUMBER() OVER 配合进行分页:
SELECT * FROM (SELECT TOP (?) ROW_NUMBER() OVER (ORDER BY o.order_id DESC) AS rownum, * FROM t_order o) AS temp WHERE temp.rownum > ? ORDER BY temp.order_id
支持 SQLServer 2012 之后的 OFFSET FETCH 的分页方式:
SELECT * FROM t_order o ORDER BY id OFFSET ? ROW FETCH NEXT ? ROWS ONLY
目前不支持使用 WITH xxx AS (SELECT …) 的方式进行分页。由于 Hibernate 自动生成的 SQLServer 分页语句使用了 WITH 语句,因此目前并不支持基于 Hibernate 的 SQLServer 分页。 目前也不支持使用两个 TOP + 子查询的方式实现分页。
MySQL 和 PostgreSQL 都支持 LIMIT 分页,无需子查询:
SELECT * FROM t_order o ORDER BY id LIMIT ? OFFSET ?
二、实现细节
ShardingSphere的三个产品的数据分片主要流程是完全一致的,按照是否进行查询优化,可以分为Standard内核流程和Federation执行引擎流程,如下图所示。 Standard内核流程由 SQL解析 => SQL路由 => SQL改写 => SQL执行 => 结果归并 组成,主要用于处理标准分片场景下的SQL执行。 Federation执行引擎流程由 SQL解析 => 逻辑优化 => 物理优化 => 优化执行 => Standard内核流程 组成,Federation执行引擎内部进行逻辑优化和物理优化,在优化执行阶段依赖Standard内核流程,对优化后的逻辑SQL进行路由、改写、执行和归并。
- SQL解析
分为词法解析和语法解析。先通过词法解析器将SQL拆分为一个个不可再分的单词,再使用语法解析器对SQL进行理解,并最终提炼出解析上下文。解析上下文包括表、选择项、排序项、分组项、聚合函数、分页信息、查询条件以及可能需要修改的占位符的标记。
- SQL路由
根据解析上下文匹配用户配置的分片策略,并生成路由路径。目前支持分片路由和广播路由。
- SQL改写
将SQL改写为在真实数据库中可以正确执行的语句。SQL改写分为正确性改写和优化改写。
- SQL执行
通过多线程执行器异步执行。
- 结果归并
将多个执行结果集归并以便于通过统一的JDBC接口输出。结果归并包括流式归并、内存归并和使用装饰者模式的追加归并这几种方式。
- 查询优化
由Federation执行引擎(开发中)提供支持,对关联查询、子查询等复杂查询进行优化,同时支持跨多个数据库实例的分布式查询,内部使用关系代数优化查询计划,通过最优计划查询出结果。
1. 解析引擎
相对于其他编程语言,SQL是比较简单的。不过,它依然是一门完善的编程语言,因此对SQL的语法进行解析,与解析其他编程语言(如:Java 语言、C 语言、Go 语言等)并无本质区别。
(1)抽象语法树
解析过程分为词法解析和语法解析。词法解析器用于将SQL拆解为不可再分的原子符号,称为Token。并根据不同数据库方言所提供的字典,将其归类为关键字、表达式、字面量和操作符。再使用语法解析器将词法解析器的输出转换为抽象语法树。例如以下SQL:
SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18
解析之后的为抽象语法树见下图。
为了便于理解,抽象语法树中的关键字的Token用绿色表示,变量的Token用红色表示,灰色表示需要进一步拆分。
最后,通过visitor对抽象语法树遍历构造域模型,通过域模型(SQLStatement)去提炼分片所需的上下文,并标记有可能需要改写的位置。供分片使用的解析上下文包含查询选择项(Select Items)、表信息(Table)、分片条件(Sharding Condition)、自增主键信息(Auto increment Primary Key)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit、Rownum、Top)。 SQL的一次解析过程是不可逆的,一个个Token按SQL原本的顺序依次进行解析,性能很高。考虑到各种数据库SQL方言的异同,在解析模块提供了各类数据库的SQL方言字典。
(2)SQL解析引擎
SQL解析作为分库分表类产品的核心,其性能和兼容性是最重要的衡量指标。ShardingSphere的SQL解析器经历了三代产品的更新迭代。
第一代SQL解析器为了追求性能与快速实现,在1.4.x之前的版本使用Druid作为SQL解析器。经实际测试,它的性能远超其它解析器。
第二代SQL解析器从1.5.x版本开始,ShardingSphere采用完全自研的SQL解析引擎。由于目的不同,ShardingSphere并不需要将 SQL转为一颗完全的抽象语法树,也无需通过访问器模式进行二次遍历。它采用对SQL半理解的方式,仅提炼数据分片需要关注的上下文,因此SQL解析的性能和兼容性得到了进一步的提高。
第三代SQL解析器从3.0.x版本开始,尝试使用ANTLR作为SQL解析引擎的生成器,并采用Visit的方式从AST中获取SQL Statement。从5.0.x版本开始,解析引擎的架构已完成重构调整,同时通过将第一次解析得到的AST放入缓存,方便下次直接获取相同SQL的解析结果,来提高解析效率。因此建议用户采用PreparedStatement这种SQL预编译的方式来提升性能。
SQL解析引擎具有以下特性:
- 提供独立的SQL解析功能。
- 可以非常方便地对语法规则进行扩充和修改(使用了ANTLR)。
- 支持多种方言的SQL解析。
下表给出了SQL解析引擎对主要数据库系统SQL的支持状态。
数据库 | 支持状态 |
MySQL | 支持,完善 |
PostgreSQL | 支持,完善 |
SQLServer | 支持 |
Oracle | 支持 |
SQL92 | 支持 |
openGauss | 支持 |
Springboot2.x + ShardingSphere 实现分库分表
Apache ShardingSphere ---;2.数据分片的核心概念
ShardingSphere技术专题「ShardingJDBC进阶阶段」深入领略一下ShardingJDBC数据分片的核心概念和原理