数据库切?/em> 是一个固有的关系程Q可以通过一些逻辑数据块将一个表的行分ؓ(f)不同的小l。例如,如果(zhn)正在根据时间戳对一个名?foo 的超大型表进行分区,2010 q?8 月之前的所有数据都进入分?AQ而之后的数据则全部进入分?B。分区可以加快读写速度Q因为它们的目标是单独分Z的较?yu)型数据集?/p> 分区功能q不L可用的(MySQL 直到 5.1 版本后才支持Q,而且光要的商业pȝ的成本也让h望而却步。更重要的是Q大部分分区实现在同一个物理机上存储数据,所以受到硬件基的媄(jing)响。除此之外, 分区也不能鉴别硬件的可靠性或者说~Z可靠性。因此,很多智慧的h们开始寻找进行~的新方法?/p>
切分 实质上是数据库别的分区Q它不是通过数据块分割数据表的行Q而是通过一些逻辑数据元素Ҏ(gu)据库本nq行分割Q通常跨不同的计算机)(j)。也是_(d)切分不是?em>数据?/em> 分割成小块,而是整?em>数据?/em> 分割成小块?/p>
切分的一个典型示例是ZҎ(gu)区域对一个存储世界范围客h据的大型数据库进行分Ԍ(x)切分 A 用于存储国的客户信息,切分 B 用户存储亚洲的客户信息,切分 C Ƨ洲Q等。这些切分分别处于不同的计算ZQ且每个切分存储所有相x据,如客户喜好或订购历史?/p>
切分的好处(如分ZP(j)在于它可以压~大型数据:(x)单独的数据表在每个切分中相对较小Q这样就可以支持更快速的d速度Q从而提高性能。切? q可以改善可靠性,因ؓ(f)即便一个切分意外失效,其他切分仍然可以服务数据。而且因ؓ(f)切分是在应用E序层面q行的,(zhn)可以对不支持常规分区的数据库进行切? 处理。资金成本较低同样也是一个潜在优ѝ?/p>
切分和策?/a>
像很多其他技术一Pq行切分时也需要作出部分妥协。因为切分不是一Ҏ(gu)地数据库技?— 也就是说Q必d应用E序中实?—在开始切分之前需要制定出(zhn)的切分{略。进行切分时主键和跨切分查询都扮演重要角Ԍ主要通过定义(zhn)不可以做什么实现?/p>
主键
切分利用多个数据库,其中所有数据库都独立v作用Q不q涉其他切分。因此,如果(zhn)依赖于数据库序列(如自动主键生成)(j)Q很有可能在一个数据库集中出现同 一个主键。可以跨分布式数据库协调序列Q但是这样会(x)增加pȝ的复杂程度。避免相同主键最安全的方法就是让应用E序Q应用程序将理切分pȝQ生成主键?/p>
跨切分查?/strong>
大部分切分实玎ͼ包括 Hibernate ShardsQ不支持跨切分查询,q就意味着Q如果?zhn)惛_用不同切分的两个数据集,必d理额外的长度。(有趣的是QAmazon ? SimpleDB 也禁止跨域查询)(j)例如Q如果将国客户信息存储在切?1 中,q需要将所有相x据存储在此。如果?zhn)试那些数据存储在切?2 中,情况׃(x)变得复杂Q系l性能也可能受影响。这U情况还与之前提到的一Ҏ(gu)?— 如果(zhn)因为某U原因需要进行跨切分q接Q最好采用一U可以消除重复的方式理键!
很明显,在徏立数据库前必d面考虑切分{略。一旦选择?jin)一个特定的方向之后Q?zhn)差不多就被它l定?— q行切分后很N便移动数据了(jin)?/p>
一个策略示?/a>
因ؓ(f)切分?zhn)l定在一个线型数据模型中Q也是_(d)(zhn)无法轻松连接不同切分中的数据)(j)Q?zhn)必须对如何在每个切分中对数据q行逻辑l织有一个清 晰的概念。这可以通过聚焦域中的主要节点实现。如在一个电(sh)子商务系l中Q主要节点可以是一个订单或者一个客戗因此,如果(zhn)选择 “客户” 作ؓ(f)切分{略的节点,那么与客h关的所有数据将Ud臛_自的切分中,但是(zhn)仍然必选择这些数据移动至哪个切分?/p>
对于客户来说Q?zhn)可以?gu)所在地Q欧zӀ亚zӀ非z等Q切分,或者?zhn)也可以根据其他元素进行切分。这由?zhn)军_。但是,(zhn)的切分{略应该包含? 数据均匀分布x有切分的Ҏ(gu)。切分的M概念是将大型数据集分割ؓ(f)型数据集;因此Q如果一个特定的?sh)子商务域包含一个大型的Ƨ洲客户集以?qing)一个相对小 的美国客户集Q那么基于客h在地的切分可能没有什么意义?/p>
回到比赛 — 使用切分Q?/a>
现在让我们回到我l常提到的赛跑应用程序示例中Q我可以Ҏ(gu)比赛或参赛者进行切分。在本示例中Q我根据比赛进行切分,因ؓ(f)我看到域是根据参 加不同比赛的参赛者进行组l的。因此,比赛是域的根。我也将Ҏ(gu)比赛距离q行切分Q因为比赛应用程序包含不同长度和不同参赛者的多项比赛?/p>
h意:(x)在进行上q决定时Q我已经接受?jin)一个妥协:(x)如果一个参赛者参加了(jin)不止一Ҏ(gu)赛,他们分属不同的切分,那该怎么?呢?Hibernate Shards Q像大多数切分实CP(j)不支持跨切分q接。我必须忍受q些d不便Q允许参赛者被包含在多个切分中 — 也就是说Q我在参赛者参加的多个比赛切分中重参赛者?/p>
Z(jin)便v见,我将创徏两个切分Q一个用?10 英里以下的比赛;另一个用?10 英里以上的比赛?/p>
实现 Hibernate Shards
Hibernate Shards 几乎可以与现?Hibernate 目无缝l合使用。唯一问题?Hibernate Shards 需要一些特定信息和行ؓ(f)。比如,需要一个切分访问策略、一个切分选择{略和一个切分处理策略。这些是(zhn)必d现的接口Q虽焉分情况下Q?zhn)可以使用默认{? 略。我们将在后面的部分逐个?jin)解各个接口?/p>
ShardAccessStrategy
执行查询ӞHibernate Shards 需要一个决定首个切分、第二个切分?qing)后l切分的机制。Hibernate Shards 无需定查询什么(q是 Hibernate Core 和基数据库需要做的)(j)Q但是它实意识刎ͼ在获得答案之前可能需要对多个切分q行查询。因此,Hibernate Shards 提供?jin)两U极具创意的逻辑实现Ҏ(gu)Q一U方法是Ҏ(gu)序列机制Q一ơ一个)(j)对切分进行查询,直到获得{案为止Q另一U方法是q行讉K{略Q这U方法用一? U程模型一ơ对所有切分进行查询?/p>
Z(jin)佉K题简单,我将使用序列{略Q名UCؓ(f) SequentialShardAccessStrategy
。我们将E后对其q行配置?/p>
ShardSelectionStrategy
当创Z个新对象Ӟ例如Q当通过 Hibernate 创徏一个新 Race
?Runner
Ӟ(j)QHibernate Shards 需要知道需对应的数据写入臛_些切分。因此,(zhn)必d现该接口q对切分逻辑q行~码。如果?zhn)惌行默认实玎ͼ有一个名?RoundRobinShardSelectionStrategy
的策略,它用一个@环策略将数据输入切分中?/p>
对于赛跑应用E序Q我需要提供根据比赛距进行切分的行ؓ(f)。因此,我们需要实?ShardSelectionStrategy
接口q提供依?Race
对象?distance
采用 selectShardIdForNewObject
Ҏ(gu)q行切分的简易逻辑。(我将E候在 Race
对象中展C。)(j)
q行Ӟ当在我的域对象上调用某一cM save
的方法时Q该接口的行为将被深层用?Hibernate 的核?j)?/p>
清单 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); } } }
|
如?zhn)?清单 1 中所看到的,如果持久化对象是一?Race
Q那么其距离被确定,而且Q因此)(j)选择?jin)一个切分。在q种情况下,有两个切分:(x)0 ?1Q其中切?1 中包?10 英里以上的比赛,切分 0 中包含所有其他比赛?/p>
如果持久化一?Runner
或其他对象,情况?x)稍微复杂一些。我已经~码?jin)一个逻辑规则Q其中有三个原则Q?/p>
- 一?
Runner
在没有对应的 Race
时无法存在? - 如果
Runner
被创建时参加?jin)多?Race
sQ这?Runner
被持久化到L到的首场 Race
所属的切分中。(Z说一句,该原则对未来有负面媄(jing)响。)(j) - 如果q保存了(jin)其他域对象,现在引发一个异常?
然后Q?zhn)可以擦掉额头的热汗了(jin),因?f)大部分艰隄工作已经搞定?jin)。随着比赛应用E序的增长,我所使用的逻辑可能?x)显得不够灵z,但是它完全可以顺利地完成q次演示Q?/p>
ShardResolutionStrategy
当通过键搜索一个对象时QHibernate Shards 需要一U可以决定首个切分的Ҏ(gu)。将需要?SharedResolutionStrategy
接口对其q行指引?/p>
如我之前提到的那P切分q(zhn)重视主键,因ؓ(f)(zhn)将需要亲自管理这些主键。幸q的是,Hibernate 在提供键?UUID 生成斚w表现良好。因?Hibernate Shards 创造性地提供一?ID 生成器,名ؓ(f) ShardedUUIDGenerator
Q它可以灉|地将切分 ID 信息嵌入?UUID 中?/p>
如果(zhn)最后?ShardedUUIDGenerator
q行键生成(我在本文中也采取这U方法)(j)Q那么?zhn)也可以?Hibernate Shards 提供的创?ShardResolutionStrategy
实现Q名?AllShardsShardResolutionStrategy
Q这可以军_依据一个特定对象的 ID 搜烦(ch)什么切分?/p>
配置?Hibernate Shards 工作所需的三个接口后Q我们就可以对切分示例应用程序的W二步进行实C(jin)。现在应该启?Hibernate ?SessionFactory
?jin)?/p>
配置 Hibernate Shards
Hibernate 的其中一个核?j)接口对象是它?SessionFactory
。Hibernate 的所有神奇都是在光|?Hibernate 应用E序q程中通过q个对象实现的Q例如,通过加蝲映射文g和配|。如果?zhn)使用了(jin)注释?Hibernate 珍贵?.hbm 文gQ那么?zhn)q需要一?SessionFactory
来让 Hibernate 知道哪些对象是可以持久化的,以及(qing)它们持久化?哪里?/p>
因此Q?Hibernate Shards Ӟ(zhn)必M用一个增强的 SessionFactory
cd来配|多个数据库。它可以被命名ؓ(f) ShardedSessionFactory
Q而且它当然是 SessionFactory
cd的。当创徏一?ShardedSessionFactory
Ӟ(zhn)必L供之前配|好的三个切分实现类型(ShardAccessStrategy
?code>ShardSelectionStrategy ?ShardResolutionStrategy
Q。?zhn)q需提供 POJO 所需的所有映文件。(如果(zhn)用一个基于备注的 Hibernate POJO 配置Q情况可能会(x)有所不同。)(j)最后,一?ShardedSessionFactory
CZ需要每个切分都对应多个 Hibernate 配置文g?/p>
创徏一?Hibernate 配置
我已l创Z(jin)一?ShardedSessionFactoryBuilder
cdQ它有一个主要方?createSessionFactory
Q可以创Z个配|合理的 SessionFactory
。之后,我将所有的一切都?Spring q接在一P现在谁不使用一?IOC 容器Q)(j)。现在,清单 2 昄?ShardedSessionFactoryBuilder
的主要作用:(x)创徏一?Hibernate 配置
Q?/p>
清单 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; }
|
如?zhn)?清单 2 中所看到的,可以?Hibernate 配置文g中创Z(jin)一个简单的 Configuration
? 该文件包含如下信息,如用的是什么类型的数据库、用户名和密码等Q以?qing)所有必ȝ资源文gQ如 POJO 所用的 .hbm 文g。在q行切分的情况下Q?zhn)通常需要用多个数据库配置Q但?Hibernate Shards 支持(zhn)仅使用一? hibernate.cfg.xml 文gQ从而简化了(jin)整个q程Q但是,如?zhn)?清单 4 中所看到的,(zhn)将需要对使用的每一个切分准备一?hibernate.cfg.xml 文gQ?/p>
下一步,在清?3 中,我将所有的切分配置都收集到?jin)一?List
中:(x)
清单 3. 切分配置列表
List<ShardConfiguration> shardConfigs = new ArrayList<ShardConfiguration>(); for (String hibconfig : this.hibernateConfigurations) { shardConfigs.add(buildShardConfig(hibconfig)); }
|
Spring 配置
?清单 3 中,?hibernateConfigurations
的引用指向了(jin) String
s List
Q其中每?String 都包含了(jin) Hibernate 配置文g的名字。该 List
通过 Spring 自动q接。清?4 是我?Spring 配置文g中的一D|录:(x)
清单 4. Spring 配置文g中的一部分
<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>
|
如?zhn)?清单 4 中所看到的,ShardedSessionFactoryBuilder
正在与一?POJO 映射文g和两个切分配|文件连接。清?5 中是 POJO 文g的一D|录:(x)
清单 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>
|
h意,清单 5 中的 POJO 映射的唯一独特斚w?ID 的生成器c?— q就?ShardedUUIDGenerator
Q它Q如(zhn)想象的一P(j)切?ID 信息嵌入?UUID 中。这是我的 POJO 映射中切分的唯一独特斚w?/p>
切分配置文g
下一步,如清?6 中所C,我已l配|了(jin)一个切?— 在本CZ中,除切?ID 和连接信息外Q切?0 和切?1 的文件是一L(fng)?/p>
清单 6. Hibernate Shards 配置文g
<?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>
|
如其名字所C,enable_cross_shard_relationship_checks
属性对跨切分关p进行了(jin)(g)查。根?Hibernate Shards 文档记录Q该属性非常耗时Q在生成环境中应该关闭?/p>
最后,ShardedSessionFactoryBuilder
通过创徏 ShardStrategyFactory
Q然后添加三个类型(包括 清单 1 中的 RacerShardSelectionStrategy
Q,一切都整合C(jin)一P如清?7 中所C:(x)
清单 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; }
|
最后,我执行了(jin)那个名ؓ(f) createSessionFactory
的绝妙方法,在本CZ中创Z(jin)一?ShardedSessionFactory
Q如清单 8 所C:(x)
清单 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 q接域对?/a>
现在可以深呼怸下了(jin)Q因为我们马上就成功?jin)。到目前为止Q我已经创徏一个可以合理配|?ShardedSessionFactory
的生成器c,其实是实现 Hibernate 无处不在?SessionFactory
cd?code>ShardedSessionFactory 完成?jin)切分中所有的奇。它利用我在 清单 1 中所部v的切分选择{略Qƈ从我配置的两个切分中d数据。(清单 6 昄?jin)切?0 和切?1 的配|几乎相同。)(j)
现在我需要做的就是连接我的域对象Q在本示例中Q因为它们依赖于 HibernateQ需要一?SessionFactory
cdq行工作。我仅使用我的 ShardedSessionFactoryBuilder
提供一U?SessionFactory
cdQ如清单 9 中所C:(x)
清单 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>
|
如?zhn)?清单 9 中所看到的,我首先在 Spring 中创Z(jin)一个类似工厂的 beanQ也是_(d)我的 RaceDAOImpl
cd有一个名?sessionFactory
的属性,?SessionFactory
cd。之后,mySessionFactory
引用通过?ShardedSessionFactoryBuilder
上调?createSessionFactory
Ҏ(gu)创徏?jin)一?SessionFactory
CZQ如 清单 4 中所C?/p>
当我为我?Race
对象CZ使用 SpringQ我主要其作ؓ(f)一个巨型工厂用,以返回预配置的对象)(j)Ӟ一切事情就都搞定了(jin)。虽然没有展C,RaceDAOImpl
cd是一个利?Hibernate 模板q行数据存储和检索的对象。我?Race
cd包含一?RaceDAOImpl
CZQ它?yu)所有与数据商店相关的活动都推迟x。很默契Q不是吗Q?/p>
h意,我的 DAO ?Hibernate Shards 在代码方面ƈ没有l定Q而是通过配置q行?jin)绑定。配|(?清单 5 中的Q将它们l定在一个特定切?UUID 生成Ҏ(gu)中,也就是说?jin)我可以在需要切分时从已?Hibernate 实现中重C用域对象?/p>
切分Q?easyb 的测试驱?/a>
接下来,我需要验证我的切分实现可以工作。我有两个数据库q过距离q行切分Q所以当我创Z场马拉松Ӟ10 英里以上的比赛)(j)Q该 Race
CZ应在切分 1 中找到。一个小型的比赛Q如 5 公里的比赛(3.1 英里Q,在切分 0 中找到。创Z?Race
后,我可以检查单个数据库的记录?/p>
在清?10 中,我已l创Z(jin)一场马拉松Q然后l验证记录确实是在切?1 中而非切分 0 中。事情更加有趣Q和单)(j)的是Q我使用? easybQ这是一个基?Groovy 的行为驱动开发架构,利用自然语言验证。easyb 也可以轻村֤?Java 代码。即便不?jin)? Groovy ?easybQ?zhn)也可以通过查看清单 10 中的代码Q看C切如期进行。(h意,我帮助创Z(jin) easybQƈ且在 developerWorks 中对q个话题发表q文章。)(j)
清单 10. 一个验证切分正性的 easyb 故事中一D|?/strong>
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() } }
|
当然Q我的工作还没有?— 我还需要创Z个短E比赛,q证其位于切分 0 中而非切分 1 中。?zhn)可以在本文提供?代码下蝲 中看到该验证操作Q?/p>
切分的利?/a>
切分可以增加应用E序的读写速度Q尤其是如果(zhn)的应用E序包含大量数据 — 如数 TB — 或者?zhn)的域处于无限制发展中Q如 Google ?Facebook?/p>
在进行切分之前,一定要定应用E序的规模和增长对其有利。切分的成本Q或者说~点Q包括对如何存储和检索数据的特定应用E序逻辑q行~码的成本。进行切分后Q?zhn)多多少都被锁定在(zhn)的切分模型中Q因为重新切分ƈ非易事?/p>
如果能够正确实现Q切分可以用于解决传l?RDBMS 规模和速度问题。切分对于绑定于关系基础架构、无法l升U硬件以满大量可~数据存储要求的l织来说是一个非常成本高效的决策?/p>