`
forchenyun
  • 浏览: 312217 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

使用Spring和Hibernate框架操作数据库水平分区

    博客分类:
  • Java
阅读更多

翻译了几年前的一篇文章,思想很不错。

http://www.jroller.com/kenwdelong/entry/horizontal_database_partitioning_with_spring

简介

       大约在一年以前,我决定水平扩展我们的数据库。在我们的数据库中我们拥有数百万的用户,我们期望我们的用户为我们的网站生成更多的内容,同时我们将收集更多的用户行为。我们已经被垂直扩展的策略搞得焦头烂额,我们越来越难于对硬件进行扩展,你只能同时扩展一两个硬件,并且当它挂掉时,所有的东西都崩溃掉了。因此,我们决定使用普通硬件设备对数据库进行水平扩展。

       一个专门从事Mysql数据库的可扩展性方面研究的顾问建议我们,基于用户进行水平分区:一个用户及其所有数据(人物概况,用户生成的内容等等)将存在某一个分区上。一个全局用户数据库(GLUD)将会是这组数据库的主键,GLUD将存储每个用户的主键和这个用户所在的那个分区的ID

       我们继续。我们最初的想法是为每个分区创建一个Hibernatesession factory。假设我们有两个用户数据库,user1user2.那么我们将有两个session factories,每个数据库对应一个。使用这些数据库的Service(例如ProfileService)将会为每个数据库创建一个实例。Profile1Service将关联到使用use1SessionFactoryprofile1Dao.对于N个分区的类似。调用该Service将触发一个Spring aop的拦截器,它将获取该用户的标识符,查询GLUD来决定该用户的数据存在哪个分区上,随后将会转发调用到正确的ProfileService实例上。

       我们实现了一个这种方式的原型,并且它运行良好。随后我们遇到了两个想法。第一个是Interface21's Mark Fisher的博客所介绍的AbstractRoutingDataSource。第二个是Hibernate shards项目。第一种方式我们只需要创建一个ProfileService,一个ProfileDao以及一个UserSessionFactory,并让datasource知道有多个用户数据库。Hibernate shards是一个项目,其运行原理和我们最初的那个想法类似,为每个数据库创建一个session factory实例。

       我们倾向于使用hibernate shards,这样就不用编写我们自己的分区系统。但是hibernate shards目前只发布了测试版。最好我们由于以下几个原因放弃使用hibernate shards:我们观察了数周,但是只有极少的人活跃在hibernate shards的社区。在我们核心基础设施使用如此新和不确定的项目使得我们没有安全感。第二,多session factories的策略本来就不具备扩展性:你需要为你的每个新加入的分区生成一个session factory。如果你变得像myspace那样成功,你将需要上百个session factory。根据文献所说的那样,多session factories是资源密集型应用(消耗大量的资源),这一点我们不会觉得舒服(这也是我们上面的想法的硬伤)。最后,我们查询了hibernate shards的文档,它对于如何与spring集成和配置的说明并不清楚,那么,springlocalSessionFactoryBean将无法工作。我不太喜欢深入spring的事务基础设施来创建一个ShardsSessionFactoryBean来合适地集成这种想法的事务管理。因此,我们决定采用routing-datasource的方法。

实现

我将带领你思考我们如何实现,已经它的优点和不足。首先是GLUD数据库。这个数据库包含了master_user表,他包含了在所有分区的所有用户的主键和邮箱地址。事实上,它包括了一个用户的所有唯一约束属性,也是数据库的唯一性约束可以应用的地方,但是在这里我们假设我们以email作为唯一约束。给定一个用户的email地址,master_user表可以用于定位用户表的主键。另外一个表示partition_map,它包括了一个用户主键的hash到一个分区id的映射。所以,如果你有一个用户的主键,那么就可以在partition_map中查找分区。我们所使用的hash函数是主键的最后三位数字,随后我们分配一千个虚拟分区。物理分区的数目可以是11000之间。例如,如果你只有两个物理分区,那么你可以映射分区000-499user database 1500-999user database 2(或者你可以采用奇偶数的方法)。现在的问题是,你有用户的主键和email地址,你能够知道用户数据的数据库的分区id

那么,谁来负责做分区定位的计算呢?我们编写了一个spring aop的拦截器来包装所有用于我们的分区数据库的services。拦截器可以使用GLUD数据库(通过中间的GludService)来确定路由到哪个分区。最后问题在于拦截器如何知道目前的操作关联到哪个用户。因此我们约定,每个方法的第一个参数可以识别用户:它应该是用户对象本身或者是用户主键或email。以上的这些将会帮助我们确定数据存在哪个分区。然而这是有漏洞的:在现有的分区系统中,使用分区数据库的service只能使用愚蠢的方法签名,它们不是类型安全的。下面是拦截器中方法的大致实现:

public Object selectExistingPartitionWithUser(ProceedingJoinPoint jp, LocatePreexistingUser annotation, User user) throws Throwable 
    {
        GludEntry gludEntry = getGludService().getGludEntryForExistingUser(user);
        int partitionNumber = gludEntry.getDatabasePartition();
        datasourceNumberCache.set(partitionNumber);
        Object returnValue = null;
        try
        {
            returnValue = jp.proceed();
        }
        finally
        {
            datasourceNumberCache.remove();
        }
        return returnValue;
    }

 

 

这里datasourceNumberCache是一个pubilc static final ThreadLocal<Integer>,它维护了本次操作所关联的用户所在的分区id。谁将读取这个ThreadLocal将在后文介绍。

 

 

我们使用了AspectJ切入点语言来描述我们的切入点(pointcut).这将使得我们可以为我们的拦截器使用类型安全的方法签名,正如你在上文看到的那样(没有Method对象或者Object对象这样的参数)。我们也发现许多不同类型的拦截也是必要的。上文我们看到了最简单的情况,寻找与用户关联的数据。但是当用户更新他的email(或者别的存在GLUD中的唯一性字段)?那么新建一个用户呢?某个操作需要广播到所有分区呢(计算上周某个用户创建的内容总数)?如果我们需要加载所有用户生成的内容来进行索引,批量加载呢?以上所有操作都要求在拦截器中编写不同的方法。如何使拦截器绑定不同的方法到不同的service呢?

对于这种情况我们使用了注解。你可以看到注解实例通过spring集成设施进入到方法签名之上。下面你就可以看到方法的切入点:

spring的官方文档和AspectJ的官网来完全理解这段代码的意思,当然它主要指绑定到所有以LocatePreexistingUser为注解的方法,和以User 对象作为第一参数的方法。argNames项是必要的,它能正确获得注解并传递User对象。我记得曾经比较糟糕的是当多于一个参数绑定到切入点上时,那很难运行正常,直到我偶然发现argNames参数。

@Around(value="@annotation(annotation) && args(user, ..)", argNames="annotation,user")

 你可以通过

 

使用注解很爽的地方是你可以将数据从被注解的方法传递到拦截器。例如,下面是如上注解的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LocatePreexistingUser
{
    public UserIdentifier userIdentifier() default USER_OBJECT;
    public boolean userUpdate() default false;
}

 

 

 在这里, UserIdentifier是一个值为USER_OBJECTEMAILUSER_PK的枚举,如果你更新GLUD中具有唯一约束的字段,如email,你可以在你的ProfileService中使用如下注解:

 

@LocatePreexistingUser(userUpdate=true)
public void updateEmail(User user, String newEmail) 
{ ... }

 

 那么,拦截器将像下面那样:

if(annotation.userUpdate)
{
    // tell GLUD service to update its master_user record
}

 

这的确很nice。你可以传递信息到拦截器,它将告诉拦截器如何处理该方法的调用,并且注解指定了正确的方法定义。同样的,我认为这很nice

 

hibernate已经准备好发生sql到数据库了,它调用datasource获得一个连接。PartitionRoutingDataSourceThreadLocal中读取出分区id,然后返回指向该数据库的连接。它继承了SpringAbstractRoutingDataSource,其代码大致如下:

 

protected Object determineCurrentLookupKey()
    {
        Integer datasourceNumber = 
DatasourceSwitchingAspect.datasourceNumberCache.get();
        return datasourceNumber;
    }

 

Spring的配置中配置了两个普通的数据源(以两个分区为例子),user1DataSourceuser2DataSource。它们是标准的数据源指向物理的数据库(使用jboss连接池,通过jndi查找)。那么,我们供给Hibernate session factory的数据源配置应该是这样的:

<bean id="userDataSource" class="PartitionRoutingDataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.Integer">
                <entry key="1" value-ref="user1DataSource"/>
                <entry key="2" value-ref="user2DataSource"/>
            </map>
        </property>
    </bean>

 

 

 Spring创建的session factory应该是这样的:

 

 

<bean id="userSessionFactory" 
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
        <property name="dataSource" ref="userDataSource"/>
        etc 
    </bean>

 

 

这里没有什么特别的。为了能够使用多个数据库分区,简单地在应用服务器上配置连接池,把数据源加入到spring中,然后添加引用到PartitionRoutingDataSource中。完成了!最美好的事情是你可以增加任意的数据库分区而不需要创建数目众多的session factory

 

 

还有一些其他的事情需要我们去担忧。当hibernate获取到一个连接的时候,你想确认在你开启一个spring事务之前你已经设置好了分区id。换句话说,你需要确认     DatasourceSwitchingAspect 中的order属性值比事务拦截器中的低。这里,DatasourceSwitchingAspectorder设置为1,事务拦截器的order属性设置为2

<aop:config>
        <aop:pointcut id="profileServicePointcut" expression="execution(* *..ProfileService.*(..))"/>
        <aop:advisor advice-ref="userTxAdvice" pointcut-ref="profileServicePointcut" order="2"/>        
    </aop:config>

 

 

userTxAdvice是我们以前在spring使用的事务通知(advice)。事务管理器是一个普通的HibernateTransactionManager

 

 

Mark Fisher's的博客中其中一条评论中指出,配置的顺序可能会引起hibernate二级缓存的混乱,除非id分开存储。我们考虑了几种办法来解决它,如分派一个范围给每个数据库。但是DBA们比较倾向于一个high-low的表存在GLUD中,因此,我们考虑去实现它。Hibernate有一个high-low的主键生成策略,但是它会认为你插入数据的表在同一个数据库中,但是我们的主键都存在GLUD数据库中。为了不编写我们自己的high-low主键生成策略,我们需要编写一个hibernate主键生成器的包装类。该包装类简单的钩取(grabs)GLUDsession factory来发送主键生成器。GLUD session来自于ApplicationContextAware单例对象,它维持了spring应用上下文的引用,然后在必要的时候钩取GLUD session。由于hibernate在一个我们所不知道的地方创建了主键生成器,因此gludSessionFactory不能通过spring进行依赖注入。

public class UserDbIdGenerator implements IdentifierGenerator, Configurable
{
    private MultipleHiLoPerTableGenerator generator; 
    
    public ProfileIdGenerator()
    {
        generator = new MultipleHiLoPerTableGenerator();        
    }

    public Serializable generate(SessionImplementor profileSession, Object entity) throws HibernateException
    {
        SessionFactory gludSessionFactory = getGludSessionFactory();
        Session gludSession = gludSessionFactory.openSession();
        Transaction txn = gludSession.beginTransaction();
        
        // Pass through to the wrapped id generator
        Long key = (Long) generator.generate((SessionImplementor) gludSession, entity);
        
        txn.commit();
        gludSession.close();
        return key;
    }

    protected SessionFactory getGludSessionFactory()
    {
        SessionFactory sessionFactory =
 SpringContextSingleton.getInstance().getBean("gludSessionFactory");
        return sessionFactory;
    }

    public void configure(Type type, Properties props, Dialect dialect) throws MappingException
    {
        generator.configure(type, props, dialect);
    }
    
}

 

  在我们的hibernate映射文件中,对象应该使用这个主键生成器类:

 

<class name="Foo" table="foo">
        <id name="id" column="id">
            <generator class="UserDbIdGenerator">
                <param name="primary_key_value">foo</param>
                <param name="max_lo">5000</param>
            </generator>
        </id>
    </class>

 

 

问题

总体来说,这个分区模型运行得很不错。它运行在生产环境中并且性能表现良好。然而如果你要将分区应用到你的应用程序中,我对你有一些建议。

二级缓存

我们的hibernate二级缓存存在相当数量的小故障。简言之,一个hibernate session factory(我们绝对相信它是连到一个数据库),它与多个数据库一起工作就会充满了危险。对于对象缓存,一般没有问题,因为我们的id是唯一的,并且FOO#1只能在一个分区上被发现。然而,查询缓存是一个噩梦。

比方说,你发出查询:“给我从上周开始所有的blog实体”,首先,查询在分区1上面运行,结果他们被缓存在查询缓存。接下来拦截器尝试在分区2上面运行查询,但是由于之前有缓存,因此之前的缓存结果被返回。有些对象不在分区2中,但是它们在缓存中,因此,你会受到欺骗:你的所有返回的对象都来自于分区1,而没有任何对象来自于之后的分区。

一般说来,你能操作属于分区N   session,但是使用存储在分区M上的对象(因为你在二级缓存中找到了它们),如果你要去数据库中更新这些对象,那么你可能会出错,因为你找到了错误的数据库。

如果你采用shards的方式,使用单一session factory,每个数据库一个二级缓存,那么这些问题将不复存在。

JTA

分区用户数据库和GLUD数据库的系统是一个单元:你不希望事务在一个数据库中提交了,但在另一个数据库中没能提交。如果你想把它们包装在一个事务中,你可能需要使用JTA。我不确信JTA在这种情况下可以运行良好。想象这样的情景:你开启了一个JTA事务,然后使用hibernate session facory对数据库分区1进行操作。如果你进行了更新,hibernate维持该sql直到事务完成。现在,在同一个JTA事务,你连接到分区2.,我们可以看到2个不好的事情发生:1hibernate说:在这个会话中,我已经有一个分区1的连接了。2)当事务提交时,这个session维持了两个分区的sql,它知道发送到哪个分区吗?

       Hibernate的官方文档中有只言片语引导我们:可以配置hibernate会一个一个地发出sql并且释放连接,不过我没有查证这个。我曾经试图在我的应用程序中创建一个JTA事务管理器,但是不能让它运行(springJtaTransactionManager拒绝去查找Jboss中的事务管理器)。我花了大概一小时,然后我才知道大概问题所在(classpath中重复的jta.jar)。

       此外,在一个单session factory per database分区风格中,JTA应该会正常运行。

测试

当涉及到分区的时候,测试变得很痛苦(并且它不仅针对于我们的分区风格)。我们为分区数据库形成了两种测试方案:一种是ROIT(regular old integration tests),测试dao面向单一分区。随后我们有分区集成测试,使用GLUD和分区。你至少应该编写测试类来测试拦截器,路由datasource和所有的xml配置,来确定它们运行恰当。但是你需要创造力来为这些测试类创建应用程序上下文,来避免手动初始化对象或者编写重复的配置文件。使用DBUnits是一个小小的挑战,因为你通常需要插入或更新GLUD和分区中的数据(或者更多)。

共享对象

最后,更痛苦的一点是,对象在多个分区之间共享是一团糟。假设我们的Blog对象有一个Category属性,它是多对多关系,如果你希望Category对象和Blog在同一数据库中,它们需要存在每一个分区数据库中,因此,category(id=1,name=’java’)将出现在两个分区数据库中,当它们被加载到二级缓存中,它们将出现冲突,你可以关闭缓存来避免这种情况,关闭乐观锁,将它们放到另外的数据库中(GLUD?),不过,如果你有多个session factory(并且二级缓存是分开的)这样的情况不会如此糟糕。

概述

我希望你发现这篇有趣的描述。如果你即将尝试进行分区,你可以考虑使用上文的方法。但是一定要知道以上的问题:二级缓存(可能还有JTA)将不会很好的工作,每个数据库对应一个session factory的策略将消耗更多的资源(并且将降低应用程序的启动速度),但或许可以解决这两个问题。

我想如果我重新开始,我愿意回答多session factory 的方式,或者我再看看hibernate shards怎么样了,那或许要更好。我想当上面的情况改变时我可能会改变我之前的实现方法。不过,通过这些技术实现分区是一件多有趣和有挑战的事情啊。希望大家分享你自己的经验。

6
2
分享到:
评论

相关推荐

    struts2+spring+hibernate源码(oracle数据库)

    这个压缩包提供了一个使用SSH(Struts2、Spring、Hibernate)和Oracle数据库的示例项目,名为"Myssh2",对于初学者来说,这是一个很好的学习资源。 **Struts2** 是一个MVC(Model-View-Controller)框架,负责处理...

    spring集合hibernate多数据切换

    Spring框架和Hibernate作为Java领域最常用的两大技术,它们的结合使用可以极大地提升开发效率和应用性能。本知识点主要探讨如何在Spring中集成Hibernate来实现多数据源的动态切换功能,这对于需要处理多种数据源的...

    毕设bbs论坛(Spring+Structs+Hibernate框架开发).zip

    总的来说,毕设BBS论坛项目利用Spring、Struts 2和Hibernate框架,构建了一个完整的、功能丰富的Web应用程序。通过这个项目,学生可以深入理解三大框架的协同工作原理,提升Java Web开发技能。同时,这也是一个很好...

    SpringMVC4+Spring4+Hibernate5+MySQL5

    Spring4还强化了与其他框架如Hibernate的协作,使得数据库操作更加便捷。 **Hibernate5**: Hibernate是一个流行的Java ORM(对象关系映射)框架,它简化了Java应用与数据库之间的交互。Hibernate5引入了新的特性,...

    SSH框架实现增删改查,Oracle数据库

    同时,使用数据库管理工具如PL/SQL Developer或Navicat可以帮助直观地查看和操作数据库。 8. **测试**:完成编码后,可以通过JUnit进行单元测试,确保每个方法都能正常工作。在部署到服务器之前,进行集成测试和...

    Spring + Struts +Hibernate+Oracle 教程有文档及源码

    Spring、Struts、Hibernate和Oracle是Java开发中常用的四大技术框架,它们的组合常被称为SSH+Oracle架构。这个教程提供了一整套从理论到实践的学习资料,帮助开发者深入理解这四大框架如何协同工作,构建高效的企业...

    spring1.2+hibernate2对大字段的处理实例

    总之,处理大字段是Java企业级应用开发中的常见挑战,通过合理的数据模型设计、数据库优化和框架配置,我们可以有效地管理和操作这些大数据。这个"spring1.2+hibernate2对大字段的处理实例"就是一个很好的学习资源,...

    移动ssh项目(struts+spring+hibernate+oracle)130222.zip

    SSH是三个首字母缩写的组合,分别代表Struts、Spring和Hibernate,这是一套常见的Java Web开发框架。在这个"移动SSH项目"中,这些技术被整合在一起,用于构建基于JSP的动态网页应用,同时利用Oracle数据库进行数据...

    JSP源码——移动ssh项目(struts+spring+hibernate+oracle).zip

    SSH(Struts、Spring、Hibernate)是Java开发中的经典MVC框架组合,尤其在企业级应用中广泛使用。在这个名为“移动ssh项目”的源码中,我们将深入探讨这四个组件如何协同工作,构建出强大的移动应用程序。 首先,...

    移动ssh项目(struts+spring+hibernate+oracle)130222.rar

    移动SSH项目是一个基于Java技术栈的Web应用,其核心框架包括Struts、Spring和Hibernate,同时结合Oracle数据库进行数据存储。这个项目可能是针对移动通信领域的,考虑到“ChinaMobile”这一名称,很可能涉及到中国...

    springmvc hibernate oracle

    Hibernate作为ORM框架,允许开发者以对象的方式操作数据库,避免了SQL的直接编写,提高了开发效率。它通过配置文件或注解定义实体类与数据库表的映射关系,实现了对象和数据的透明化转换。Hibernate提供了Session...

    HIBERNATE:Hibernate 学习一--注解方式自动建表

    - Hibernate支持编程式事务管理和声明式事务管理,可以使用Transaction接口进行编程式事务控制,或者在Spring等框架中使用@Transactional注解进行声明式事务。 6. **查询方式**: - HQL(Hibernate Query ...

    spring hibernate BoneCP設定

    通过以上配置,Spring应用就能使用BoneCP连接池来管理和分配数据库连接,同时也能与Hibernate无缝集成,实现高效、自动化的数据库操作。这样的设置有助于提高系统的可扩展性和性能,特别是在高并发场景下。

    论坛系统(Struts 2+Hibernate+Spring实现)

    此外,Spring与Hibernate的集成使得我们可以轻松地在业务层使用Hibernate的DAO操作,而无需关注具体的数据库连接细节。 **系统架构** 在这个论坛系统中,通常会包含以下组件: 1. 用户模块:用于用户注册、登录、...

    Spring,SpringMVC,Hibernate,Oracle知识汇总

    Spring还提供了对数据库访问的支持,包括JDBC抽象层和集成ORM框架,如Hibernate。 **SpringMVC**:Spring MVC是Spring框架的一部分,用于构建Web应用程序。它采用模型-视图-控制器(Model-View-Controller,MVC)...

    物流项目 4大模块 j2ee struts spring hibernate oracle

    Hibernate是一个对象关系映射(Object-Relational Mapping,ORM)框架,它允许Java开发者以面向对象的方式操作数据库。Hibernate简化了数据访问层的编写,通过自动处理SQL语句和结果集的映射,避免了大量手动的JDBC...

    est2.1+Struct2.0+Spring2.0+hibernate3.1+oracle10g的例子

    这是一个基于老旧技术栈的Web应用开发实例,涵盖了四个主要的技术:Est2.1、Struts2.0、Spring2.0和Hibernate3.1,同时使用Oracle10g作为数据库。下面将对这些技术及其相互关系进行详述。 **Est2.1**:Est...

    Springmvc5.0+Hibernate 5.2.12 + mysql jar整合

    在IT行业中,Spring MVC、Hibernate和MySQL是三个非常重要的开源框架和数据库系统,它们共同构成了许多企业级应用的基础。下面将详细介绍这三个技术以及如何将它们整合在一起。 **Spring MVC** Spring MVC是Spring...

    SpringBatch批处理 刘相编

    以及Spring Batch框架中经典的三步走策略:数据读、数据处理和数据写,详尽地介绍了如何对CVS格式文件、JSON格式文件、XML文件、数据库和JMS消息队列中的数据进行读操作、处理和写操作,对于数据库的操作详细介绍了...

Global site tag (gtag.js) - Google Analytics