当关系数据库尝试将TB的数据存储在单个表中时,总体性能通常会下降。 索引所有这些数据显然对于读取和写入来说都是昂贵的。 虽然NoSQL数据存储特别适合存储大数据(例如Google的Bigtable),但NoSQL是一种专利的非关系方法。 对于喜欢ACID-ity和关系数据库的可靠结构的开发人员,或者需要它的项目,分片可能是一个令人兴奋的选择。
分片 ,数据库分区的一个分支,是不是本地数据库技术-它发生在应用的水平。 在各种分片实现中,Hibernate Shards可能是Java™技术领域中最流行的。 这个漂亮的项目使您可以使用映射到逻辑数据库的POJO或多或少地无缝处理分片数据集(我将在稍后解释“或多或少”部分)。 使用Hibernate Shards时,不必专门将POJO映射到分片-您可以像使用Hibernate方式处理任何普通关系数据库一样映射它们。 Hibernate Shards为您管理低级分片的东西。
到目前为止,在本系列文章中 ,我已经基于种族和跑步者的类比使用了一个简单的域来演示各种数据存储技术。 本月,我将使用这个熟悉的示例介绍一个实用的分片策略,然后在Hibernate Shards中实现它。 注意,与分片有关的工作首屈一指,不一定与Hibernate有关。 实际上,为Hibernate Shards编码是很容易的部分。 真正的工作是弄清楚如何分片 。
分片一目了然
数据库分区是一个固有的关系过程,将表的行按逻辑数据段分成较小的组。 例如,如果您基于时间戳对名为foo的巨型表进行分区,则2010年8月的所有数据都将进入分区A,而此后的所有数据都将进入分区B,依此类推。 分区的作用是使读取和写入更快,因为它们的目标是各个分区中的较小数据集。
分区并非总是可用的(MySQL直到5.1版才支持),并且使用商业系统进行分区的成本可能很高。 而且,大多数分区实现都将数据存储在同一台物理计算机上,因此您仍然受制于硬件的限制。 分区也无法解决硬件的可靠性或缺乏可靠性。 因此,各种聪明人开始寻找新的扩展方式。
由数据,数据库本身被分离的片(通常在不同的机器上)通过一些逻辑数据元素,而不是分表的行: 分片基本上分隔在数据库级别。 也就是说,分片不是将表拆分为较小的块,而是将整个数据库拆分为较小的块。
分片的典型示例基于按区域划分存储全球客户数据的大型数据库:针对美国客户的Shard A,针对亚洲客户的Shard B,针对欧洲的Shard C,等等。 分片本身将驻留在不同的计算机上,并且每个分片将保存所有相关数据,例如客户偏好或订单历史记录。
分片(例如分区)的好处是它可以压缩大数据:每个分片中的单个表较小,这样可以加快读写速度,从而提高性能。 分片也可以提高可靠性,因为即使一个分片意外失败,其他分片仍然能够提供数据。 而且由于分片是在应用程序层完成的,因此您可以对不支持常规分区的数据库进行分片。 货币成本也可能更低。
分片与策略
与大多数技术一样,分片确实需要进行一些折衷。 因为分片不是本机数据库技术,也就是说,您必须在应用程序中实现它,所以在开始之前,您需要制定分片策略。 在分片时,主键和跨分片查询都起主要作用,主要是通过定义您不能执行的操作。
主键
分片利用多个数据库,所有这些数据库都可以自动运行,而无需了解其同级。 结果,如果您依赖数据库序列(例如用于自动主键生成),则可能会在一组数据库中显示相同的主键。 可以在分布式数据库中协调序列,但这样做会增加系统复杂性。 禁止重复的主键的最安全方法是让您的应用程序(无论如何将管理分片系统)生成密钥。
跨分片查询
大多数分片实现(包括Hibernate Shard)都不允许跨分片查询,这意味着如果要利用来自不同分片的两组数据,则必须加倍努力。 (有趣的是,Amazon的SimpleDB也禁止跨域查询。)例如,如果您将美国客户存储在Shard 1中,则还需要将其所有相关数据存储在那里。 如果尝试将这些数据存储在Shard 2中,则事情将会变得复杂,并且系统性能可能会受到影响。 这种情况也与前面提到的观点有关-如果您最终以某种方式需要进行交叉分片联接,则最好以消除重复的可能性的方式来管理密钥!
显然,在建立数据库之前,您需要充分考虑分片策略。 一旦选择了一个特定的方向,您便或多或少地受到它的束缚–数据在分片后很难移动。
战略实例
由于分片将您绑定到线性数据模型(也就是说,您无法轻松地将数据连接到不同的分片中),因此应该首先清楚地了解每个分片在逻辑上如何组织数据。 通常,最简单的做法是关注域的主节点。 在电子商务系统的情况下,主要节点可以是订单或客户。 因此,如果您选择“客户”作为分片策略的基础,那么与客户相关的所有数据都将被移至相应的分片中,尽管您仍然必须选择将数据移至哪个分片上。
对于客户,您可以根据地理位置(欧洲,亚洲,非洲等)进行分片,也可以根据其他内容进行分片。 由你决定。 但是,您的分片策略应该包含一些在所有分片之间平均分配数据的方法。 分片的整个想法是将大数据集分解为较小的数据集。 因此,如果特定的电子商务领域拥有大量的欧洲客户,而在美国则相对较少,那么根据客户位置进行分片可能就没有意义。
参加比赛-分片!
回到我的赛车应用程序的熟悉示例,我可以按种族或按跑步者进行分片。 在这种情况下,我将按种族进行分片,因为我看到域是由属于种族的跑步者组织的。 因此种族是我领域的根源。 我还将根据比赛距离进行分片,因为我的赛车应用程序与无数跑步者一起举行不同长度的无数比赛。
请注意,在做出这些决定时,我已经接受了一个折衷方案:如果跑步者参加了不止一场比赛,而每个人都生活在不同的碎片中,该怎么办? Hibernate Shards(像大多数分片实现一样)不支持跨分片联接。 我将不得不忍受这种轻微的不便,并允许跑步者住在多个碎片中,也就是说,我将在他或她的各种种族所居住的碎片中重新创建每个跑步者。
为简单起见,我将创建两个碎片:一个碎片用于小于10英里的比赛,另一个碎片用于大于10英里的比赛。
实施Hibernate分片
Hibernate Shards可以与现有的Hibernate项目无缝地协同工作。 唯一的不足是,Hibernate Shards需要您提供一些特定的信息和行为。 即,它需要分片访问策略,分片选择策略和分片解析策略。 这些是必须实现的接口,尽管在某些情况下可以使用默认接口。 在以下各节中,我们将分别查看每个接口。
分片访问策略
执行查询时,Hibernate Shards需要一种机制来确定哪个,第一个,第二个命中碎片。 Hibernate Shards不一定找出查询的内容(这是Hibernate Core和基础数据库要做的),但它确实知道在获得答案之前,可能需要针对多个分片执行查询。 因此,Hibernate Shards提供了两种开箱即用的逻辑实现:一种以顺序机制(一次一个)对分片执行查询,直到返回答案为止,或者直到所有分片都被查询为止。 另一种实现是并行访问策略,该策略使用线程模型一次命中所有分片。
我将使事情变得简单,并使用适当地命名为SequentialShardAccessStrategy
的顺序策略。 我们将很快对其进行配置。
分片选择策略
创建新对象时(即,通过Hibernate创建新的Race
或Runner
),Hibernate Shards需要知道应将相应数据写入哪个分片。 因此,您必须实现此接口并编码分片逻辑。 如果要使用默认实现,则有一个名为RoundRobinShardSelectionStrategy
,它使用循环策略将数据放入分RoundRobinShardSelectionStrategy
。
对于赛车应用程序,我需要提供根据比赛距离进行分片的行为。 因此,我将需要实现ShardSelectionStrategy
接口,并提供一些简单的逻辑,这些逻辑基于selectShardIdForNewObject
方法中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);
}
}
}
如L 清单1所示 ,如果持久化的对象是Race
,则确定其距离,并因此选择一个分片。 在这种情况下,有两个分片:0和1,其中分片1持有距离大于10英里的种族,而分片0持有所有其他种族。
如果要Runner
或其他对象,那么事情会涉及更多。 我编写了具有三个规定的逻辑规则:
- 没有相应的
Race
Runner
者不可能存在。 - 如果一个
Runner
已经有多个创建Race
秒,Runner
将在碎片被坚持了第一个Race
中。 (顺便说一下,这条规则对未来有负面影响。) - 如果正在保存其他域对象,则将引发异常。
这样,您就可以擦去额头上的汗水,因为大多数辛苦的工作已经完成。 随着赛车应用程序的增长,我所捕获的逻辑可能不够灵活,但是对于本演示而言,它将起作用!
分片解决策略
当通过其键搜索对象时,Hibernate Shards需要一种确定首先击中哪个碎片的方法。 您将使用SharedResolutionStrategy
接口进行指导。
正如我之前提到的,分片迫使您敏锐地意识到主键,因为您可以自己管理主键。 幸运的是,Hibernate已经擅长提供密钥或UUID生成。 因此,Hibernate Shards ShardedUUIDGenerator
即ShardedUUIDGenerator
提供了一个名为ShardedUUIDGenerator
的ID生成器,该ID生成器具有将Shard ID信息嵌入UUID本身的ShardedUUIDGenerator
。
如果最终使用ShardedUUIDGenerator
进行密钥生成(如本文所述),那么您也可以使用名为AllShardsShardResolutionStrategy
的Hibernate Shards开箱即用ShardResolutionStrategy
实现,该实现可以根据特定的内容确定要搜索的碎片。对象的ID。
配置了Hibernate Shards正常工作所需的三个接口后,我们就可以开始分步示例应用程序了。 现在该启动Hibernate的SessionFactory
。
配置Hibernate分片
Hibernate的核心接口对象之一是它的SessionFactory
。 所有的Hibernate魔术都通过这个小对象发生,因为它配置了Hibernate应用程序,例如,通过加载映射文件和配置。 如果您使用批注或Hibernate的古老.hbm文件,您仍然需要一个SessionFactory
来允许Hibernate知道哪些对象是可持久的,以及在哪里持久化。
因此,对于Hibernate Shards,您必须利用能够配置多个数据库的增强型SessionFactory
类型。 它被适当地命名为ShardedSessionFactory
并且当然是SessionFactory
类型。 创建ShardedSessionFactory
,必须提供先前配置的三种shard实现类型( ShardAccessStrategy
, ShardSelectionStrategy
和ShardResolutionStrategy
)。 您还必须提供POJO所需的所有映射文件。 (如果使用基于注释的Hibernate POJO配置,则略有不同。)最后,一个ShardedSessionFactory
实例需要具有多个Hibernate配置文件,这些文件与您希望利用的每个分片相对应。
创建Hibernate配置
我创建了一个ShardedSessionFactoryBuilder
类型,它具有一个主要方法createSessionFactory
,该方法创建了一个适当配置的SessionFactory
。 稍后,我将把所有东西与Spring连接在一起(这些天谁没有利用IOC容器?)。 现在,清单2显示了ShardedSessionFactoryBuilder
的主要功能:创建一个Hibernate Configuration
:
清单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碎片可以很方便地只使用一个hibernate.cfg.xml文件(你需要为每个你打算使用碎片,但是,你可以在看上市4 )。
接下来,在清单3中,我将所有分片配置收集到一个List
:
清单3.分片配置列表
List<ShardConfiguration> shardConfigs = new ArrayList<ShardConfiguration>();
for (String hibconfig : this.hibernateConfigurations) {
shardConfigs.add(buildShardConfig(hibconfig));
}
Spring配置
在清单3中 ,对hibernateConfigurations
的引用指向String
的List
,每个List
包含一个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映射文件和两个shard配置文件连接在一起。 清单5显示了POJO文件的一个片段:
清单5. Race 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
,(您会记得)将Shard ID信息嵌入到UUID本身中。 那是我的POJO映射中分片的唯一特定方面。
分片配置文件
接下来,在清单6中,我配置了一个分片,在这种情况下,是分片0。分片1的文件除了分片ID和连接信息外都是相同的。
清单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中定义。
当我向Spring(我主要用作返回预配置对象的巨型工厂)向我的Race
对象的实例询问时,一切都将被设置。 尽管未显示, RaceDAOImpl
类型是一个利用Hibernate模板进行数据存储和检索的对象。 我的Race
类型拥有RaceDAOImpl
的实例,该实例将延缓与数据存储相关的所有活动。 很舒服吧?
请注意,我的DAO不在代码中与Hibernate Shards绑定,而是通过配置绑定。 配置(在清单5中 )将它们绑定到特定于分片的UUID生成方案,这意味着当我需要分片时,我可以重用现有Hibernate实现中的域对象。
分片:使用easyb进行测试
接下来,我需要验证我的分片实施是否正常。 我有两个数据库,并且要按距离进行分片,因此,当我创建马拉松(大于10英里)时,例如,应该在Shard 1中找到Race
实例。较小的种族,如5K(即3.1)英里),应该在碎片0中找到。创建Race
,可以检查单个数据库的记录。
在清单10中,我创建了一个马拉松比赛,然后继续验证该记录确实在Shard 1中,而不是在Shard 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中规模和速度的关键。 对于与关系基础架构相关的组织而言,分片是一种特别具有成本效益的决策,而这些关系基础架构无法继续升级硬件以满足大规模可扩展数据存储的需求。
翻译自: https://www.ibm.com/developerworks/java/library/j-javadev2-11/index.html