当关系数据库试图在一个单一表中存储数 TB 的数据时,总性能经常会降低。显然,对所有数据编索引不仅对于读而且对于写都很耗时。因为 NoSQL 数据商店尤其适合存储大型数据(如 Google 的 Bigtable),显然 NoSQL 是一种非关系数据库方法。对于倾向于使用 ACID-ity 和实体结构关系数据库的开发人员及需要这种结构的项目来说,切分是一个令人振奋的可选方法。

切分 是数据库分区的一个分支,但是它不是本地数据库技术 — 切分发生在应用程序级别。在各种切分实现中,Hibernate Shards 是 Java™ 技术世界中最受欢迎的一个。这个灵活绝妙的项目可以让您使用映射至逻辑数据库的 POJO 对切分数据集进行几乎无缝操作(我将在下文简要介绍 “几乎” 的原因)。使用 Hibernate Shards 时,您无须将您的 POJO 特别映射至切分 — 您可以像使用 Hibernate 方法对任何常见关系数据库进行映射时一样对其进行映射。Hibernate Shards 可以为您管理低级别的切分任务。

事实上,Hibernate Shards 的编码工作比较简单。其中关键的部分在于判断 如何进行切分以及对什么进行切分

切分简介

数据库切分 是一个固有的关系流程,可以通过一些逻辑数据块将一个表的行分为不同的小组。例如,如果您正在根据时间戳对一个名为 foo 的超大型表进行分区,2010 年 8 月之前的所有数据都将进入分区 A,而之后的数据则全部进入分区 B。分区可以加快读写速度,因为它们的目标是单独分区中的较小型数据集。

分区功能并不总是可用的(MySQL 直到 5.1 版本后才支持),而且其需要的商业系统的成本也让人望而却步。更重要的是,大部分分区实现在同一个物理机上存储数据,所以受到硬件基础的影响。除此之外, 分区也不能鉴别硬件的可靠性或者说缺乏可靠性。因此,很多智慧的人们开始寻找进行伸缩的新方法。

切分 实质上是数据库级别的分区:它不是通过数据块分割数据表的行,而是通过一些逻辑数据元素对数据库本身进行分割(通常跨不同的计算机)。也就是说,切分不是将数据表 分割成小块,而是将整个数据库 分割成小块。

切分的一个典型示例是基于根据区域对一个存储世界范围客户数据的大型数据库进行分割:切分 A 用于存储美国的客户信息,切分 B 用户存储亚洲的客户信息,切分 C 欧洲,等。这些切分分别处于不同的计算机上,且每个切分将存储所有相关数据,如客户喜好或订购历史。

切分的好处(如分区一样)在于它可以压缩大型数据:单独的数据表在每个切分中相对较小,这样就可以支持更快速的读写速度,从而提高性能。切分 还可以改善可靠性,因为即便一个切分意外失效,其他切分仍然可以服务数据。而且因为切分是在应用程序层面进行的,您可以对不支持常规分区的数据库进行切分 处理。资金成本较低同样也是一个潜在优势。

切分和策略

像很多其他技术一样,进行切分时也需要作出部分妥协。因为切分不是一项本地数据库技术 — 也就是说,必须在应用程序中实现 —在开始切分之前需要制定出您的切分策略。进行切分时主键和跨切分查询都扮演重要角色,主要通过定义您不可以做什么实现。

主键
切分利用多个数据库,其中所有数据库都独立起作用,不干涉其他切分。因此,如果您依赖于数据库序列(如自动主键生成),很有可能在一个数据库集中将出现同 一个主键。可以跨分布式数据库协调序列,但是这样会增加系统的复杂程度。避免相同主键最安全的方法就是让应用程序(应用程序将管理切分系统)生成主键。

跨切分查询
大部分切分实现(包括 Hibernate Shards)不支持跨切分查询,这就意味着,如果您想利用不同切分的两个数据集,就必须处理额外的长度。(有趣的是,Amazon 的 SimpleDB 也禁止跨域查询)例如,如果将美国客户信息存储在切分 1 中,还需要将所有相关数据存储在此。如果您尝试将那些数据存储在切分 2 中,情况就会变得复杂,系统性能也可能受影响。这种情况还与之前提到的一点有关 — 如果您因为某种原因需要进行跨切分连接,最好采用一种可以消除重复的方式管理键!

很明显,在建立数据库前必须全面考虑切分策略。一旦选择了一个特定的方向之后,您差不多就被它绑定了 — 进行切分后很难随便移动数据了。

一个策略示例

因为切分将您绑定在一个线型数据模型中(也就是说,您无法轻松连接不同切分中的数据),您必须对如何在每个切分中对数据进行逻辑组织有一个清 晰的概念。这可以通过聚焦域中的主要节点实现。如在一个电子商务系统中,主要节点可以是一个订单或者一个客户。因此,如果您选择 “客户” 作为切分策略的节点,那么与客户有关的所有数据将移动至各自的切分中,但是您仍然必须选择将这些数据移动至哪个切分。

对于客户来说,您可以根据所在地(欧洲、亚洲、非洲等)切分,或者您也可以根据其他元素进行切分。这由您决定。但是,您的切分策略应该包含将 数据均匀分布至所有切分的方法。切分的总体概念是将大型数据集分割为小型数据集;因此,如果一个特定的电子商务域包含一个大型的欧洲客户集以及一个相对小 的美国客户集,那么基于客户所在地的切分可能没有什么意义。

回到比赛 — 使用切分!

现在让我们回到我经常提到的赛跑应用程序示例中,我可以根据比赛或参赛者进行切分。在本示例中,我将根据比赛进行切分,因为我看到域是根据参 加不同比赛的参赛者进行组织的。因此,比赛是域的根。我也将根据比赛距离进行切分,因为比赛应用程序包含不同长度和不同参赛者的多项比赛。

请注意:在进行上述决定时,我已经接受了一个妥协:如果一个参赛者参加了不止一项比赛,他们分属不同的切分,那该怎么办 呢?Hibernate Shards (像大多数切分实现一样)不支持跨切分连接。我必须忍受这些轻微不便,允许参赛者被包含在多个切分中 — 也就是说,我将在参赛者参加的多个比赛切分中重建该参赛者。

为了简便起见,我将创建两个切分:一个用于 10 英里以下的比赛;另一个用于 10 英里以上的比赛。

实现 Hibernate Shards

Hibernate Shards 几乎可以与现有 Hibernate 项目无缝结合使用。唯一问题是 Hibernate Shards 需要一些特定信息和行为。比如,需要一个切分访问策略、一个切分选择策略和一个切分处理策略。这些是您必须实现的接口,虽然部分情况下,您可以使用默认策 略。我们将在后面的部分逐个了解各个接口。

ShardAccessStrategy

执行查询时,Hibernate Shards 需要一个决定首个切分、第二个切分及后续切分的机制。Hibernate Shards 无需确定查询什么(这是 Hibernate Core 和基础数据库需要做的),但是它确实意识到,在获得答案之前可能需要对多个切分进行查询。因此,Hibernate Shards 提供了两种极具创意的逻辑实现方法:一种方法是根据序列机制(一次一个)对切分进行查询,直到获得答案为止;另一种方法是并行访问策略,这种方法使用一个 线程模型一次对所有切分进行查询。

为了使问题简单,我将使用序列策略,名称为 SequentialShardAccessStrategy。我们将稍后对其进行配置。

ShardSelectionStrategy

当创建一个新对象时(例如,当通过 Hibernate 创建一个新 RaceRunner 时),Hibernate Shards 需要知道需将对应的数据写入至哪些切分。因此,您必须实现该接口并对切分逻辑进行编码。如果您想进行默认实现,有一个名为 RoundRobinShardSelectionStrategy 的策略,它使用一个循环策略将数据输入切分中。

对于赛跑应用程序,我需要提供根据比赛距离进行切分的行为。因此,我们需要实现 ShardSelectionStrategy 接口并提供依据 Race 对象的 distance 采用 selectShardIdForNewObject 方法进行切分的简易逻辑。(我将稍候在 Race 对象中展示。)

运行时,当在我的域对象上调用某一类似 save 的方法时,该接口的行为将被深层用于 Hibernate 的核心。


清单 1. 一个简单的切分选择策略

				
import org.hibernate.shards.ShardId;
import org.hibernate.shards.strategy.selection.ShardSelectionStrategy;

public class RacerShardSelectionStrategy implements ShardSelectionStrategy {

public ShardId selectShardIdForNewObject(Object obj) {
if (obj instanceof Race) {
Race rce = (Race) obj;
return this.determineShardId(rce.getDistance());
} else if (obj instanceof Runner) {
Runner runnr = (Runner) obj;
if (runnr.getRaces().isEmpty()) {
throw new IllegalArgumentException("runners must have at least one race");
} else {
double dist = 0.0;
for (Race rce : runnr.getRaces()) {
dist = rce.getDistance();
break;
}
return this.determineShardId(dist);
}
} else {
throw new IllegalArgumentException("a non-shardable object is being created");
}
}

private ShardId determineShardId(double distance){
if (distance > 10.0) {
return new ShardId(1);
} else {
return new ShardId(0);
}
}
}

 

如您在 清单 1 中所看到的,如果持久化对象是一场 Race,那么其距离被确定,而且(因此)选择了一个切分。在这种情况下,有两个切分:0 和 1,其中切分 1 中包含 10 英里以上的比赛,切分 0 中包含所有其他比赛。

如果持久化一个 Runner 或其他对象,情况会稍微复杂一些。我已经编码了一个逻辑规则,其中有三个原则:

  • 一名 Runner 在没有对应的 Race 时无法存在。
  • 如果 Runner 被创建时参加了多场 Races,这名 Runner 将被持久化到寻找到的首场 Race 所属的切分中。(顺便说一句,该原则对未来有负面影响。)
  • 如果还保存了其他域对象,现在将引发一个异常。

然后,您就可以擦掉额头的热汗了,因为大部分艰难的工作已经搞定了。随着比赛应用程序的增长,我所使用的逻辑可能会显得不够灵活,但是它完全可以顺利地完成这次演示!

ShardResolutionStrategy

当通过键搜索一个对象时,Hibernate Shards 需要一种可以决定首个切分的方法。将需要使用 SharedResolutionStrategy 接口对其进行指引。

如我之前提到的那样,切分迫使您重视主键,因为您将需要亲自管理这些主键。幸运的是,Hibernate 在提供键或 UUID 生成方面表现良好。因此 Hibernate Shards 创造性地提供一个 ID 生成器,名为 ShardedUUIDGenerator,它可以灵活地将切分 ID 信息嵌入到 UUID 中。

如果您最后使用 ShardedUUIDGenerator 进行键生成(我在本文中也将采取这种方法),那么您也可以使用 Hibernate Shards 提供的创新 ShardResolutionStrategy 实现,名为 AllShardsShardResolutionStrategy,这可以决定依据一个特定对象的 ID 搜索什么切分。

配置好 Hibernate Shards 工作所需的三个接口后,我们就可以对切分示例应用程序的第二步进行实现了。现在应该启动 Hibernate 的 SessionFactory 了。

配置 Hibernate Shards

Hibernate 的其中一个核心接口对象是它的 SessionFactory。Hibernate 的所有神奇都是在其配置 Hibernate 应用程序过程中通过这个小对象实现的,例如,通过加载映射文件和配置。如果您使用了注释或 Hibernate 珍贵的 .hbm 文件,那么您还需要一个 SessionFactory 来让 Hibernate 知道哪些对象是可以持久化的,以及将它们持久化到 哪里

因此,使用 Hibernate Shards 时,您必须使用一个增强的 SessionFactory 类型来配置多个数据库。它可以被命名为 ShardedSessionFactory,而且它当然是 SessionFactory 类型的。当创建一个 ShardedSessionFactory 时,您必须提供之前配置好的三个切分实现类型(ShardAccessStrategyShardSelectionStrategyShardResolutionStrategy)。您还需提供 POJO 所需的所有映射文件。(如果您使用一个基于备注的 Hibernate POJO 配置,情况可能会有所不同。)最后,一个 ShardedSessionFactory 示例需要每个切分都对应多个 Hibernate 配置文件。

创建一个 Hibernate 配置

我已经创建了一个 ShardedSessionFactoryBuilder 类型,它有一个主要方法 createSessionFactory,可以创建一个配置合理的 SessionFactory。之后,我将将所有的一切都与 Spring 连接在一起(现在谁不使用一个 IOC 容器?)。现在,清单 2 显示了 ShardedSessionFactoryBuilder 的主要作用:创建一个 Hibernate 配置


清单 2. 创建一个 Hibernate 配置

				
private Configuration getPrototypeConfig(String hibernateFile, List<String>
resourceFiles) {
Configuration config = new Configuration().configure(hibernateFile);
for (String res : resourceFiles) {
configs.addResource(res);
}
return config;
}

 

如您在 清单 2 中所看到的,可以从 Hibernate 配置文件中创建了一个简单的 Configuration。 该文件包含如下信息,如使用的是什么类型的数据库、用户名和密码等,以及所有必须的资源文件,如 POJO 所用的 .hbm 文件。在进行切分的情况下,您通常需要使用多个数据库配置,但是 Hibernate Shards 支持您仅使用一个 hibernate.cfg.xml 文件,从而简化了整个过程(但是,如您在 清单 4 中所看到的,您将需要对使用的每一个切分准备一个 hibernate.cfg.xml 文件)。

下一步,在清单 3 中,我将所有的切分配置都收集到了一个 List 中:


清单 3. 切分配置列表

				
List<ShardConfiguration> shardConfigs = new ArrayList<ShardConfiguration>();
for (String hibconfig : this.hibernateConfigurations) {
shardConfigs.add(buildShardConfig(hibconfig));
}

 

Spring 配置

清单 3 中,对 hibernateConfigurations 的引用指向了 Strings List,其中每个 String 都包含了 Hibernate 配置文件的名字。该 List 通过 Spring 自动连接。清单 4 是我的 Spring 配置文件中的一段摘录:


清单 4. Spring 配置文件中的一部分

				
<bean id="shardedSessionFactoryBuilder"
class="org.disco.racer.shardsupport.ShardedSessionFactoryBuilder">
<property name="resourceConfigurations">
<list>
<value>racer.hbm.xml</value>
</list>
</property>
<property name="hibernateConfigurations">
<list>
<value>shard0.hibernate.cfg.xml</value>
<value>shard1.hibernate.cfg.xml</value>
</list>
</property>
</bean>

 

如您在 清单 4 中所看到的,ShardedSessionFactoryBuilder 正在与一个 POJO 映射文件和两个切分配置文件连接。清单 5 中是 POJO 文件的一段摘录:


清单 5. 比赛 POJO 映射

				
<class name="org.disco.racer.domain.Race" table="race"dynamic-update="true"
dynamic-insert="true">

<id name="id" column="RACE_ID" unsaved-value="-1">
<generator class="org.hibernate.shards.id.ShardedUUIDGenerator"/>
</id>

<set name="participants" cascade="save-update" inverse="false" table="race_participants"
lazy="false">
<key column="race_id"/>
<many-to-many column="runner_id" class="org.disco.racer.domain.Runner"/>
</set>

<set name="results" inverse="true" table="race_results" lazy="false">
<key column="race_id"/>
<one-to-many class="org.disco.racer.domain.Result"/>
</set>

<property name="name" column="NAME" type="string"/>
<property name="distance" column="DISTANCE" type="double"/>
<property name="date" column="DATE" type="date"/>
<property name="description" column="DESCRIPTION" type="string"/>
</class>

 

请注意,清单 5 中的 POJO 映射的唯一独特方面是 ID 的生成器类 — 这就是 ShardedUUIDGenerator,它(如您想象的一样)将切分 ID 信息嵌入到 UUID 中。这就是我的 POJO 映射中切分的唯一独特方面。

切分配置文件

下一步,如清单 6 中所示,我已经配置了一个切分 — 在本示例中,除切分 ID 和连接信息外,切分 0 和切分 1 的文件是一样的。


清单 6. Hibernate Shards 配置文件

				
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory name="HibernateSessionFactory0">
<property name="dialect">org.hibernate.dialect.HSQLDialect</property>
<property name="connection.driver_class">org.hsqldb.jdbcDriver</property>
<property name="connection.url">
jdbc:hsqldb:file:/.../db01/db01
</property>
<property name="connection.username">SA</property>
<property name="connection.password"></property>
<property name="hibernate.connection.shard_id">0</property>
<property name="hibernate.shard.enable_cross_shard_relationship_checks">true
</property>
</session-factory>
</hibernate-configuration>

 

如其名字所示,enable_cross_shard_relationship_checks 属性对跨切分关系进行了检查。根据 Hibernate Shards 文档记录,该属性非常耗时,在生成环境中应该关闭。

最后,ShardedSessionFactoryBuilder 通过创建 ShardStrategyFactory ,然后添加三个类型(包括 清单 1 中的 RacerShardSelectionStrategy),将一切都整合到了一起,如清单 7 中所示:


清单 7. 创建 ShardStrategyFactory

				
private ShardStrategyFactory buildShardStrategyFactory() {
ShardStrategyFactory shardStrategyFactory = new ShardStrategyFactory() {
public ShardStrategy newShardStrategy(List<ShardId> shardIds) {
ShardSelectionStrategy pss = new RacerShardSelectionStrategy();
ShardResolutionStrategy prs = new AllShardsShardResolutionStrategy(shardIds);
ShardAccessStrategy pas = new SequentialShardAccessStrategy();
return new ShardStrategyImpl(pss, prs, pas);
}
};
return shardStrategyFactory;
}

 

最后,我执行了那个名为 createSessionFactory 的绝妙方法,在本示例中创建了一个 ShardedSessionFactory,如清单 8 所示:


清单 8. 创建 ShardedSessionFactory

				
public SessionFactory createSessionFactory() {
Configuration prototypeConfig = this.getPrototypeConfig
(this.hibernateConfigurations.get(0), this.resourceConfigurations);

List<ShardConfiguration> shardConfigs = new ArrayList<ShardConfiguration>();
for (String hibconfig : this.hibernateConfigurations) {
shardConfigs.add(buildShardConfig(hibconfig));
}

ShardStrategyFactory shardStrategyFactory = buildShardStrategyFactory();
ShardedConfiguration shardedConfig = new ShardedConfiguration(
prototypeConfig, shardConfigs,shardStrategyFactory);
return shardedConfig.buildShardedSessionFactory();
}

 

使用 Spring 连接域对象

现在可以深呼吸一下了,因为我们马上就成功了。到目前为止,我已经创建一个可以合理配置 ShardedSessionFactory 的生成器类,其实就是实现 Hibernate 无处不在的 SessionFactory 类型。ShardedSessionFactory 完成了切分中所有的神奇。它利用我在 清单 1 中所部署的切分选择策略,并从我配置的两个切分中读写数据。(清单 6 显示了切分 0 和切分 1 的配置几乎相同。)

现在我需要做的就是连接我的域对象,在本示例中,因为它们依赖于 Hibernate,需要一个 SessionFactory 类型进行工作。我将仅使用我的 ShardedSessionFactoryBuilder 提供一种 SessionFactory 类型,如清单 9 中所示:


清单 9. 在 Spring 中连接 POJO

				
<bean id="mySessionFactory"
factory-bean="shardedSessionFactoryBuilder"
factory-method="createSessionFactory">
</bean>

<bean id="race_dao" class="org.disco.racer.domain.RaceDAOImpl">
<property name="sessionFactory">
<ref bean="mySessionFactory"/>
</property>
</bean>

 

如您在 清单 9 中所看到的,我首先在 Spring 中创建了一个类似工厂的 bean;也就是说,我的 RaceDAOImpl 类型有一个名为 sessionFactory 的属性,是 SessionFactory 类型。之后,mySessionFactory 引用通过在 ShardedSessionFactoryBuilder 上调用 createSessionFactory 方法创建了一个 SessionFactory 示例,如 清单 4 中所示。

当我为我的 Race 对象示例使用 Spring(我主要将其作为一个巨型工厂使用,以返回预配置的对象)时,一切事情就都搞定了。虽然没有展示,RaceDAOImpl 类型是一个利用 Hibernate 模板进行数据存储和检索的对象。我的 Race 类型包含一个 RaceDAOImpl 示例,它将所有与数据商店相关的活动都推迟至此。很默契,不是吗?

请注意,我的 DAO 与 Hibernate Shards 在代码方面并没有绑定,而是通过配置进行了绑定。配置(如 清单 5 中的)将它们绑定在一个特定切分 UUID 生成方案中,也就是说了我可以在需要切分时从已有 Hibernate 实现中重新使用域对象。

切分:使用 easyb 的测试驱动

接下来,我需要验证我的切分实现可以工作。我有两个数据库并通过距离进行切分,所以当我创建一场马拉松时(10 英里以上的比赛),该 Race 示例应在切分 1 中找到。一个小型的比赛,如 5 公里的比赛(3.1 英里),将在切分 0 中找到。创建一场 Race 后,我可以检查单个数据库的记录。

在清单 10 中,我已经创建了一场马拉松,然后继续验证记录确实是在切分 1 中而非切分 0 中。使事情更加有趣(和简单)的是,我使用了 easyb,这是一个基于 Groovy 的行为驱动开发架构,利用自然语言验证。easyb 也可以轻松处理 Java 代码。即便不了解 Groovy 或 easyb,您也可以通过查看清单 10 中的代码,看到一切如期进行。(请注意,我帮助创建了 easyb,并且在 developerWorks 中对这个话题发表过文章。)


清单 10. 一个验证切分正确性的 easyb 故事中一段摘录

				
scenario "races greater than 10.0 miles should be in shard 1 or db02", {
given "a newly created race that is over 10.0 miles", {
new Race("Leesburg Marathon", new Date(), 26.2,
"Race the beautiful streets of Leesburg!").create()
}
then "everything should work fine w/respect to Hibernate", {
rce = Race.findByName("Leesburg Marathon")
rce.distance.shouldBe 26.2
}
and "the race should be stored in shard 1 or db02", {
sql = Sql.newInstance(db02url, name, psswrd, driver)
sql.eachRow("select race_id, distance, name from race where name=?",
["Leesburg Marathon"]) { row ->
row.distance.shouldBe 26.2
}
sql.close()
}
and "the race should NOT be stored in shard 0 or db01", {
sql = Sql.newInstance(db01url, name, psswrd, driver)
sql.eachRow("select race_id, distance, name from race where name=?",
["Leesburg Marathon"]) { row ->
fail "shard 0 contains a marathon!"
}
sql.close()
}
}

 

当然,我的工作还没有完 — 我还需要创建一个短程比赛,并验证其位于切分 0 中而非切分 1 中。您可以在本文提供的 代码下载 中看到该验证操作!

切分的利弊

切分可以增加应用程序的读写速度,尤其是如果您的应用程序包含大量数据 — 如数 TB — 或者您的域处于无限制发展中,如 Google 或 Facebook。

在进行切分之前,一定要确定应用程序的规模和增长对其有利。切分的成本(或者说缺点)包括对如何存储和检索数据的特定应用程序逻辑进行编码的成本。进行切分后,您多多少少都被锁定在您的切分模型中,因为重新切分并非易事。

如果能够正确实现,切分可以用于解决传统 RDBMS 规模和速度问题。切分对于绑定于关系基础架构、无法继续升级硬件以满足大量可伸缩数据存储要求的组织来说是一个非常成本高效的决策。