在过去的几年中,出现了一种新型的数据库管理系统,称为NoSQL。 这些数据存储库旨在克服尝试扩展传统关系数据库以处理某些应用程序必须处理的数据负载(例如,亚马逊)的困难。 这种可伸缩性需要付出一定的代价:NoSQL系统通常不符合ACID(原子性,一致性,隔离性和耐用性); 它们最终是一致的,从广义上讲意味着给定一定的时间,所有对数据的更新最终将在系统中传播。 对于某些类型的应用程序,这是不希望的。
用于在线事务处理(OLTP)的传统关系数据库管理系统确实提供了一致性保证(它们符合ACID要求),但是扩展难度更大且成本昂贵。 研究还表明它们并不是特别有效:CPU花费大约10%的时间来检索和更新记录,其余90%则用于处理任务,例如缓冲区管理,锁定,锁存和日志记录。
传统的关系数据库(例如MySQL)和大多数NoSQL系统将其数据存储在磁盘上。 VoltDB将所有内容存储在主内存中。 如果可以避免使用磁盘,则可以显着提高性能-访问内存比使用磁盘快一个数量级! 如今,RAM的成本大大低于以前,并且随着64位计算的出现,您可以为标准的现成服务器配备数百GB的主内存。
VoltDB数据库由分布在多个站点(服务器)上的多个分区组成。 站点上运行的每个分区都是单线程的,这消除了在典型的多线程环境中与锁定和闩锁相关的开销,并且事务请求按顺序执行。
顾名思义,NoSQL数据库不要将SQL用作其查询语言。 例如,MongoDB查询以JSON表示。 Riak和CouchDB都支持使用map / reduce函数进行查询。 VoltDB确实使用SQL作为查询语言,这在一定程度上是一个优势,因为大多数使用数据库的开发人员都将熟悉SQL。 对于某些NoSQL数据库提供的查询接口,不能说相同的话。
通过用Java语言编写的存储过程来访问VoltDB中存储的数据。 SQL语句嵌入在存储过程中。 通过诸如JDBC之类的协议从存储过程中执行SQL查询的一个优点是,每个事务仅需要在客户端和服务器之间进行一次旅行。 这消除了与在应用程序和数据库之间跨网络进行多个调用相关的延迟。
有两种版本的VoltDB:开源社区版和付费企业版。 本文重点介绍社区版。 某些功能仅在企业版中可用,此处将不涵盖这些功能。
要尝试本文中的一些示例,您将需要下载并安装VoltDB。 本文中使用的版本是社区版本的2.5版。
VoltDB需要基于Linux的64位操作系统。 它也可以在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应用程序由以下文件组成:
在本文中,我将更详细地介绍每一个。
要将项目导入Eclipse,请打开Eclipse,然后执行以下操作:
如果您想创建自己的应用程序,VoltDB提供了一个为您生成框架项目的工具。 它用于为本文随附的应用程序生成文件夹结构。
清单1显示了如何调用它。
$ cd $HOME/voltdb-2.5/tools
$ ./generate app acme $HOME/Projects/app
该工具按以下顺序接受多个参数:
运行清单1中的命令,然后查看新创建的文件夹。 您将看到该工具生成了一个框架项目,其中包含构建VoltDB应用程序所需的文件。
如引言中所述,使用Java代码编写的存储过程可实现数据访问。 像传统的RDBMS一样,您仍然必须编写SQL查询才能从适当的表中获取所需的数据。 它只是在存储过程中完成的。 存储过程的每次调用都是一个事务。 如果存储过程成功,则将提交它们,否则将回退它们。
由于事务是串行的,因此在创建存储过程时要记住它们应该尽快执行非常重要。 否则,它们将阻止其他正在运行的事务。 例如,避免执行诸如发送电子邮件或对存储过程中的数据执行复杂分析之类的任务。
清单2给出了一个存储过程的示例,该存储过程将一个条目插入到employee表中。
@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显示了如何创建与数据库的连接。
// Create a client and connect to the database
org.voltdb.client Client client;
client = ClientFactory.createClient();
client.createConnection("localhost");
建立与数据库的连接后,即可查询数据库。 清单4中的代码显示了如何调用AddEmployee
存储过程,该过程将一些条目添加到employee表中。
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查询。 请参阅相关主题的链接描述SQL的子集VoltDB不支持网页。
必须预先声明存储过程中SQL语句,但可以在查询中使用绑定变量。 您可以在运行时对数据库运行临时查询,例如,带有动态字段SQL查询,方法是调用@AdHoc系统过程(请参见清单5 )。 建议不要对查询进行优化,因为它们会作为多分区事务执行,这会影响性能。
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的条目。
<procedures>
<procedure class="acme.procedures.AddEmployee" />
...
</procedures>
存储过程的另一个重要部分是@ProcInfo批注,它告诉VoltDB有关如何在数据库中存储数据。 这称为分区,下面将进行讨论。
分区是指表数据如何在整个群集中分布; 表中的每一行跨分区分别存储。 根据您(开发人员)指定的主键对表进行分区。 分区的主要目标是在单个站点上运行尽可能多的查询。
就像存储过程一样,您还必须在项目定义文件中声明分区信息。 例如, 清单7中显示的条目指示,employee表中的条目在email列上已分区。
<partitions>
<partition table='EMPLOYEE' column='EMAIL' />
...
</partitions>
回到将雇员插入数据库的存储过程中,该存储过程中的注释类似于清单8 。
@ProcInfo (
partitionInfo = "EMPLOYEE.EMAIL: 0",
singlePartition = true
)
这告诉VoltDB使用employee表的email列作为分区键,并且它是传递给run方法的第一个参数。 引用参数时使用从零开始的编号。 它还表明该条目位于单个分区上。
选择用于对数据进行分区的键很重要,因为不使用分区键的查询将在多个分区中执行; 在单个分区上运行的查询释放了其他分区以执行其他查询(并行),从而提高了吞吐量。
例如,假设您决定使用员工的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 。
CREATE TABLE DEPARTMENT (
DEPARTMENT_ID INTEGER NOT NULL,
NAME VARCHAR(100) NOT NULL,
PRIMARY KEY (DEPARTMENT_ID)
);
您还需要在员工表中添加一列以引用员工的部门。
部门表是在所有站点上复制的理想选择,因为至少在本文中,因为您公司中的部门很少,而且大多数部门都是只读的。 通过复制表而不是对其进行分区,您可以回答诸如“电子邮件地址为'bob@acme.com'的员工所在的部门名称是什么?”之类的查询。 通过在单个站点上执行查询。 您可以避免跨多个分区进行联接的操作-回忆员工数据已在“员工电子邮件”字段中进行了分区。
请参阅本文随附的源代码中的EmployeeDetails.java,以获取针对部门表进行联接的查询示例。
要告诉VoltDB复制表而不是对表进行分区,必须从项目定义文件(project.xml)的partitions部分中排除该表的名称,并在存储过程(如清单10 )上声明@ProcInfo批注。
@ProcInfo (
singlePartition = false
)
这将导致复制表而不是分区表。
要运行本文随附的应用程序,请首先编译源代码并构建运行时目录。 在项目文件夹的根目录中,运行命令: $ ant compile
。
此命令将编译源代码并构建运行时目录(acme.jar)。
要启动数据库,请从项目目录的根目录运行清单11中的命令。
$ voltdb start \
leader localhost \
catalog acme.jar \
deployment deployment.xml
或者,您可以通过运行以下命令来编译源代码,构建运行时目录并一次启动服务器: $ ant server
。
现在数据库服务器正在运行,对它执行一些查询。 打开一个新的终端窗口,并从项目目录中启动客户端应用程序: $ ant client
。
这将启动客户端,该客户端将对数据库运行一些查询。 查看实现客户端的代码(Client.java),以了解其功能。
数据库运行后,通过分别关闭集群中的每个节点来停止它。 由于本文中的应用程序仅在本地计算机上运行,因此这不是问题-您只需从启动数据库的终端窗口执行Control-C。 如果您的集群包含多个节点,那么分别关闭每个节点将很麻烦。 VoltDB提供了一个@Shutdown过程,它将为您关闭整个数据库集群(请参见清单12 )。
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文件中。 如果您不使用本文随附的代码,则必须将其添加到构建文件中。
本节将讨论VoltDB如何实现持久性,并展示如何备份数据库以防止出现故障时数据丢失。
如引言中所述,VoltDB符合ACID。 持久性要求(ACID中的“ D”)表示事务提交后,即使在断电或系统故障的情况下,事务也将保持不变。 换句话说,您仍将拥有数据。
值得一提的是,鉴于VoltDB是内存数据库,它是如何实现持久性的。 如果数据库由于某种原因而关闭,则所有数据将从内存中删除; 毕竟,存储器是易失性存储介质。 VoltDB使用快照来保存数据。
快照的确切含义是:在给定时间点存储在数据库中的数据的快照。 可以将VoltDB配置为以固定的时间间隔自动创建快照并将其持久化到磁盘。 如果数据库由于某种原因而关闭,则可以使用快照将数据库恢复为关闭前的状态。 为此,请在项目目录中打开部署文件deployment.xml,然后对其进行编辑,使其类似于清单13:
<?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中创建备份,并保留最后三个快照。 达到保留指定的限制时,将删除较旧的快照。 实际上,理想情况下,快照会保存到网络安装位置(使用NFS),以确保将快照存储在其他位置。
保存文件,然后重新启动数据库。 到目前为止,尚未启用快照,因此将丢失所有当前数据(运行客户端时添加的所有数据)。 一旦数据库启动并再次运行,您需要通过再次运行客户端来重新加载数据。 几分钟后,文件夹/ tmp / autobackup应该包含数据库的快照。
再次关闭数据库,但这一次在您启动数据库时使用恢复选项。 当启用快照并指定恢复选项时,VoltDB将使用快照路径中找到的最后一个快照将数据库自动恢复到其先前状态。 但是请注意,如果尝试使用restore选项启动数据库,但未找到快照,则数据库不会启动。
要告诉VoltDB将数据库还原到以前的状态,请运行清单14中的命令。
$ voltdb recover \
leader localhost \
catalog acme.jar \
deployment deployment.xml
如果您再次运行该客户端(蚂蚁客户端),这一次它将总共找到五个员工记录。 当未启用快照并启动数据库时,客户端第一次运行时员工总数为零。
现在快速浏览基于VoltDB构建的实际应用程序:VoltCache。
VoltDB发行版附带一些示例,其中之一就是VoltCache。 VoltCache是在VoltDB之上实现的键值存储,可以通过与memcached兼容的API进行访问-memcached是一种流行的分布式缓存系统。 要启动和运行它,需要两个步骤。
首先,启动VoltDB应用程序。 要启动服务器,请执行清单15中的命令。
$ cd ~/voltdb-2.5/examples/voltcache
$ ./run.sh server
如有必要,这将构建源代码并启动VoltDB。 接下来,在同一目录中,但在不同的终端窗口中,运行命令: $ ./run.sh memcached-interface
。
这将启动模仿memcached API(文本协议)的应用程序; 它侦听端口11211,这是内存缓存服务器的默认端口。
清单16显示了一个示例telnet会话,您在其中将键foo
与值bar
关联,然后再次检索它。
$ telnet localhost 11211
set foo 0 60 3
bar
STORED
get foo
VALUE foo 0 3 0
bar
END
quit
另外,您可以使用许多可用的内存缓存客户端库之一。
该应用程序的源代码包含在VoltDB发行版中,因此请查看它以了解其工作方式。
VoltDB是一个内存数据库,可提供可伸缩性而不损害数据一致性。 本文简要讨论了VoltDB的一些功能。 您可以探索更多功能,例如导出实时数据,异步过程调用和JSON API,该API允许VoltDB与Web应用程序直接集成。
翻译自: https://www.ibm.com/developerworks/java/library/os-voltdb/index.html