简介
过去几年来,出现了一种称为 NoSQL 的新型数据库管理系统。设计这些数据存储是为了克服在扩展传统关系数据库来处理一些应用程序时必须处理的数据负载类型的难题,比如说 Amazon。这种可伸缩性的实现需要一定的代价:NoSQL 系统通常不符合 ACID(原子性、一致性、隔离和耐久性);它们最终一致地表明,只要给定一定量的时间,所有数据更新最终都会通过该系统传播。这不符合某些类型的应用程序的要求。
过去用于在线事务处理 (OLTP) 的关系数据库管理系统确实提供了一致性保证(它们符合 ACID),但其扩展难度更大,且成本更高。研究还表明,它们的效率也不是特别高:CPU 花费了大约 10% 的时间来检索和更新记录,剩余 90% 用于处理缓冲区管理、锁定、闩锁 (Latch) 和日志记录等任务。
传统的关系数据库(比如 MySQL)和大多数 NoSQL 系统将其数据存储在磁盘上。VoltDB 将所有内容存储在主要内存中。如果可以避免进入磁盘,就可以获得显著的性能提升,访问内存比访问磁盘快一个数量级。如今的 RAM 成本比过去廉价得多,再结合 64 位计算的出现,您可配备一个具有数百 GB 主要内存的、标准的、即买即用的服务器。
VoltDB 数据库由大量分散在多个站点(服务器)上的分区组成。一个站点上运行的每个分区是单线程的,这消除了与典型多线程环境中的锁定和闩锁有关的开销,事务请求会按顺序执行。
NoSQL 数据库(顾名思义)没有使用 SQL 作为查询语言。例如,MongoDB 查询使用 JSON 表达。Riak 和 CouchDB 都支持使用 map/reduce 功能进行查询。VoltDB 使用 SQL 作为其查询语言,大多数使用过数据库的开发人员都熟悉 SQL,从这个意义上讲,这是一种优势。但一些 NoSQL 数据库所提供的查询接口并非如此。
对 VoltDB 中存储的数据的访问是通过使用 Java 语言编写的存储过程来完成的;SQL 语句嵌入在一个存储过程中。相对于 JDBC 等协议,从存储过程内执行 SQL 查询的一个优势是:每个事务只需在客户端与服务器之间往返一次。这消除了与通过网络在应用程序与数据库之间进行多次调用相关联的延迟。
VoltDB 有两个版本:一个开源社区版本和一个付费企业版本。本文将重点介绍社区版本。一些特性只有企业版中才有,这里不会介绍这些特性。
入门
要尝试使用本文中的一些示例,您需要下载并安装 VoltDB。本文中使用的版本是 2.5 版的社区版本。
VoltDB 需要一个基于 64 位 Linux 的操作系统;此要求也适用于 Mac OSX 10.6。您还需要安装 Java 开发工具包 (JDK 6)。可以使用 Eclipse 来编辑源代码。请参阅 参考资料,获取下载页面的链接和完整的系统需求列表。
Amazon EC2 和 VMware 映像也可供下载,下载它们之后就可以立即正常使用它们。
VoltDB 以 tar 压缩文件的形式分发,所以在下载它之后,可以使用以下命令解压:$ tar -zxvf voltdb-2.5.tar.gz -C ~/
。
在此实例中,我选择将它安装在我的主目录中,这非常适用于开发用途,您还可以将它解压到您选择的目录中。
解压之后,将 bin 目录添加到您的路径中:$ export PATH=$PATH:~/voltdb-2.5/bin
。
bin 目录包含一些命令,这些命令在您部署示例应用程序时会很有用。
接下来,下载本文配套的源代码。将它解压到您选择的目录中。示例应用程序主要处理一家虚构公司 Acme Inc 的员工。
一个典型的 VoltDB 应用程序由以下文件组成:
一个项目定义文件 (project.xml),其中包含哪些存储过程可用、数据库模式文件的位置、分区信息等信息。
一个部署文件 (deployment.xml),其中包含每个主机的站点数等信息。
数据库模式 (ddl.sql)。
源代码,例如:存储过程和客户端。
本文将更详细地介绍每个文件。
要将项目导入 Eclipse 中,请打开 Eclipse,然后执行以下操作:
选择 File>New>Project。
选择 Java Project from Existing Ant Buildfile,然后单击 Next。
勾选复选框 Link to the build file in the file system。
从您刚安装示例应用程序的目录选择 build.xml 作为 Ant 生成文件,然后选择 Finish。
如果希望创建您自己的应用程序,VoltDB 提供了一个工具来为您生成框架项目;该项目用于生成本文中附带的应用程序的文件夹结构。
清单 1 展示了如何调用它。
清单 1. 生成一个框架项目
$ cd $HOME/voltdb-2.5/tools
$ ./generate app acme $HOME/Projects/app
该工具按以下顺序接受多个参数:
应用程序的名称
(Java 代码的)包名称
创建项目的位置
运行 清单 1 中的命令并查看新创建的文件夹。您将看到该工具生成了一个框架项目,其中包含构建一个 VoltDB 应用程序所需的文件。
存储过程
简介中已经提到,数据访问是使用 Java 代码编写的存储过程来实现的。类似于传统的 RDBMS,您仍然必须编写 SQL 查询,以便从适当的表中获取所需的数据。刚才已从存储过程内完成此任务。对存储过程的每次调用都是一个事务;如果调用成功,则会提交存储过程,否则它们会回滚。
由于事务的顺序性质,在创建存储过程时一定要记住,应尽可能快地执行它们;否则它们将阻止其他等待运行的事务。例如,在存储过程中避免发送电子邮件或对数据执行复杂分析等任务。
清单 2 提供了一个存储过程示例,它将一个条目插入 employee 表中。
清单 2. 添加一位员工 (AddEmployee.java)
@ProcInfo (
partitionInfo = "EMPLOYEE.EMAIL: 0",
singlePartition = true
)
public class AddEmployee extends VoltProcedure {
public final SQLStmt addemployee = new SQLStmt(
"INSERT INTO EMPLOYEE VALUES (?, ?, ?, ?);"
);
public VoltTable[] run(String email, String firstname,
String lastname, int department)
throws VoltAbortException {
voltQueueSQL(addemployee, email, firstname,
lastname, department);
voltExecuteSQL(true);
return null;
}
}
一个存储过程必须扩展类 VoltProcedure
并实现 run
方法,该本例中,这会使用传递给该方法的参数将一个条目插入 employee 表中。选择、更新和删除也遵循类似的模式。
调用一个存储过程之前,应用程序需要创建与数据库的连接。在建立连接时,需要指定运行数据库的主机名称;如果运行一个集群,则可以指定该集群中的任何节点。清单 3 展示了如何创建与数据库的连接。
清单 3. 连接到数据库
// Create a client and connect to the database
org.voltdb.client Client client;
client = ClientFactory.createClient();
client.createConnection("localhost");
在建立与数据库的连接后,就可以查询数据库。清单 4 中的代码展示了如何调用 AddEmployee
存储过程,该存储过程向 employee 表添加了一些条目。
清单 4. 插入员工 (Client.java)
client.callProcedure("AddEmployee", "wile@acme.com",
"Wile", "Coyote", 1);
client.callProcedure("AddEmployee", "larry@acme.com",
"Larry", "Merchant", 2);
请注意,要调用的过程名称 (AddEmployee
) 与实现存储过程的 Java 类的名称是匹配的。
从 AddEmployee
存储过程中可以看到,这里使用了 SQL 来查询数据库。VoltDB 仅支持 SQL 的一个子集。如果希望将现有应用程序迁移到 VoltDB,那么有可能需要重写一些 SQL 查询。请参阅 参考资料,获取描述 VoltDB 支持的 SQL 子集的页面链接。
存储过程中的 SQL 语句必须提前声明,但可以在查询中使用绑定变量。可在运行时对数据库运行临时查询,例如一个具有动态字段的 SQL 查询,只需调用 @AdHoc 系统过程(参见 清单 5)。我们不建议这样做,因为查询没有优化,并且会以多分区事务的形式执行,这可能会影响性能。
清单 5. 在运行时执行一个 SQL 语句
String tableName = "EMPLOYEE";
VoltTable[] count = client.callProcedure("@AdHoc",
"SELECT COUNT(*) FROM " + tableName).getResults();
System.out.printf("Found %d employees.\n",
count[0].fetchRow(0).getLong(0));
最后,存储过程必须在项目文件 (project.xml) 中声明。如果打开本文随带的 项目文件,就会看到一些类似 清单 6 的条目。
清单 6. 在项目文件中声明存储过程
<procedures>
<procedure class="acme.procedures.AddEmployee" />
...
</procedures>
存储过程的另一个重要部分是注释 @ProcInfo,它向 VoltDB 告知数据在数据库中的存储方式。这称为分区,接下来我们将会讨论它。
分区
分区指表数据分散在整个集群中;一个表中的每一行在各个分区中分开存储。表基于您(开发人员)指定的一个主键进行分区。分区的主要目的是让尽可能多的查询在当站点上运行。
就像存储过程一样,您还必须在项目定义文件中声明分区信息。例如,清单 7 中所示的条目表明,employee 表中的条目分区在 email 列上。
清单 7. employee 表中的条目分区在 email 列上
<partitions>
<partition table='EMPLOYEE' column='EMAIL' />
...
</partitions>
回头看一个将某位员工的数据插入数据库中的存储过程,存储过程上的注释类似于 清单 8。
清单 8. 存储过程上的注释
@ProcInfo (
partitionInfo = "EMPLOYEE.EMAIL: 0",
singlePartition = true
)
这告诉 VoltDB 使用 employee 表的 email 列作为分区键,并且它是传递给 run 方法的第一个参数。在引用参数时使用以 0 开头的编号。它还表明该条目位于单个分区上。
选择用来对数据进行分区的键很重要,因为不使用分区键的查询会在多个分区上执行;在一个分区上运行的查询使其他分区能够(并行)执行其他查询,实现更高的吞吐量。
例如,假设您决定使用员工的 EMAIL(分区键)对 employee 表进行分区。以下查询将在单个分区上运行:SELECT FIRSTNAME, LASTNAME FROM EMPLOYEE WHERE EMAIL = "bob@acme.com";
。
因为每个员工的电子邮件地址是惟一的,所有只有一位员工拥有指定的电子邮件地址。但是,如果某个查询使用了不是分区键的字段,那么该查询将会在所有分区上执行(一种多分区查询),这会带来更低的总吞吐量:SELECT EMAIL FROM EMPLOYEE WHERE LASTNAME = "Smith"
。
这是因为多个员工可能拥有名字 “Smith”,这个名字并不是惟一的,所以必须查询所有分区。
出于这个原因,首先应该设计一组查询(和执行它们的频率),然后对表进行相应的分区,以便能够在一个站点上执行尽可能多的查询。
重复的表
除了分区表,也可以跨所有站点复制表。例如,将一个表添加到模式中,以记录虚构公司 Acme Inc 中存在的部门。表定义 (ddl.sql) 类似于 清单 9。
清单 9. ddl.sql
CREATE TABLE DEPARTMENT (
DEPARTMENT_ID INTEGER NOT NULL,
NAME VARCHAR(100) NOT NULL,
PRIMARY KEY (DEPARTMENT_ID)
);
还需要向 employee 表添加一个表示员工所在部门的列。
department 表是跨所有站点复制的理想的候选表,因为(至少本文中的)公司中拥有较少的部门且该表通常是只读的。通过复制表而不是对其分区,您可回答 “拥有电子邮件地址 ‘bob@acme.com’ 的员工所工作的部门叫什么?” 这样的查询,还可以避免跨多个分区执行联接 (join) 的需要 — 回想一下员工数据分隔到员工电子邮件字段上的情况。
请参见本文随带的 源代码 中的 EmployeeDetails.java,获得一个对 department 表执行联接的查询示例。
要告诉 VoltDB 复制一个表而不是对其分区,必须将该表的名称从项目定义文件 (project.xml) 的分区节中排除,在存储过程上声明 @ProcInfo 注释,就像 清单 10 一样。
清单 10. 声明 @ProcInfo 注释
@ProcInfo (
singlePartition = false
)
这会得到一个复制的表,而不是一个分区的表。
运行应用程序
要运行本文随带的应用程序,首先需要编译源代码并生成运行时目录。在项目文件夹的 root 目录中,运行命令 $ ant compile
。
此命令将编译源代码并生成运行时目录 (acme.jar)。
要启动数据库,请从项目的 root 目录运行 清单 11 中的命令。
清单 11. 启动数据库
$ voltdb start \
leader localhost \
catalog acme.jar \
deployment deployment.xml
还可以运行以下命令一次性地编译源代码,生成运行时目录和启动服务器:$ ant server
。
现在数据库服务器正在运行,可以对它执行一些查询。打开一个新终端窗口,从项目目录中启动客户端应用程序:$ ant client
。
这会启动将对数据库运行一些查询的客户端。请查看实现客户端的代码 (Client.java),以了解它的用途。
在运行数据库之后,可以通过分别关闭集群中的每个节点来停止它。由于本文中的应用程序仅在本地机器上运行,所以这没什么问题,您只需要在启动数据库的终端窗口中按下 Control-C 即可。如果您有一个集群包含几个节点,分别关闭每个节点可能会很麻烦。VoltDB 提供了一个 @Shutdown 过程,它将为您关闭整个数据库集群(参见 清单 12)。
清单 12. 关闭数据库 (ShutdownClient.java)
try {
client.callProcedure("@Shutdown");
} catch (Exception e) {
// An exception is expected here as
// when the database server is shut down
// it breaks the database connection to the client.
System.out.println("Shutdown request has been sent.");
}
要停止数据库,可以打开一个新终端窗口,从项目目录运行以下命令:$ ant shutdown
。
注意:关闭任务已添加到本文的 build.xml 文件中。如果您未使用本文随带的代码,则必须将它添加到您的生成文件中。
D 表示耐久性
本节将探讨 VoltDB 如何实现耐久性,介绍如何备份数据库来防止发生故障时丢失数据。
简介中已经提到,VoltDB 符合 ACID。耐久性需求(ACID 中的 “D”)表示,在提交一个事务之后,它的状态将保持不变,甚至在停电或发生系统故障时也是如此。换句话说,您仍然拥有您的数据。
值得一提的是,VoltDB 如何实现了耐久性,它毕竟是一个内存数据库。如果数据库出于某种原因而关闭,那么所有数据都会从内存中删除;毕竟内存是一种易失性存储媒介。VoltDB 使用快照来保存数据。
快照的名称准确表达了它的用途:数据库中存储的数据在给定时间点的快照。可以将 VoltDB 配置为以固定时间间隔自动创建快照,并将这些快照持久存储到磁盘上。当数据库出于某种原因而关闭时,可使用快照将数据库返回到它关闭前的状态。为此,在项目目录中打开部署文件 deployment.xml 并编辑它,类似于 清单 13:
清单 13. deployment.xml
<?xml version="1.0"?>
<deployment>
<cluster hostcount="1" sitesperhost="2" />
<paths>
<voltdbroot path="/tmp" />
<snapshots path="/tmp/autobackup" />
</paths>
<httpd enabled="true">
<jsonapi enabled="true" />
</httpd>
<snapshot prefix="acmesave"
frequency="2m"
retain="3" />
</deployment>
清单 13 告诉 VoltDB 每隔两分钟在 /tmp/autobackup 中创建一个备份并保留最后 3 个快照:在达到指定的保留限制时,会删除旧快照。在实际中,快照非常适合保存到网络挂载的位置(使用 NFS),以确保它们存储在一个不同的位置。
保存该文件,然后重新启动数据库。这时快照尚未启用,所以所有最新数据(运行客户端时添加的数据)都将丢失。您需要在数据库再次正常运行时再次运行客户端重新加载数据。几分钟后,文件夹 /tmp/autobackup 应包含数据库的快照。
再次关闭数据库,但这一次在启动它时使用恢复选项。当启用快照并指定了恢复选项时,VoltDB 会自动使用快照路径中最新的快照,将数据库还原到以前的状态。但是,请注意,如果尝试使用恢复选项启动数据库但没有找到快照,那么数据库不会启动。
要告诉 VoltDB 将数据库还原到以前的状态,可以运行 清单 14 中的命令。
清单 14. 告诉 VoltDB 还原数据库
$ voltdb recover \
leader localhost \
catalog acme.jar \
deployment deployment.xml
如果再次运行客户端 (ant client),这一次它总共会找到 5 个员工记录。当快照尚未启用且数据库已启动的时候,客户端第一次运行时员工总数为 0。
示例:VoltCache
现在快速看一下一个构建于 VoltDB 之上的真实应用程序:VoltCache。
VoltDB 发行版带来了许多示例,其中一个就是 VoltCache。VoltCache 是一个在 VoltDB 上实现的键值存储,可通过一个兼容 memcached 的 API 访问它,memcached 是一种流行的分布式缓存系统。只需执行两个步骤就能让它正常运行。
首先,启动 VoltDB 应用程序。要启动服务器,可以执行 清单 15 中的命令。
清单 15. 启动服务器
$ cd ~/voltdb-2.5/examples/voltcache
$ ./run.sh server
如果有必要的话,这会生成源代码并启动 VoltDB。接下来,在来自不同终端窗口的相同目录中运行以下命令:$ ./run.sh memcached-interface
。
这将启动模仿 memcached API(文本协议)的应用程序,它在端口 11211(memcached 服务器的默认端口)上进行监听。
清单 16 给出了一个 telnet 会话示例,您应在其中将键 foo
与值 bar
相关联,并再次检索它。
清单 16. telnet 会话示例
$ telnet localhost 11211
set foo 0 60 3
bar
STORED
get foo
VALUE foo 0 3 0
bar
END
quit
还可以使用已提供的许多 memcache 客户端库中的某个库。
该应用程序的源代码包含在 VoltDB 发行版中,我们可以看看它的工作原理。
结束语
VoltDB 是一个内存数据库,它提供了可伸缩性,但并没有在数据一致性上妥协。本文简要探讨了 VoltDB 的一些特性。您可以体验它的更多特性,比如导出实时数据、异步过程调用和 JSON API,这些特性支持您直接将 VoltDB 与一个 Web 应用程序进行集成。