Sharding-JDBC系列(二):Sharding-JDBC分片、算法、配置、行表达式概念
本篇描述 Sharding-JDBC 分片相关概念,了解相关概念有助于理解 Sharding-JDBC 的使用和配置方式,更好地在项目中应用,有问题时更容易的排查和定位。
Sharding-JDBC 相关概念包有:分片方式包括水平分片和垂直分片;分片算法包括常见的范围分片,取模分片,Hash分片,时间分片等;分表涉及到逻辑表,真实表,绑定表,广播表;基于 Spring Boot Starter 框架使用的行表达式。
此系列文章都是基于 Sharding-JDBC 4.x
版本, 在写此文章时,正式发布的是 4.1.0
版本,点此 4.x 官方文档。
Sharding-JDBC分片
背景
传统的将数据集中存储至单一数据节点的解决方案,在性能、可用性和运维成本这三方面已经难于满足互联网的海量数据场景。
从性能方面来说,由于关系型数据库大多采用 B+ 树类型的索引,在数据量超过阈值的情况下,索引深度的增加也将使得磁盘访问的 IO 次数增加,进而导致查询性能的下降;同时,高并发访问请求也使得集中式数据库成为系统的最大瓶颈。
从可用性的方面来讲,服务化的无状态型,能够达到较小成本的随意扩容,这必然导致系统的最终压力都落在数据库之上。而单一的数据节点,或者简单的主从架构,已经越来越难以承担。数据库的可用性,已成为整个系统的关键。
从运维成本方面考虑,当一个数据库实例中的数据达到阈值以上,对于 DBA 的运维压力就会增大。数据备份和恢复的时间成本都将随着数据量的大小而愈发不可控。一般来讲,单一数据库实例的数据的阈值在 1TB 之内,是比较合理的范围。
在传统的关系型数据库无法满足互联网场景需要的情况下,将数据存储至原生支持分布式的 NoSQL 的尝试越来越多。 但 NoSQL 对 SQL 的不兼容性以及生态圈的不完善,使得它们在与关系型数据库的博弈中始终无法完成致命一击,而关系型数据库的地位却依然不可撼动。
通过分库和分表进行数据的拆分来使得各个表的数据量保持在阈值以下,以及对流量进行疏导应对高访问量,是应对高并发和海量数据系统的有效手段。
目标
Sarding-JDBC数据分片尽量透明化分库分表所带来的影响,让使用方尽量像使用一个数据库一样使用水平分片之后的数据库集群,是 Apache ShardingSphere 数据分片模块的主要设计目标。
数据分片概念
核心概念
逻辑表
逻辑表(Logic Table):水平拆分的数据库(表)的相同逻辑和数据结构表的总称。
例:订单数据根据主键尾数拆分为10张表,分别是 t_order_0 到 t_order_9 ,他们的逻辑表名为 t_order。
真实表
真实表(Actual Table):在分片的数据库中真实存在的物理表。即上个示例中的 t_order_0 到 t_order_9 。
数据节点
数据结点(Data Node):数据分片的最小单元。由 数据源名称 和 数据表 组成,例:ds_0.t_order_0。
绑定表
绑定表(Binding Table):指分片规则一致的主表和子表。
例如:t_order 表和 t_order_item 表,都是按照 order_id 分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
广播表
广播表(Broadcast Table):指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
分片方式
数据分片指按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中以达到提升性能瓶颈以及可用性的效果。
数据分片的有效手段是对关系型数据库进行分库和分表。分库和分表均可以有效的避免由数据量超过可承受阈值而产生的查询瓶颈。分库还能够用于有效的分散对数据库单点的访问量;分表虽然无法缓解数据库压力,但却能够提供尽量将分布式事务转化为本地事务的可能,一旦涉及到跨库的更新操作,分布式事务往往会使问题变得复杂。使用多主多从的分片方式,可以有效的避免数据单点,从而提升数据架构的可用性。
数据分片的拆分方式又分为垂直分片和水平分片。下面章节是 ShardingSphere-JDBC 分表和分库涉及一些概念,了解这些概念可以更好帮助选择分片策略和实践配置。
垂直分片
按照业务拆分的方式称为垂直分片,又称为纵向拆分,它的核心理念是专库专用。
在拆分之前,一个数据库由多个数据表构成,每个表对应着不同的业务。而拆分之后,则是按照业务将表进行归类,分布到不同的数据库中,从而将压力分散至不同的数据库。
在现在流行的微服务架构下,通常会按业务或职能划分微服务,每个服务有自己的数据库,这就是典型的垂直分片。
垂直分片无法解决单表大数据量的问题,则需要 水平分片 来进一步处理。
水平分片
水平分片又称为横向拆分。 相对于垂直分片,它不再将数据根据业务逻辑分类,而是通过某个字段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个分片仅包含数据的一部分。 例如:根据主键分片,偶数主键的记录放入 0 库(或表),奇数主键的记录放入 1 库(或表)。
水平分片从理论上突破了单机数据量处理的瓶颈,并且扩展相对自由,是分库分表的标准解决方案。
合理采用分表,可以在降低单表数据量的情况下,尽量使用本地事务,善于使用同库不同表可有效避免分布式事务带来的麻烦。
分片概念
分片键
分片键(Sharding Key):用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。
例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。 SQL中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,ShardingSphere 也支持根据多个字段进行分片。
分片算法
分片算法(Sharding Algorithm):通过分片算法将数据分片,支持通过 =, >=, <=, >, <, BETWEEN, IN
分片。分片算法需要应用方开发者自行实现,可实现的灵活度非常高。
目前提供 4 种分片算法。由于分片算法和业务实现紧密相关,此并未提供内置分片算法(官方文档描述,5.x 开始会提供内置的分片算法),而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。
精确分片算法
对应
PreciseShardingAlgorithm
,用于处理使用单一键作为分片键的=
与IN
进行分片的场景。需要配合StandardShardingStrategy 使用。范围分片算法
对应
RangeShardingAlgorithm
,用于处理使用单一键作为分片键的BETWEEN AND, >, <, >=, <=
进行分片的场景。需要配合 StandardShardingStrategy 使用。复合分片算法
对应
ComplexKeysShardingAlgorithm
,用于处理使用 多键 作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合 ComplexShardingStrategy 使用。Hint分片算法
对应
HintShardingAlgorithm
,用于处理使用Hint
行分片的场景。需要配合 HintShardingStrategy 使用。
分片策略
分片策略(Sharding Strategy):包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前提供5种分片策略。
标准分策策略
对应 StandardShardingStrategy
。提供对 SQ L语句中的 =
, >
, <
, >=
, <=
, 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 注释(待实现)两种方式使用。详情请参见 强制分片路由。
分片算法
范围分片
按照一定的范围将数据分片到不同的表中。如 1 ~ 10000 分布到 table_0 表中,10001 ~ 20000 分布到 table_1 表中。
取模分片
取一个数字类型的字段作为分片键,对此分片键取模。通常用 id 作为分片键,例如要分成 4 个表,则用 id % 4 来分配。
Hash 分片
对某些字段进行 Hash ,另一致性 Hash 可以解决数据扩容问题。
时间分片
通常按照年、月来分,数量大甚至可以按天分表。
配置概念
分片规则
分片规则配置的总入口。包含数据源配置、表配置、绑定表配置以及读写分离配置等。
分表配置
逻辑表名称、数据节点与分表规则的配置。
数据源配置
真实数据源列表。
数据节点配置
用于配置逻辑表与真实表的映射关系。可分为均匀分布和自定义分布两种形式。
均匀分布
指数据表在每个数据源内呈现均匀分布的态势,例如:
1
2
3
4
5
6db0
t_order0
t_order1
db1
t_order0
t_order1那么数据节点的配置如下:
1
db0.t_order1, db1.t_order0, db1.t_order1
自定义分布
指数据表呈现有特定规则的分布,例如:
1
2
3
4
5
6
7db0
t_order0
t_order1
db1
t_order2
t_order3
t_order4那么数据节点的配置如下:
1
db0.t_order1, db1.t_order2, db1.t_order3, db1.t_order4
分片策略配置
对于分片策略存有数据源分片策略和表分片策略两种维度。
数据源分片策略
对应于
DatabaseShardingStrategy
。用于配置数据被分配的目标数据源。表分片策略
对应于
TableShardingStrategy
。用于配置数据被分配的目标表,该目标表存在与该数据的目标数据源内。故表分片策略是依赖与数据源分片策略的结果的。
两种策略的API完全相同。
自增主键生成策略
通过在客户端生成自增主键替换以数据库原生自增主键的方式,做到分布式主键无重复。
行表达式
配置的简化与一体化是 行表达式 所希望解决的两个主要问题。
在繁琐的数据分片规则配置中,随着数据节点的增多,大量的重复配置使得配置本身不易被维护。通过行表达式可以有效的简化数据节点配置工作量。
对于常见的分片算法,使用Java代码实现并不有助于配置的统一管理。通过行表达式书写分片算法,可以有效的将规则配置一同存放,更加易于浏览与存储。
语法说明
行表达式的使用非常直观,只需要在配置中使用${ expression }
或$->{ expression }
标识行表达式即可。
注意:行表达式标识符可以使用 ${...}
或 $->{...}
,但前者与 Spring 本身的属性文件占位符冲突,因此在 Spring 环境中使用行表达式标识符建议使用 $->{...}
。
目前支持数据节点和分片算法这两个部分的配置。行表达式的内容使用的是Groovy的语法,Groovy能够支持的所有操作,行表达式均能够支持。例如:
${begin..end}
表示范围区间
${[unit1, unit2, unit_x]}
表示枚举值
行表达式中如果出现连续多个${ expression }
或$->{ expression }
表达式,整个表达式最终的结果将会根据每个子表达式的结果进行笛卡尔组合。
例如,以下行表达式:
1 | ${['online', 'offline']}_table${1..3} |
最终会解析为:
1 | online_table2, online_table3, offline_table1, offline_table2, offline_table3 |
配置数据节点
对于均匀分布的数据节点,如果数据结构如下:
1 | db0 |
用行表达式可以简化为:
1 | db${0..1}.t_order${0..1} |
或者
1 | db$->{0..1}.t_order$->{0..1} |
对于自定义的数据节点,如果数据结构如下:
1 | db0 |
用行表达式可以简化为:
1 | db0.t_order${0..1},db1.t_order${2..4} |
或者
1 | db0.t_order$->{0..1},db1.t_order$->{2..4} |
对于有前缀的数据节点,也可以通过行表达式灵活配置,如果数据结构如下:
1 | db0 |
可以使用分开配置的方式,先配置包含前缀的数据节点,再配置不含前缀的数据节点,再利用行表达式笛卡尔积的特性,自动组合即可。 上面的示例,用行表达式可以简化为:
1 | db${0..1}.t_order_${10..20} |
或者
1 | db$->{0..1}.t_order_$->{10..20} |
配置分片算法
对于只有一个分片键的使用=
和IN
进行分片的SQL,可以使用行表达式代替编码方式配置。
行表达式内部的表达式本质上是一段Groovy代码,可以根据分片键进行计算的方式,返回相应的真实数据源或真实表名称。
例如:分为10个库,尾数为0的路由到后缀为0的数据源, 尾数为1的路由到后缀为1的数据源,以此类推。用于表示分片算法的行表达式为:
1 | ds${id % 10} |
或者
1 | ds$->{id % 10} |
强制分片路由
实现动机
通过解析SQL语句提取分片键列与值并进行分片是ShardingSphere对SQL零侵入的实现方式。若SQL语句中没有分片条件,则无法进行分片,需要全路由。
在一些应用场景中,分片条件并不存在于SQL,而存在于外部业务逻辑。因此需要提供一种通过外部指定分片结果的方式,在ShardingSphere中叫做Hint。
实现机制
ShardingSphere使用ThreadLocal管理分片键值。可以通过编程的方式向HintManager中添加分片条件,该分片条件仅在当前线程内生效。
除了通过编程的方式使用强制分片路由,ShardingSphere还计划通过SQL中的特殊注释的方式引用Hint,使开发者可以采用更加透明的方式使用该功能。
指定了强制分片路由的SQL将会无视原有的分片逻辑,直接路由至指定的真实数据节点。
内核剖析
ShardingSphere的3个产品的数据分片主要流程是完全一致的。 核心由:
SQL解析 => 执行器优化 => SQL路由 => SQL改写 => SQL执行 => 结果归并
的流程组成。
SQL解析
分为词法解析和语法解析。 先通过词法解析器将SQL拆分为一个个不可再分的单词。再使用语法解析器对SQL进行理解,并最终提炼出解析上下文。 解析上下文包括表、选择项、排序项、分组项、聚合函数、分页信息、查询条件以及可能需要修改的占位符的标记。
执行器优化
合并和优化分片条件,如OR等。
SQL路由
根据解析上下文匹配用户配置的分片策略,并生成路由路径。目前支持分片路由和广播路由。
SQL改写
将SQL改写为在真实数据库中可以正确执行的语句。SQL改写分为正确性改写和优化改写。
SQL执行
通过多线程执行器异步执行。
结果归并
将多个执行结果集归并以便于通过统一的JDBC接口输出。结果归并包括流式归并、内存归并和使用装饰者模式的追加归并这几种方式。
分布式主键
参考 分布式主键,注意查看末尾一段内容,从 5.0.0 版本开始会有些变化。
数据分片挑战
虽然数据分片解决了性能、可用性以及单点备份恢复等问题,但分布式的架构在获得了收益的同时,也引入了新的问题。
面对如此散乱的分库分表之后的数据,应用开发工程师和数据库管理员对数据库的操作变得异常繁重就是其中的重要挑战之一。他们需要知道数据需要从哪个具体的数据库的分表中获取。
另一个挑战则是,能够正确的运行在单节点数据库中的 SQL,在分片之后的数据库中并不一定能够正确运行。例如,分表导致表名称的修改,或者分页、排序、聚合分组等操作的不正确处理。
跨库事务也是分布式的数据库集群要面对的棘手事情。 合理采用分表,可以在降低单表数据量的情况下,尽量使用本地事务,善于使用同库不同表可有效避免分布式事务带来的麻烦。 在不能避免跨库事务的场景,有些业务仍然需要保持事务的一致性。 而基于 XA 的分布式事务由于在并发度高的场景中性能无法满足需要,并未被互联网巨头大规模使用,他们大多采用最终一致性的柔性事务代替强一致事务。
Sharding-JDBC系列(二):Sharding-JDBC分片、算法、配置、行表达式概念
http://blog.gxitsky.com/2019/09/20/sharding-jdbc-2-sharding-concept/