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

时间:2017-07-09 13:33

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

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

  我们继续。我们最初的想法是为每个分区创建一个Hibernate的session factory。假设我们有两个用户数据库,user1和user2.那么我们将有两个session factories,每个数据库对应一个。使用这些数据库的Service(例如ProfileService)将会为每个数据库创建一个实例。Profile1Service将关联到使用use1SessionFactory的profile1Dao.对于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集成和配置的说明并不清楚,那么,spring的localSessionFactoryBean将无法工作。我不太喜欢深入spring的事务基础设施来创建一个ShardsSessionFactoryBean来合适地集成这种想法的事务管理。因此,我们决定采用routing-datasource的方法。

  实现

  我将带领你思考我们如何实现,已经它的优点和不足。首先是GLUD数据库。这个数据库包含了master_user表,他包含了在所有分区的所有用户的主键和邮箱地址。事实上,它包括了一个用户的所有唯一约束属性,也是数据库的唯一性约束可以应用的地方,但是在这里我们假设我们以email作为唯一约束。给定一个用户的email地址,master_user表可以用于定位用户表的主键。另外一个表示partition_map,它包括了一个用户主键的hash到一个分区id的映射。所以,如果你有一个用户的主键,那么就可以在partition_map中查找分区。我们所使用的hash函数是主键的最后三位数字,随后我们分配一千个虚拟分区。物理分区的数目可以是1到1000之间。例如,如果你只有两个物理分区,那么你可以映射分区000-499到user database 1,500-999到user 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,它维护了本次操作所关联的用户所在的分区id。谁将读取这个ThreadLocal将在后文介绍。