11、Sharding-JDBC 实战 - Sharding-JDBC 核心之执行引擎

ShardingSphere采用一套自动化的执行引擎,负责将路由和改写完成之后的真实SQL安全且高效发送到底层数据源执行。 它不是简单地将SQL通过JDBC直接发送至数据源执行;也并非直接将执行请求放入线程池去并发执行。它更关注平衡数据源连接创建以及内存占用所产生的消耗,以及最大限度地合理利用并发等问题。 执行引擎的目标是自动化的平衡资源控制与执行效率。

一、连接模式

ShardingSphere提供了一种解决思路。 它提出了连接模式(Connection Mode)的概念,将其划分为内存限制模式(MEMORY_STRICTLY)和连接限制模式(CONNECTION_STRICTLY)这两种类型。

1.1 内存限制模式

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

注释:以结果集游标下移进行结果归并的方式,称之为流式归并,它无需将结果数据全数加载至内存,可以有效的节省内存资源,进而减少垃圾回收的频次。可以参考:Sharding-JDBC 核心之归并引擎

1.2 连接限制模式

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

注释:连接限制模式串行处理,只持有一个独立数据库连接时,复用该数据库连接获取下一张分表的查询结果集之前,会将当前的查询结果集全数加载至内存再进行一下次查询,然后将所有的分表结果在内存中归并处理。可以参考:Sharding-JDBC 核心之归并引擎

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

二、自动化执行引擎

ShardingSphere最初将使用何种模式的决定权交由用户配置,让开发者依据自己业务的实际场景需求选择使用内存限制模式或连接限制模式。

这种一分为二的处理方案,将两种模式的切换交由静态的初始化配置,是缺乏灵活应对能力的。在实际的使用场景中,面对不同SQL以及占位符参数,每次的路由结果是不同的。 这就意味着某些操作可能需要使用内存归并,而某些操作则可能选择流式归并更优,具体采用哪种方式不应该由用户在Shard ingSphere启动之前配置好,而是应该根据SQL和占位符参数的场景,来动态的决定连接模式。

自动化执行引擎将连接模式的选择粒度细化至每一次SQL的操作。 针对每次SQL请求,自动化执行引擎都将根据其路由结果,进行实时的演算和权衡,并自主地采用恰当的连接模式执行,以达到资源控制和效率的最优平衡。 针对自动化的执行引擎,用户只需配置maxConnectionSizePerQuery即可,该参数表示一次查询时每个数据库所允许使用的最大连接数。

执行引擎分为准备和执行两个阶段:

  • 准备阶段
  • 执行阶段

2.1 准备阶段

顾名思义,此阶段用于准备执行的数据。它分为结果集分组和执行单元创建两个步骤

2.1.1 结果集分组

结果集分组是实现内化连接模式概念的关键。执行引擎根据maxConnectionSizePerQuery配置项,结合当前路由结果,选择恰当的连接模式。 具体步骤如下:

1、 将SQL的路由结果按照数据源的名称进行分组;
2、 通过下图的公式,可以获得每个数据库实例在maxConnectionSizePerQuery的允许范围内,每个连接需要执行的SQL路由结果组,并计算出本次请求的最优连接模式;
 

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

例如路由和改写之后的sql为:

	SELECT * FROM t_order WHERE ds0.order_id_0 IN (1, 2, 3) ;
	SELECT * FROM t_order WHERE ds0.order_id_1 IN (1, 2, 3)  ;
	SELECT * FROM t_order WHERE ds0.order_id_2 IN (1, 2, 3)  ;
	SELECT * FROM t_order WHERE ds1.order_id_0 IN (1, 2, 3) ;
	SELECT * FROM t_order WHERE ds1.order_id_1 IN (1, 2, 3) ; 

将SQL的路由结果按照数据源的名称进行分组得到ds0 和 ds1 ,假如maxConnectionSizePerQuery设置为 2 ,那么 ds0 连接需要执行的请求数量为 3/2 >1,意味着ds0 的数据库连接将使用连接限制模式,只会创建唯一的数据库连接,复用数据库连接执行下一次的查询操作时,必须将当前得到的结果集加载到内存,则必须采用内存归并。反之 ds1 连接需要执行的请求数量为 2/2 = 1,采用内存限制模式,则可以采用流式归并

2.1.2 执行单元创建

通过上一步骤获得的路由分组结果创建执行的单元。 当数据源使用数据库连接池等控制数据库连接数量的技术时,在获取数据库连接时,如果不妥善处理并发,则有一定几率发生死锁。 在多个请求相互等待对方释放数据库连接资源时,将会产生饥饿等待,造成交叉的死锁问题。

举例说明,假设一次查询需要在某一数据源上获取两个数据库连接,并路由至同一个数据库的两个分表查询。 则有可能出现查询A已获取到该数据源的1个数据库连接,并等待获取另一个数据库连接;而查询B也已经在该数据源上获取到的一个数据库连接,并同样等待另一个数据库连接的获取。 如果数据库连接池的允许最大连接数是2,那么这2个查询请求将永久的等待下去。下图描绘了死锁的情况。

 

ShardingSphere为了避免死锁的出现,在获取数据库连接时进行了同步处理。 它在创建执行单元时,以原子性的方式一次性获取本次SQL请求所需的全部数据库连接,杜绝了每次查询请求获取到部分资源的可能。 由于对数据库的操作非常频繁,每次获取数据库连接时时都进行锁定,会降低ShardingSphere的并发。因此,ShardingSphere在这里进行了2点优化:

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

2.2 执行阶段

该阶段用于真正的执行SQL,它分为分组执行和归并结果集生成两个步骤。

分组执行将准备执行阶段生成的执行单元分组下发至底层并发执行引擎,并针对执行过程中的每个关键步骤发送事件。 如:执行开始事件、执行成功事件以及执行失败事件。执行引擎仅关注事件的发送,它并不关心事件的订阅者。 ShardingSphere的其他模块,如:分布式事务、调用链路追踪等,会订阅感兴趣的事件,并进行相应的处理。。

ShardingSphere通过在执行准备阶段的获取的连接模式,生成内存归并结果集或流式归并结果集,并将其传递至结果归并引擎,以进行下一步的工作。可以参考:Sharding-JDBC 核心之归并引擎

执行引擎的整体结构划分如下图所示。
 
**可以参考官方文档:
https://shardingsphere.apache.org/document/legacy/3.x/document/cn/features/sharding/principle/execute/