面试方案中间件

sharding-jdbc分库分表-使用及原理

2021-06-01  本文已影响0人  三不猴子

原文来自于:三不猴子

水平分只是对数据进行拆分,不改变结构

在系统设计阶段就应该根据业务耦合松紧来确定垂直分库,垂直分表方案,在数据量及访问压力不是特别大的情况,首先考虑缓存、读写分离、索引技术等方案。若数据量极大,且持续增长,再考虑水平分库水平分表

分库分表带来的问题

image.png
优点 缺点 适用场景
使用UUID算法生成唯一id 无任何依赖 ID太长,且不是数字类型 生成seesion_id
利用单机数据库主键自增来生成唯一id 方便接入,单调递增 生成效率低,强依赖于数据库,id是连续的 适用于并发量不高的业务。
多数据库主键自增生成唯一id 方便接入,单调递增,生成效率比单机数据库高 不方便扩容,强依赖于数据库,id是连续的 适合分库分表的架构生成id
数据库分段发号生成唯一id 效率高 强依赖于数据库,id是连续的 适合id生成并发量高的业务,并且id连续 不会破坏信息安全的业务。
基于snowflake算法生成唯一id 效率高,运行期间可以不依赖其他组件 id分布不均,对有些业务会造成数据倾斜的问题 适合id生成并发量高的业务

Sharding-JDBC介绍

它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种
ORM框架。
Sharding-JDBC的核心功能为数据分片和读写分离,通过Sharding-JDBC,应用可以透明的使用jdbc访问已经分库分表、读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。
适用于任何基于Java的ORM框架,如: Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。

image.png

sharding-jdbc流程

通过日志分析,Sharding-JDBC在拿到用户要执行的sql之后干了哪些事儿:

  1. 解析sql,获取片键值,比如order_id
  2. Sharding-JDBC通过规则配置 t_order_$->{order_id % 2 + 1},知道了当order_id为偶数时,应该往t_order_1表插数据,为奇数时,往t_order_2插数据。
  3. 于是Sharding-JDBC根据order_id的值改写sql语句,改写后的SQ语句是真实所要执行的SQL语句。
  4. 执行改写后的真实sql语句
  5. 将所有真正执行sql的结果进行汇总合并,返回。

Sharding-JDBC执行原理

在了解Sharding-JDBC的执行原理前,需要了解以下概念:

SQL解析

当Sharding-JDBC接受到一条SQL语句时,会陆续执行 SQL解析 => 查询优化 => SQL路由 => SQL改写 => SQL执行 =>结果归并 ,最终返回执行结果。

image.png

SQL解析过程分为词法解析和语法解析。 词法解析器用于将SQL拆解为不可再分的原子符号,称为Token。并根据不同数据库方言所提供的字典,将其归类为关键字,表达式,字面量和操作符。 再使用语法解析器将SQL转换为抽象语法树。
例如,以下SQL:

SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18

解析之后的为抽象语法树见下图:

image.png

通过对抽象语法树的遍历去提炼分片所需的上下文,并标记有可能需要SQL改写(后边介绍)的位置。供分片使用的解析上下文包含查询选择项(Select Items)、表信息(Table)、分片条件(Sharding Condition)、自增主键信息(Auto increment Primary Key)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit、Rownum、Top)。

SQL路由

SQL路由就是把针对逻辑表的数据操作映射到对数据结点操作的过程。

根据解析上下文匹配数据库和表的分片策略,并生成路由路径。

对于携带分片键的SQL,根据分片键操作符不同可以划分为单片路由(分片键的操作符是等号)、多片路由(分片键的操作符是IN)和范围路由(分片键的操作符是BETWEEN),不携带分片键的SQL则采用广播路由。

标准路由

标准路由是 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 o JOIN t_order_item i ON o.order_id=i.order_id  WHERE order_id IN (1, 2);

那么路由的结果应为:

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_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id  WHERE order_id IN (1, 2);

可以看到,SQL 拆分的数目与单表是一致的。

笛卡尔路由

笛卡尔路由是最复杂的情况,它无法根据绑定表的关系定位分片规则,因此非绑定表之间的关联查询需要拆解为笛卡尔积组合执行。 如果上个示例中的 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);

SQL改写

工程师面向逻辑表书写的SQL,并不能够直接在真实的数据库中执行,SQL改写用于将逻辑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;

补列

再比如,Sharding-JDBC需要在结果归并时获取相应数据,但该数据并未能通过查询的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;

补列的另一种情况是使用 AVG 聚合函数。在分布式的场景中,使用 avg1 + avg2 + avg3 / 3 计算平均值并不正确,需要改写为 (sum1 + sum2 + sum3) / (count1 + count2 + count3)。 这就需要将包含 AVG 的 SQL 改写为 SUM 和 COUNT,并在结果归并时重新计算平均值。例如以下 SQL:

SELECT AVG(price) FROM t_order WHERE user_id=1;

需要改写为:

SELECT COUNT(price) AS AVG_DERIVED_COUNT_0, SUM(price) AS AVG_DERIVED_SUM_0 FROM t_order WHERE user_id=1;

然后才能够通过结果归并正确的计算平均值。

分页修正

从多个数据库获取分页数据与单数据库的场景是不同的。 假设每 10 条数据为一页,取第 2 页数据。在分片环境下获取 LIMIT 10, 10,归并之后再根据排序条件取出前 10 条数据是不正确的。 举例说明,若 SQL 为:

SELECT score FROM t_score ORDER BY score DESC LIMIT 1, 2;

下图展示了不进行 SQL 的改写的分页执行结果。

image

通过图中所示,想要取得两个表中共同的按照分数排序的第 2 条和第 3 条数据,应该是 9590。 由于执行的 SQL 只能从每个表中获取第 2 条和第 3 条数据,即从 t_score_0 表中获取的是 9080;从 t_score_1 表中获取的是 8575。 因此进行结果归并时,只能从获取的 90808575 之中进行归并,那么结果归并无论怎么实现,都不可能获得正确的结果。

正确的做法是将分页条件改写为 LIMIT 0, 3,取出所有前两页数据,再结合排序条件计算出正确的数据。 下图展示了进行 SQL 改写之后的分页执行结果。

image

越获取偏移量位置靠后数据,使用 LIMIT 分页方式的效率就越低。 有很多方法可以避免使用 LIMIT 进行分页。比如构建行记录数量与行偏移量的二级索引,或使用上次分页数据结尾 ID 作为下次查询条件的分页方式等。

分页信息修正时,如果使用占位符的方式书写 SQL,则只需要改写参数列表即可,无需改写 SQL 本身。

SQL执行

内存限制模式

使用此模式的前提是,ShardingSphere 对一次操作所耗费的数据库连接数量不做限制。 如果实际执行的 SQL 需要对某数据库实例中的 200 张表做操作,则对每张表创建一个新的数据库连接,并通过多线程的方式并发处理,以达成执行效率最大化。 并且在 SQL 满足条件情况下,优先选择流式归并,以防止出现内存溢出或避免频繁垃圾回收情况。

连接限制模式

使用此模式的前提是,ShardingSphere 严格控制对一次操作所耗费的数据库连接数量。 如果实际执行的 SQL 需要对某数据库实例中的 200 张表做操作,那么只会创建唯一的数据库连接,并对其 200 张表串行处理。 如果一次操作中的分片散落在不同的数据库,仍然采用多线程处理对不同库的操作,但每个库的每次操作仍然只创建一个唯一的数据库连接。 这样即可以防止对一次请求对数据库连接占用过多所带来的问题。该模式始终选择内存归并。

内存限制模式适用于 OLAP 操作,可以通过放宽对数据库连接的限制提升系统吞吐量; 连接限制模式适用于 OLTP 操作,OLTP 通常带有分片键,会路由到单一的分片,因此严格控制数据库连接,以保证在线系统数据库资源能够被更多的应用所使用,是明智的选择。

自动化执行引擎

两种模式的切换交由静态的初始化配置,是缺乏灵活应对能力的。在实际的使用场景中,面对不同 SQL 以及占位符参数,每次的路由结果是不同的。 这就意味着某些操作可能需要使用内存归并,而某些操作则可能选择流式归并更优,具体采用哪种方式不应该由用户在 ShardingSphere 启动之前配置好,而是应该根据 SQL 和占位符参数的场景,来动态的决定连接模式。用户无需了解所谓的内存限制模式和连接限制模式是什么,而是交由执行引擎根据当前场景自动选择最优的执行方案。

自动化执行引擎将连接模式的选择粒度细化至每一次 SQL 的操作。 针对每次 SQL 请求,自动化执行引擎都将根据其路由结果,进行实时的演算和权衡,并自主地采用恰当的连接模式执行,以达到资源控制和效率的最优平衡。

image.png
在 maxConnectionSizePerQuery 允许的范围内,当一个连接需要执行的请求数量大于 1 时,意味着当前的数据库连接无法持有相应的数据结果集,则必须采用内存归并; 反之,当一个连接需要执行的请求数量等于 1 时,意味着当前的数据库连接可以持有相应的数据结果集,则可以采用流式归并。

**每一次的连接模式的选择,是针对每一个物理数据库的**。在同一次查询中,如果路由至一个以上的数据库,每个数据库的连接模式不一定一样,它们可能是混合存在的形态。

当数据源使用数据库连接池等控制数据库连接数量的技术时,在获取数据库连接时,如果不妥善处理并发,则有一定几率发生死锁。 在多个请求相互等待对方释放数据库连接资源时,将会产生饥饿等待,造成交叉的死锁问题。

假设一次查询需要在某一数据源上获取两个数据库连接,并路由至同一个数据库的两个分表查询。 则有可能出现查询 A 已获取到该数据源的 1 个数据库连接,并等待获取另一个数据库连接;而查询 B 也已经在该数据源上获取到的一个数据库连接,并同样等待另一个数据库连接的获取。 如果数据库连接池的允许最大连接数是 2,那么这 2 个查询请求将永久的等待下去。下图描绘了死锁的情况。
image.png
ShardingSphere 为了避免死锁的出现,在获取数据库连接时进行了同步处理。 它在创建执行单元时,以原子性的方式一次性获取本次 SQL 请求所需的全部数据库连接,杜绝了每次查询请求获取到部分资源的可能。 由于对数据库的操作非常频繁,每次获取数据库连接时时都进行锁定,会降低 ShardingSphere 的并发。因此,ShardingSphere 在这里进行了 2 点优化:

1. 避免锁定一次性只需要获取1个数据库连接的操作。因为每次仅需要获取 1 个连接,则不会发生两个请求相互等待的场景,无需锁定。 对于大部分 OLTP 的操作,都是使用分片键路由至唯一的数据节点,这会使得系统变为完全无锁的状态,进一步提升了并发效率。 除了路由至单分片的情况,读写分离也在此范畴之内。
2. 仅针对内存限制模式时才进行资源锁定。在使用连接限制模式时,所有的查询结果集将在装载至内存之后释放掉数据库连接资源,因此不会产生死锁等待的问题。
image.png
## 结果归并

将从各个数据节点获取的多数据结果集,组合成为一个结果集并正确的返回至请求客户端,称为结果归并。

Sharding-JDBC支持的结果归并从功能上可分为遍历、排序、分组、分页和聚合5种类型,它们是组合而非互斥的关系。归并引擎的整体结构划分如下图。
image.png
上一篇 下一篇

猜你喜欢

热点阅读