mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 08:46:40 +08:00
1249 lines
38 KiB
HTML
1249 lines
38 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>24 归并引擎:如何理解数据归并的类型以及简单归并策略的实现过程?.md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
|
||
|
||
|
||
<body>
|
||
|
||
|
||
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/00 如何正确学习一款分库分表开源框架?.md.html">00 如何正确学习一款分库分表开源框架?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/01 从理论到实践:如何让分库分表真正落地?.md.html">01 从理论到实践:如何让分库分表真正落地?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/02 顶级项目:ShardingSphere 是一款什么样的 Apache 开源软件?.md.html">02 顶级项目:ShardingSphere 是一款什么样的 Apache 开源软件?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/03 规范兼容:JDBC 规范与 ShardingSphere 是什么关系?.md.html">03 规范兼容:JDBC 规范与 ShardingSphere 是什么关系?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/04 应用集成:在业务系统中使用 ShardingSphere 的方式有哪些?.md.html">04 应用集成:在业务系统中使用 ShardingSphere 的方式有哪些?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/05 配置驱动:ShardingSphere 中的配置体系是如何设计的?.md.html">05 配置驱动:ShardingSphere 中的配置体系是如何设计的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/06 数据分片:如何实现分库、分表、分库+分表以及强制路由?(上).md.html">06 数据分片:如何实现分库、分表、分库+分表以及强制路由?(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/07 数据分片:如何实现分库、分表、分库+分表以及强制路由?(下).md.html">07 数据分片:如何实现分库、分表、分库+分表以及强制路由?(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/08 读写分离:如何集成分库分表+数据库主从架构?.md.html">08 读写分离:如何集成分库分表+数据库主从架构?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/09 分布式事务:如何使用强一致性事务与柔性事务?.md.html">09 分布式事务:如何使用强一致性事务与柔性事务?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/10 数据脱敏:如何确保敏感数据的安全访问?.md.html">10 数据脱敏:如何确保敏感数据的安全访问?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/11 编排治理:如何实现分布式环境下的动态配置管理?.md.html">11 编排治理:如何实现分布式环境下的动态配置管理?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/12 从应用到原理:如何高效阅读 ShardingSphere 源码?.md.html">12 从应用到原理:如何高效阅读 ShardingSphere 源码?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/13 微内核架构:ShardingSphere 如何实现系统的扩展性?.md.html">13 微内核架构:ShardingSphere 如何实现系统的扩展性?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/14 分布式主键:ShardingSphere 中有哪些分布式主键实现方式?.md.html">14 分布式主键:ShardingSphere 中有哪些分布式主键实现方式?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/15 解析引擎:SQL 解析流程应该包括哪些核心阶段?(上).md.html">15 解析引擎:SQL 解析流程应该包括哪些核心阶段?(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/16 解析引擎:SQL 解析流程应该包括哪些核心阶段?(下).md.html">16 解析引擎:SQL 解析流程应该包括哪些核心阶段?(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/17 路由引擎:如何理解分片路由核心类 ShardingRouter 的运作机制?.md.html">17 路由引擎:如何理解分片路由核心类 ShardingRouter 的运作机制?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/18 路由引擎:如何实现数据访问的分片路由和广播路由?.md.html">18 路由引擎:如何实现数据访问的分片路由和广播路由?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/19 路由引擎:如何在路由过程中集成多种路由策略和路由算法?.md.html">19 路由引擎:如何在路由过程中集成多种路由策略和路由算法?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/20 改写引擎:如何理解装饰器模式下的 SQL 改写实现机制?.md.html">20 改写引擎:如何理解装饰器模式下的 SQL 改写实现机制?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/21 执行引擎:分片环境下 SQL 执行的整体流程应该如何进行抽象?.md.html">21 执行引擎:分片环境下 SQL 执行的整体流程应该如何进行抽象?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/22 执行引擎:如何把握 ShardingSphere 中的 Executor 执行模型?(上).md.html">22 执行引擎:如何把握 ShardingSphere 中的 Executor 执行模型?(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/23 执行引擎:如何把握 ShardingSphere 中的 Executor 执行模型?(下).md.html">23 执行引擎:如何把握 ShardingSphere 中的 Executor 执行模型?(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/ShardingSphere 核心原理精讲-完/24 归并引擎:如何理解数据归并的类型以及简单归并策略的实现过程?.md.html">24 归并引擎:如何理解数据归并的类型以及简单归并策略的实现过程?.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/25 归并引擎:如何理解流式归并和内存归并在复杂归并场景下的应用方式?.md.html">25 归并引擎:如何理解流式归并和内存归并在复杂归并场景下的应用方式?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/26 读写分离:普通主从架构和分片主从架构分别是如何实现的?.md.html">26 读写分离:普通主从架构和分片主从架构分别是如何实现的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/27 分布式事务:如何理解 ShardingSphere 中对分布式事务的抽象过程?.md.html">27 分布式事务:如何理解 ShardingSphere 中对分布式事务的抽象过程?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/28 分布式事务:ShardingSphere 中如何集成强一致性事务和柔性事务支持?(上).md.html">28 分布式事务:ShardingSphere 中如何集成强一致性事务和柔性事务支持?(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/29 分布式事务:ShardingSphere 中如何集成强一致性事务和柔性事务支持?(下).md.html">29 分布式事务:ShardingSphere 中如何集成强一致性事务和柔性事务支持?(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/30 数据脱敏:如何基于改写引擎实现低侵入性数据脱敏方案?.md.html">30 数据脱敏:如何基于改写引擎实现低侵入性数据脱敏方案?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/31 配置中心:如何基于配置中心实现配置信息的动态化管理?.md.html">31 配置中心:如何基于配置中心实现配置信息的动态化管理?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/32 注册中心:如何基于注册中心实现数据库访问熔断机制?.md.html">32 注册中心:如何基于注册中心实现数据库访问熔断机制?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/33 链路跟踪:如何基于 Hook 机制以及 OpenTracing 协议实现数据访问链路跟踪?.md.html">33 链路跟踪:如何基于 Hook 机制以及 OpenTracing 协议实现数据访问链路跟踪?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/34 系统集成:如何完成 ShardingSphere 内核与 Spring+SpringBoot 的无缝整合?.md.html">34 系统集成:如何完成 ShardingSphere 内核与 Spring+SpringBoot 的无缝整合?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/35 结语:ShardingSphere 总结及展望.md.html">35 结语:ShardingSphere 总结及展望.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>24 归并引擎:如何理解数据归并的类型以及简单归并策略的实现过程?</h1>
|
||
|
||
<p>在上一课时,我们提到在 ShardingStatement 和 ShardingPreparedStatement 中,执行 executeQuery 或 executeUpdate 方法时会使用到归并引擎 MergeEngine:</p>
|
||
|
||
<pre><code>//调用归并引擎
|
||
|
||
MergeEngine mergeEngine = MergeEngineFactory.newInstance(connection.getRuntimeContext().getDatabaseType(), connection.getRuntimeContext().getRule(), sqlRouteResult, connection.getRuntimeContext().getMetaData().getRelationMetas(), statementExecutor.executeQuery());
|
||
|
||
//获取归并结果
|
||
|
||
result = getResultSet(mergeEngine);
|
||
|
||
</code></pre>
|
||
|
||
<p>在 ShardingSphere 整个分片机制的结构中,归并引擎是执行引擎后的下一环,也是整个数据分片引擎的最后一环。</p>
|
||
|
||
<p>在今天以及下一课时中,我将带领大家对 ShardingSphere 中的归并引擎做详细的展开,让我们先从归并这一基本概念说起。</p>
|
||
|
||
<h3>归并与归并引擎</h3>
|
||
|
||
<p>我们知道,在分库分表环境下,一句逻辑 SQL 会最终解析成多条真正的 SQL,并被路由到不同的数据库中进行执行,每个数据库都可能返回最终结果中的一部分数据。</p>
|
||
|
||
<p>这样我们就会碰到一个问题,即如何把这些来自不同数据库的部分数据组合成最终结果呢?这就需要引入归并的概念。</p>
|
||
|
||
<h4>1.归并的分类及其实现方案</h4>
|
||
|
||
<p>所谓<strong>归并</strong>,就是将从各个数据节点获取的多数据结果集,通过一定的策略组合成为一个结果集并正确的返回给请求客户端的过程。</p>
|
||
|
||
<p><strong>按照不同的 SQL 类型以及应用场景划分</strong>,归并的类型可以分为遍历、排序、分组、分页和聚合 5 种类型,这 5 种类型是组合而非互斥的关系。</p>
|
||
|
||
<p>其中遍历归并是最简单的归并,而排序归并是最常用地归并,在下文我会对两者分别详细介绍。</p>
|
||
|
||
<p><img src="assets/CgqCHl9QzC6AA5U7AABkojINfPw834.png" alt="Lark20200903-185718.png" /></p>
|
||
|
||
<p>归并的五大类型</p>
|
||
|
||
<p><strong>按照归并实现的结构划分</strong>,ShardingSphere 中又存在流式归并、内存归并和装饰者归并这三种归并方案。</p>
|
||
|
||
<ul>
|
||
|
||
<li>所谓的<strong>流式归并</strong>,类似于 JDBC 中从 ResultSet 获取结果的处理方式,也就是说通过逐条获取的方式返回正确的单条数据;</li>
|
||
|
||
<li><strong>内存归并</strong>的思路则不同,是将结果集的所有数据先存储在内存中,通过统一的计算之后,再将其封装成为逐条访问的数据结果集进行返回。</li>
|
||
|
||
<li>最后的<strong>装饰者归并</strong>是指,通过装饰器模式对所有的结果集进行归并,并进行统一的功能增强,类似于改写引擎中 SQLRewriteContextDecorator 对 SQLRewriteContext 进行装饰的过程。</li>
|
||
|
||
</ul>
|
||
|
||
<p>显然,流式归并和内存归并是互斥的,装饰者归并可以在流式归并和内存归并之上做进一步的处理。</p>
|
||
|
||
<p><strong>归并方案</strong>与归并类型之间同样存在一定的关联关系,其中遍历、排序以及流式分组都属于流式归并的一种,内存归并可以作用于统一的分组、排序以及聚合,而装饰者归并有分页归并和聚合归并这 2 种类型,它们之间的对应关系如下图所示:</p>
|
||
|
||
<p><img src="assets/CgqCHl9QzD6AG8aFAACQddjkR3E088.png" alt="Lark20200903-185710.png" /></p>
|
||
|
||
<p>归并类型与归并方案之间的对应关系图</p>
|
||
|
||
<h4>2.归并引擎</h4>
|
||
|
||
<p>讲完概念回到代码,我们首先来到 shardingsphere-merge 代码工程中的 MergeEngine 接口:</p>
|
||
|
||
<pre><code>public interface MergeEngine {
|
||
|
||
//执行归并
|
||
|
||
MergedResult merge() throws SQLException;
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>可以看到 MergeEngine 接口非常简单,只有一个 merge 方法。在 ShardingSphere 中,该接口存在五个实现类,其类层结构如下所示:</p>
|
||
|
||
<p><img src="assets/CgqCHl9Qli6AUetjAAAuTLxL5xw054.png" alt="Drawing 2.png" /></p>
|
||
|
||
<p>MergeEngine 类层结构图</p>
|
||
|
||
<p>从命名上看,可以看到名称中带有“Encrypt”的两个 MergeEngine 与数据脱敏相关,放在后续专题中再做讲解,其余的三个我们会先做一些分析。</p>
|
||
|
||
<p>在此之前,我们还要来关注一下代表归并结果的 MergedResult 接口:</p>
|
||
|
||
<pre><code>public interface MergedResult {
|
||
|
||
boolean next() throws SQLException;
|
||
|
||
Object getValue(int columnIndex, Class<?> type) throws SQLException;
|
||
|
||
Object getCalendarValue(int columnIndex, Class<?> type, Calendar calendar) throws SQLException;
|
||
|
||
InputStream getInputStream(int columnIndex, String type) throws SQLException;
|
||
|
||
boolean wasNull() throws SQLException;
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>可以看到 MergedResult 与执行引擎中的 QueryResult 非常相似,只是少了几个方法。理解了归并引擎的定义以及归并结果的表现形式之后,我们来分析创建 MergeEngine 的过程,前面已经看到这实际上是依赖于工厂类 MergeEngineFactory,其实现过程如下所示:</p>
|
||
|
||
<pre><code>public static MergeEngine newInstance(final DatabaseType databaseType, final ShardingRule shardingRule,
|
||
|
||
final SQLRouteResult routeResult, final RelationMetas relationMetas, final List<QueryResult> queryResults) {
|
||
|
||
|
||
|
||
//如果是查询语句,就创建一个 DQLMergeEngine
|
||
|
||
if (routeResult.getSqlStatementContext() instanceof SelectSQLStatementContext) {
|
||
|
||
return new DQLMergeEngine(databaseType, (SelectSQLStatementContext) routeResult.getSqlStatementContext(), queryResults);
|
||
|
||
}
|
||
|
||
//如果是数据库管理语句,就创建一个 DALMergeEngine
|
||
|
||
if (routeResult.getSqlStatementContext().getSqlStatement() instanceof DALStatement) {
|
||
|
||
return new DALMergeEngine(shardingRule, queryResults, routeResult.getSqlStatementContext(), relationMetas);
|
||
|
||
}
|
||
|
||
return new TransparentMergeEngine(queryResults);
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这个 newInstance 方法的参数值得关注一下,这些参数我们都很眼熟,包括数据库类型 DatabaseType、分片规则 ShardingRule、路由结果 SQLRouteResult、执行结果列表 List 等。</p>
|
||
|
||
<p>然后,我们看到代码逻辑会根据 SQLRouteResult 中 SqlStatementContext 的不同类型返回不同类型的 MergeEngine,即如果是 SelectSQLStatementContext 则返回用于查询的 DQLMergeEngine;而如果 SQLStatement 是一种执行数据库管理语句的 DALStatement,则返回 DALMergeEngine;如果都不是,则直接返回 TransparentMergeEngine。</p>
|
||
|
||
<p>对于归并而言,显然 DQLMergeEngine 是最重要的一种引擎类型,我们重点对它进行展开,</p>
|
||
|
||
<p>它的 merge 方法如下所示:</p>
|
||
|
||
<pre><code>public MergedResult merge() throws SQLException {
|
||
|
||
//如果结果集数量为 1
|
||
|
||
if (1 == queryResults.size()) {
|
||
|
||
return new IteratorStreamMergedResult(queryResults);
|
||
|
||
}
|
||
|
||
Map<String, Integer> columnLabelIndexMap = getColumnLabelIndexMap(queryResults.get(0));
|
||
|
||
selectSQLStatementContext.setIndexes(columnLabelIndexMap);
|
||
|
||
//如果结果集数量大于 1,则构建不同的归并方案
|
||
|
||
return decorate(build(columnLabelIndexMap));
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这里先出现了一个判断,即当查询结果集数量为 1 时,我们只需调用遍历结果集进行归并即可,这种类型就属于遍历归并。遍历归并是我们将要介绍的第一种归并类型,也是所有归并类型中最为简单的一种。</p>
|
||
|
||
<p>如果结果集不是只有一个,那就意味了需要进行合并,我们会通过如下所示的 build 方法根据不同的条件构建不同的 MergedResult 并返回:</p>
|
||
|
||
<pre><code>private MergedResult build(final Map<String, Integer> columnLabelIndexMap) throws SQLException {
|
||
|
||
//查询语句中分组语句或者聚合函数不为空,则执行分组归并
|
||
|
||
if (isNeedProcessGroupBy()) {
|
||
|
||
return getGroupByMergedResult(columnLabelIndexMap);
|
||
|
||
}
|
||
|
||
//如果聚合中存在 Distinct 列,设置分组 Context 并执行分组归并
|
||
|
||
if (isNeedProcessDistinctRow()) {
|
||
|
||
setGroupByForDistinctRow();
|
||
|
||
return getGroupByMergedResult(columnLabelIndexMap);
|
||
|
||
}
|
||
|
||
//排序语句不为空,则执行排序结果集归并
|
||
|
||
if (isNeedProcessOrderBy()) {
|
||
|
||
return new OrderByStreamMergedResult(queryResults, selectSQLStatementContext.getOrderByContext().getItems());
|
||
|
||
}
|
||
|
||
//如果都不满足归并提交,则执行遍历结果集归并
|
||
|
||
return new IteratorStreamMergedResult(queryResults);
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>可以看到,这里涉及了分组归并和排序归并这两大类归并策略。然后,我们还看到有一个构建在上述 build 方法之上的 decorate 方法。这个 decorate 方法体现的就是一种装饰者归并,用于针对不同的数据库方言完成分页归并操作,我们会在下一课时中对这个方法做详细展开。</p>
|
||
|
||
<p>这样,我们把 ShardingSphere 中的各种归并类型通过归并引擎 MergeEngine 串联了起来,接下来的时间就来讨论各种归并类型的具体实现机制。</p>
|
||
|
||
<p>让我们先来看遍历归并。</p>
|
||
|
||
<h3>最简单的归并:遍历归并</h3>
|
||
|
||
<p><strong>遍历归并</strong>是最为简单的归并方式,我们只需将多个数据结果集合并为一个单向链表就可以了。遍历数据的操作,就相当于是在遍历一个单向列表。而在实现上,这个遍历结果集的表现形式就是一个 IteratorStreamMergedResult 类,该类又继承自 StreamMergedResult,代表的是一种流式合并结果。</p>
|
||
|
||
<p>IteratorStreamMergedResult 的 next 方法如下所示:</p>
|
||
|
||
<pre><code>@Override
|
||
|
||
public boolean next() throws SQLException {
|
||
|
||
if (getCurrentQueryResult().next()) {
|
||
|
||
return true;
|
||
|
||
}
|
||
|
||
if (!queryResults.hasNext()) {
|
||
|
||
return false;
|
||
|
||
}
|
||
|
||
//流式获取结果并设置为当前的 QueryResult
|
||
|
||
setCurrentQueryResult(queryResults.next());
|
||
|
||
boolean hasNext = getCurrentQueryResult().next();
|
||
|
||
if (hasNext) {
|
||
|
||
return true;
|
||
|
||
}
|
||
|
||
while (!hasNext && queryResults.hasNext()) {
|
||
|
||
setCurrentQueryResult(queryResults.next());
|
||
|
||
hasNext = getCurrentQueryResult().next();
|
||
|
||
}
|
||
|
||
return hasNext;
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>它的 getValue 方法在父类 StreamMergedResult,如下所示:</p>
|
||
|
||
<pre><code>@Override
|
||
|
||
public Object getValue(final int columnIndex, final Class<?> type) throws SQLException {
|
||
|
||
Object result = getCurrentQueryResult().getValue(columnIndex, type);
|
||
|
||
wasNull = getCurrentQueryResult().wasNull();
|
||
|
||
return result;
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这里同样也是通过 getCurrentQueryResult 方法流式获取当前的数据项,进而获取具体的值。</p>
|
||
|
||
<h3>最常用的归并:排序归并</h3>
|
||
|
||
<p>我们将要介绍的第二个归并类型是排序归并,它的返回结果是一个 OrderByStreamMergedResult,该类同样继承了用于流式归并的 StreamMergedResult 类。</p>
|
||
|
||
<p>在介绍 OrderByStreamMergedResult 前,我们可以先想象一下排序归并的场景。</p>
|
||
|
||
<p>当在多个数据库中执行某一条 SQL 语句时,我们可以做到在每个库的内部完成排序功能。也就是说,我们的执行结果中保存着内部排好序的多个 QueryResult,然后要做的就是把它们放在一个地方然后进行全局的排序。因为每个 QueryResult 内容已经是有序的,因此只需要将 QueryResult 中当前游标指向的数据值进行排序即可,相当于对多个有序的数组进行排序。</p>
|
||
|
||
<p>这个过程有点抽象,我们通过如下的示意图进行进一步说明。假设,在我们的健康任务 health_task 表中,存在一个健康点数字段 health_point,用于表示完成这个健康任务能够获取的健康分数。</p>
|
||
|
||
<p>然后,我们需要根据这个 health_point 进行排序归并,初始的数据效果如下图所示:</p>
|
||
|
||
<p><img src="assets/Ciqc1F9QzRSADVUyAABkYnJfMvs829.png" alt="Lark20200903-190058.png" /></p>
|
||
|
||
<p>三张 health_task 表中的初始数据</p>
|
||
|
||
<p>上图中展示了 3 张表返回的数据结果集,每个数据结果集都已经根据 health_point 字段进行了排序,但是 3 个数据结果集之间是无序的。排序归并的做法就是将 3 个数据结果集的当前游标指向的数据值进行排序,并放入到一个排序好的队列中。</p>
|
||
|
||
<p>在上图中可以看到 health_task0 的第一个 health_point 最小,health_task1 的第一个 health_point 最大,health_task2 的第一个 health_point 次之,因此队列中应该按照 health_task1,health_task2 和 health_task0 的方式排序队列,效果如下:</p>
|
||
|
||
<p><img src="assets/CgqCHl9QzHiAf33WAABsaH9vLR0050.png" alt="Lark20200903-185846.png" />
|
||
|
||
队列中已排序的三张 health_task 表</p>
|
||
|
||
<p>在 OrderByStreamMergedResult 中,我们可以看到如下所示的队列定义,用到了 JDK 中的 Queue 接口:</p>
|
||
|
||
<pre><code>private final Queue<OrderByValue> orderByValuesQueue;
|
||
|
||
</code></pre>
|
||
|
||
<p>而在 OrderByStreamMergedResult 的构造函数中,我们进一步看到 orderByValuesQueue 实际上是一个 PriorityQueue:</p>
|
||
|
||
<pre><code>public OrderByStreamMergedResult(final List<QueryResult> queryResults, final Collection<OrderByItem> orderByItems) throws SQLException {
|
||
|
||
this.orderByItems = orderByItems;
|
||
|
||
//构建 PriorityQueue
|
||
|
||
this.orderByValuesQueue = new PriorityQueue<>(queryResults.size());
|
||
|
||
//初始化 PriorityQueue
|
||
|
||
orderResultSetsToQueue(queryResults);
|
||
|
||
isFirstNext = true;
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>讲到这里,有必要对 JDK 中的 PriorityQueue 做一下简单介绍。对于 PriorityQueue 而言,它的特性是可以对其中所包含的元素进行自动排序,既可以存放基本数据类型的包装类,也可以支持自定义类。对于基本数据类型的包装器类,优先级队列中元素默认排列顺序是升序排列,而对于自己定义的类来说,需要定义定制化的比较器。</p>
|
||
|
||
<p>PriorityQueue 的常用方法如下所示:</p>
|
||
|
||
<ul>
|
||
|
||
<li>peek():返回队首元素</li>
|
||
|
||
<li>poll():返回队首元素,并且将队首元素弹出队列</li>
|
||
|
||
<li>offer():添加元素</li>
|
||
|
||
<li>size():返回队列元素个数</li>
|
||
|
||
<li>isEmpty():判断队列是否为空</li>
|
||
|
||
</ul>
|
||
|
||
<p>了解了 PriorityQueue 的功能特性之后,我们来看一下如何基于一个 QueryResult 列表对队列进行初始化,orderResultSetsToQueue 方法如下所示:</p>
|
||
|
||
<pre><code>private void orderResultSetsToQueue(final List<QueryResult> queryResults) throws SQLException {
|
||
|
||
for (QueryResult each : queryResults) {
|
||
|
||
//构建 OrderByValue
|
||
|
||
OrderByValue orderByValue = new OrderByValue(each, orderByItems);
|
||
|
||
if (orderByValue.next()) {
|
||
|
||
//添加 OrderByValue 到队列中
|
||
|
||
orderByValuesQueue.offer(orderByValue);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
setCurrentQueryResult(orderByValuesQueue.isEmpty() ? queryResults.get(0) : orderByValuesQueue.peek().getQueryResult());
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这里基于 QueryResult 构建了 OrderByValue 对象,并通过该对象的 next 方法判断是否需要将其添加到 PriorityQueue 中。</p>
|
||
|
||
<p>我们看到这里调用了 PriorityQueue 的 offer 方法将特定元素插入到优先级队列中。</p>
|
||
|
||
<p>当将所有的 OrderByValue 添加到 PriorityQueue 之后,OrderByStreamMergedResult 通过父类 StreamMergedResult 的 setCurrentQueryResult 方法将 PriorityQueue 中的第一个元素作为当前的查询结果,这时候 PriorityQueue 指向的就是全局排序好的第一个元素,也就是上图中的 50。</p>
|
||
|
||
<p>显然,对于 PriorityQueue 而言,这里新创建的 OrderByValue 就是自定义类,所以需要实现自定义的比较器。我们在 OrderByValue 类中看到它实现了 Java 的 Comparable 接口,compareTo 方法实现如下,针对每个排序项 OrderByItem 进行值的比对:</p>
|
||
|
||
<pre><code>@Override
|
||
|
||
public int compareTo(final OrderByValue o) {
|
||
|
||
int i = 0;
|
||
|
||
for (OrderByItem each : orderByItems) {
|
||
|
||
int result = CompareUtil.compareTo(orderValues.get(i), o.orderValues.get(i), each.getSegment().getOrderDirection(),
|
||
|
||
each.getSegment().getNullOrderDirection(), orderValuesCaseSensitive.get(i));
|
||
|
||
if (0 != result) {
|
||
|
||
return result;
|
||
|
||
}
|
||
|
||
i++;
|
||
|
||
}
|
||
|
||
return 0;
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>根据前面示意图中的结果,当使用 PriorityQueue 每次获取下一条数据时,我们只需将队列顶端结果集的游标下移,并根据新游标重新进入优先级队列并找到自己的位置即可。</p>
|
||
|
||
<p>这个步骤体现在如下所示的 next 方法中:</p>
|
||
|
||
<pre><code>@Override
|
||
|
||
public boolean next() throws SQLException {
|
||
|
||
if (orderByValuesQueue.isEmpty()) {
|
||
|
||
return false;
|
||
|
||
}
|
||
|
||
if (isFirstNext) {
|
||
|
||
isFirstNext = false;
|
||
|
||
return true;
|
||
|
||
}
|
||
|
||
|
||
|
||
//获取 PriorityQueue 中的第一个元素,并弹出该元素
|
||
|
||
OrderByValue firstOrderByValue = orderByValuesQueue.poll();
|
||
|
||
|
||
|
||
//将游标指向 firstOrderByValue 的下一个元素,并重新插入到 PriorityQueue 中,这会促使 PriorityQueue 进行自动的重排序
|
||
|
||
if (firstOrderByValue.next()) {
|
||
|
||
orderByValuesQueue.offer(firstOrderByValue);
|
||
|
||
}
|
||
|
||
if (orderByValuesQueue.isEmpty()) {
|
||
|
||
return false;
|
||
|
||
}
|
||
|
||
|
||
|
||
//将当前结果集指向 PriorityQueue 的第一个元素
|
||
|
||
setCurrentQueryResult(orderByValuesQueue.peek().getQueryResult());
|
||
|
||
return true;
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这个过程同样需要用一系列图来进行解释。当进行第一次 next 调用时,排在队列首位的 health_task1 将会被弹出队列,并且将当前游标指向的数据值 50 返回。同时,我们还会将游标下移一位之后,重新把 health_task1 放入优先级队列。而优先级队列也会根据 health_task1 的当前数据结果集指向游标的数据值 45 进行排序,根据当前数值,health_task1 将会被排列在队列的第三位。如下所示:</p>
|
||
|
||
<p><img src="assets/Ciqc1F9QzJaANhr0AABsaCCFqA0376.png" alt="Lark20200903-185915.png" /></p>
|
||
|
||
<p>第一次 next 之后的优先级队列中的三张 health_task 表</p>
|
||
|
||
<p>之前队列中排名第二的 health_task2 的数据结果集则自动排在了队列首位。而在进行第二次 next 时,只需要将目前排列在队列首位的 health_task2 弹出队列,并且将其数据结果集游标指向的值返回。当然,对于 health_task2 而言,我们同样下移游标,并继续将它加入优先级队列中,以此类推。</p>
|
||
|
||
<p><img src="assets/CgqCHl9QzJ2AMQzxAABrQS5M0oA899.png" alt="Lark20200903-185920.png" /></p>
|
||
|
||
<p>第二次 next 之后的优先级队列中的三张 health_task 表</p>
|
||
|
||
<p>可以看到,基于上述的设计和实现方法,对于每个数据结果集内部数据有序、而多数据结果集整体无序的情况下,我们无需将所有的数据都加载至内存即可进行排序。</p>
|
||
|
||
<p>因此,ShardingSphere 在这里使用的是流式归并的方式,充分提高了归并效率。</p>
|
||
|
||
<h3>从源码解析到日常开发</h3>
|
||
|
||
<p>队列是我们常用的一种数据结构,而对于需要进行数据比对和排序的场景下,今天介绍的优先级队列非常有用。基于自身所具备的排序特性,处理类似 ShardingSphere 中全局性的排序场景,优先级队列的实现方案优雅而高效,可以根据需要应用到在日常开发过程中。</p>
|
||
|
||
<h3>小结与预告</h3>
|
||
|
||
<p>今天的内容关注于 ShardingSphere 中的归并引擎,归并是分库分表环境下处理 SQL 执行结果的最终环节。我们抽象了 ShardingSphere 的几种常见的归并类型以及实现方案。同时,给出了其中最简单的遍历归并和最常用的排序归并的设计思想和实现细节。</p>
|
||
|
||
<p>这里给你留一道思考题:ShardingSphere 中,分片数据基于 JDK 中的哪种数据结构完成排序的?</p>
|
||
|
||
<p>在下一课时中,我们将继续介绍 ShardingSphere 归并引擎中剩余的几种归并类型,包括分组归并、聚合归并以及分页归并。</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/23 执行引擎:如何把握 ShardingSphere 中的 Executor 执行模型?(下).md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/ShardingSphere 核心原理精讲-完/25 归并引擎:如何理解流式归并和内存归并在复杂归并场景下的应用方式?.md.html">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709974d4ab6b3d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
|
||
|
||
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
|
||
|
||
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
|
||
|
||
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
|
||
|
||
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</html>
|
||
|