写点什么

如何高效地将 SQL 数据映射到 NoSQL 存储系统中

2015 年 1 月 07 日

通常来说,我们都知道:

  • SQL 数据库只限在单机上运行,但它提供了更强的事务管理、schema 与查询功能。
  • NoSQL 数据库为了伸缩性与容错性的目的,放弃了事务管理与 schema。

而 FoundationDB 的 SQL 层结合了这两个方面:它首先是一个开源的 SQL 数据库,能够线性地伸缩与提升容错性,并且还具有真正的 ACID 事务功能。曾经互不相容的两种特性,现在已融合在一个统一的系统中。

对于处于以下几种情况的公司来说,这一特性是非常重要的:

  • 新的项目要为大规模的伸缩性进行计划。
  • 现有的项目遇到了数据库伸缩性的瓶颈。
  • 现有的许多项目希望能用一个唯一的、容错性强的数据库抽象层统一工作模式。

在本文中,我将为读者介绍 FoundationDB ,并解释 FoundationDB 的 SQL 层是怎样将 SQL 数据映射到 FoundationDB 中的键 - 值存储后台系统中的。

NoSQL 数据库 ——FoundationDB 的键 - 值存储系统

FoundationDB 是一个分布式的键 - 值存储系统,支持全局 ACID 事务操作,并且性能出众。在安装系统时,可以指定数据分发的级别。数据分发为容错性提供了支持:当某个服务器或网络的某部分产生故障时,数据库仍然可以正常操作,你的应用也不会受到影响。

键- 值与SQL 架构

我们开发的这套架构能够在键- 值存储系统上支持多个层,每个层都能够在FoundationDB 的基础上提供一套不同的数据模型,例如SQL 数据库、文档数据库或图形数据库。许多使用者也自行创建了自定义的层。

下图中列出架构中的了关键部分。处于最底层的是FoundationDB 集群,无论集群的实际大小如何,对它的操作与一个单独的逻辑数据库并没有分别。SQL 层则以一种无状态的中间层方式运行在键- 值存储系统之上。这一层通过SQL 与应用程序进行通信,并使用FoundationDB 的客户端API 与键- 值存储系统进行通信。由于SQL 层是无状态的,因此可以并行地运行任意数据的SQL 层。

SQL 层为键 - 值存储系统带来了如 Google 的 F1 般的能力

SQL 层是对 SQL 与键 - 值存储 API 进行转换的一套逻辑严密的层。首先,SQL 层会从一条 SQL 语句开始,将其转换为最高效地键 - 值操作。这种方式类似于编译器将代码转换为低级别的执行格式。并且,这种转换是完全符合 ANSI SQL 92 标准的。开发者可以将该功能与 ORM、REST API 进行接合,或者直接使用 SQL 层的命令行界面进行调用。从代码的角度来说,SQL 层与键 - 值存储是完全分离的,它是通过 FoundationDB 的 Java 绑定方式与键 - 值存储进行通信的。感兴趣的读者可以查看 FoundationDB 的 SQL 层在 GitHub 上的代码库,其代码是完全开源的。眼下唯一能够和这套系统进行比较的是 Google 的 F1 ,后者是一套基于该公司的 Spanner 技术所创建的 SQL 引擎。

如以下的简单图例所示,SQL 层是由一系列组件所组成的。应用程序通过某种受支持的 SQL 客户端向 SQL 层发送查询语句,在解析之后转换为一棵计划节点树。优化器(Optimizer)会计算最佳的执行计划,并以一棵操作符树的方式表现出来,随后由执行框架(Execution Framework)运行。在执行阶段,对数据的请求将被发送到存储虚拟(Storage Abstraction)层,这一层通过使用 Java 的键 - 值 API 在数据与 FoundationDB 集群之间进行传输。数据库模型将存放在 Information Schema 层中,这一层将被其它多个组件所调用。

将 SQL 数据映射到键 - 值存储系统

SQL 层需要管理两种类型的数据,首先是信息 Schema 的元数据,它负责描述所创建的表与可用的索引。其次,它还需要存储实际的数据,包括表内容、索引及序列。我们首先来描述一下这些数据是如何保存在键 - 值存储系统中的。

本质上讲,每个键都是对应了某张表中的特定行的指针,而值则包含了该行的数据。键的分配是由 Table-Group 所决定的,它是包含了一个或多个表的组。稍后会对这个概念的细节进行更深入的讲解。SQL 层会通过使用键 - 值存储目录层为每个 Table-Group 创建一个目录,存储目录层是为用户管理键空间的一个工具,它为每个独立的目录分配一个简短的字节数组,作为该目录的唯一键。同时,它也维护着其它元数据,以实现通过名称进行查找的功能。

下面这个例子演示了如何创建目录的映射,通过以下语句分配键。

复制代码
CREATE TABLE schema_a.table1(id INT PRIMARY KEY, c CHAR(10));
CREATE TABLE schema_a.table2(id INT PRIMARY KEY);

在键 - 值存储系统中有一些预定义的目录:

Directory

Tuple

Raw Key

sql/

(9)

\x15\x09

sql/data/

(3)

\x15\x03

sql/data/table/

(31)

\x15\x1F

sql/data/table/schema_a/table1/

(215)

\x15\xD7

sql/data/table/schema_a/table2/

(247)

\x15\xF7

在存储数据时,可以选择使用以下三种格式中的一种:“元组(Tuple)”、“原始数据(Row_Data)”或者是“Protobuf”。如果使用默认的 Tuple 存储格式,那么每一行内容都将保存为一个单独的键 - 值对,键是通过连接以下字符串所生成的元组:目录前缀、该表在 Table-Group 中的位置,以及主键。而值的内容则是由该行中的所有列所组成的一个元组。

举例来说,以下代码对之前创建的表进行操作,产生对应的键与值。

复制代码
INSERT INTO schema_a.table1 VALUES (1, 'hello'), (2, 'world');
INSERT INTO schema_a.table2 VALUES (5);

Raw Key

Tuple Key

Raw Value

Tuple Value

\x15\xD7\x15\x01\x15\x01

(215, 1, 1)

\x15\x01\x02hello\x00

(1, ‘hello’)

\x15\xD7\x15\x01\x15\x02

(215, 1, 2)

\x15\x02\x02world\x00

(2, ‘world’)

\x15\xF7\x15\x01\x15\x05

(247, 1, 5)

\x15\x05

(5)

了解了键 - 值存储系统中键的结构之后,你就能够从存储系统中直接读取数据了。我们将使用 FoundationDB 的 Python API 来演示这一功能。在 SQL 层中,键与值是通过“.pack()”方法进行编码,并通过“.unpack()”方法进行解码的。下面的示例为你演示如何获取并解码数据。

复制代码
import fdb  fdb.api_version(200)
db = fdb.open()
directory = fdb.directory.open(db,('sql','data','table','schema_a','table1'))
for key, value in db[directory.range()]:         print fdb.tuple.unpack(key), ' --> ', fdb.tuple.unpack(value)

以上代码会输出类似下面的结果:

复制代码
(215, 1, 1) --> (1, u'hello')
(215, 1, 2) --> (2, u'world')

现在让我们再来近距离观察一下 Table-Group。每个独立的表都属于一个单独的组,如果某张额外的表能够创建一个对第一张表的“组外键”引用,那么它也能够加入到同一个组中。当我们为某张表创建组外键时,字表将与父表所在的目录进行交互。字表将成为 Table-Group 的一部分,在源表之后进行命名。这两张表的数据在将同一个目录中进行交互,这保证了范围扫描的高速,并且在 Table-Group 之内访问对象及表连接的开销极小。为了演示这一特性,我们将继续之前的示例,这一次的 SQL 语句如下:

复制代码
CREATE TABLE schema_a.table3(id INT PRIMARY KEY, id_1 INT, GROUPING FOREIGN KEY (id_1) REFERENCES schema_a.table1(id));
INSERT INTO schema_a.table3 VALUES (100, 2), (200, 2), (300, 1);

该语句将返回以下结果:

复制代码
directory = fdb.directory.open(db,('sql','data','table','schema_a','table1'))
for key, value in db[directory.range()]:     print fdb.tuple.unpack(key), ' --> ', fdb.tuple.unpack(value)
(215, 1, 1)          -->  (1, u'hello')
(215, 1, 1, 2, 300)  -->  (300, 1)
(215, 1, 2)          -->  (2, u'world')
(215, 1, 2, 2, 100)  -->  (100, 2)
(215, 1, 2, 2, 200)  -->  (200, 2)

由于第三张表的键都处于第一张表中各行的命名空间范围内,因此第三张表中所有插入的行都能够与第一张表的行相关联。键中的两个额外的值分别对应了 Table-Group 中的位置以及第三张表中的主键。对表 1 与表 3 通过引用键进行连接也无需通过标准的连接操作实现,直接通过线性扫描就语句了。这种排序方式比起传统的关系型数据库系统有着极大的优势。

由于键都已经经过排序,因此索引可以直接利用这一点所带来的便利性。所有的表索引只包含一个键值,其中包括两部分内容。每个索引都创建于该表所属的目录之下,一个名为 index 的子目录中,这是该键元组的第一部分内容。第二个部分是一个组合,首先是该索引所对应的各个列的值,之后则是指定这一行所必须的列的值。

举例来说,我们可以为这张表的 c 列创建一个索引。

复制代码
CREATE INDEX index_on_c ON schema_a.table1(c) STORAGE_FORMAT tuple;

接下来使用 Python 读取这个索引的内容,我们需要在 Python 解释器中加入以下内容:

复制代码
directory = fdb.directory.open(db, ('sql', 'data', 'table', 'schema_a', 'table1', 'index_on_c'))
for key, value in db[directory.range()]:     print fdb.tuple.unpack(key), ' --> ', fdb.tuple.unpack(value)

这段代码会输入类似于下图中的内容,显示了键的两个组成部分:即该索引所在的目录的字节值,以及创建索引的 c 列的值加上主键的值。最后一个部分将被索引的值链接到某个特定的行,而该索引键所对应的值为空。

复制代码
(20127, u'hello', 1) --> ()
(20127, u'world', 2) --> ()

如果要对 SQL 层的行为进行更多的控制调整,可以使用以下三种存储格式:一是之前描述过的元组格式,一是列键格式,以及 protobuf 格式。列健格式会为某一行的每个列值创建一个独立的键 - 值对。而 protobuf 存储格式为会每一行创建一个 protobuf 消息。

接下来还需要对元数据进行存储与组织。SQL 层使用 protobuf 消息与基于 SQL 的数据的结构进行通信。这个结构是由 schema、组、表、列、索引与外键等对象共同组成的。

SQL 与 NoSQL 的混合模式

如果在应用程序级别使用只读的键 - 值 API,那么 SQL 层就能够在客户端进行直接访问。可以通过键 - 值 API 直接访问数据,但如果增加或改写了 SQL 层所用的关键数据,那就很可能破坏系统的运行。这里例举一些可能会产生的问题:缺乏对索引的维护、缺乏应有的限定,以及忽略了对数据及元数据的版本维护。而这种方式的好处,哪怕是在进行数据读取时也并不明显,因为 SQL 层本身的额外开销就非常小。因此总的来说,性能的开销主要取决于网络延迟。

结论

SQL 与 NoSQL 的结合使用能够相互利用两者的优点。FoundationDB 的键 - 值存储系统为 SQL 层带来的好处包括可伸缩性、容错性及全局 ACID 的事务属性。你的应用程序同样也能从中受益,因此赶紧尝试一下吧!对应那些要执行大量的小批数据读取及写入的应用程序来说,FoundationDB 提供了一个高伸缩并且安全的解决方案,并且可以任意使用 SQL 或 NoSQL。

关于作者

Sytze Harkema从 2014 年 3 月起担任 FoundationDB 的软件工程师,他专注于 SQL 层的开发,致力于使其成为高伸缩 SQL 应用的最佳解决方案。Sytzey 曾经就读于荷兰的 Delft 科技大学及美国的哈佛大学。

查看英文原文: How to Effectively Map SQL Data to a NoSQL Store

2015 年 1 月 07 日 07:374762
用户头像

发布了 428 篇内容, 共 148.4 次阅读, 收获喜欢 20 次。

关注

评论

发布
暂无评论
发现更多内容

刷了LeetCode的链表专题,我发现了一个秘密!

Simon郎

Java 链表 面试数据结构与算法

TensorFlow 篇 | TensorFlow 数据输入格式之 TFRecord

Alex

tensorflow keras dataset tfrecord

推进AI融合 2020 LF AI & DATA DAY(AI开源日)即将召开

Geek_459987

央视呼吁电商双十一少一些套路:应该严打网店套路营销

石头IT视角

Linux-技术专题-Linux命令如何进行查看进程

李浩宇/Alex

Redis-缓存雪崩,缓存击穿,缓存穿透

topsion

redis

开源技术够用了么?我的 NAS 选型与搭建过程

LeanCloud

开源 NAS

架构师训练营 W03 作业

Geek_f06ede

架构师训练

程序人急速变富指南(一)

陆陆通通

程序员 职业 财富 认知 眼界

CloudQuery V1.2.0 版本发布

CloudQuery社区

数据库 sql 编辑器 工具软件

一场关于FLV是否要支持HEVC的争论

wangwei1237

技术文化

架构师训练营 W03 总结

Geek_f06ede

架构师训练

redis的stream类型命令详解

LLLibra146

redis stream 消息队列

vivo 云服务海量数据存储架构演进与实践

vivo互联网技术

数据库 架构 云服务 数据存储

第一届“多模态自然语言处理研讨会”精彩回顾(免费获取PPT)

京东智联云开发者

人工智能 自然语言处理

JDK8中的新时间API:Duration Period和ChronoUnit介绍

程序那些事

java8 jdk8 新特性 程序那些事 时间API

Linux高级编程常用的系统调用函数汇总

哒宰的自我修养

Linux 线程 网络编程 进程 MySQL数据库

面经手册 · 第16篇《码农会锁,ReentrantLock之公平锁讲解和实现》

小傅哥

Java 面试 小傅哥 ReentrantLock 公平锁

5G时代的到来对直播的影响

anyRTC开发者

5G 音视频 WebRTC 直播 RTC

深度解读智能推荐系统搭建之路 | 会展云技术揭秘

京东智联云开发者

人工智能 推荐系统

接口测试用例编写和测试关注点

测试人生路

接口测试 测试用例

23张图!万字详解「链表」,从小白到大佬!

王磊

Java 数据结构与算法

网易云音乐基于 Flink + Kafka 的实时数仓建设实践

Apache Flink

flink

英特尔独显终于来了!锐炬®Xe MAX为非凡S3x带来设计师级创作体验

intel001

腾讯内容首发:分布式核心原理解析笔记+分布式消息中间件实践笔记PDF版

Java架构追梦

Java 架构 面试 分布式 消息中间件

如何在面试中解释关键机器学习算法

计算机与AI

学习 数据科学

甲方日常 44

句子

工作 随笔杂谈 日常

区块链数字货币交易所开发方案,交易平台搭建app

WX13823153201

「排序算法」图解双轴快排

bigsai

排序算法 快速排序 双轴快排

环球易购数据平台如何做到既提速又省钱?

苏锐

大数据 hdfs S3 CDH 成本优化

给萌新HTML5 入门指南(二)

Geek_Willie

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

如何高效地将SQL数据映射到NoSQL存储系统中-InfoQ