Chinaunix首页 | 论坛 | 博客
  • 博客访问: 925071
  • 博文数量: 264
  • 博客积分: 10107
  • 博客等级: 上将
  • 技术积分: 2455
  • 用 户 组: 普通用户
  • 注册时间: 2007-05-09 16:34
文章分类

全部博文(264)

文章存档

2012年(1)

2011年(11)

2010年(128)

2009年(82)

2008年(42)

我的朋友

分类: 系统运维

2010-12-28 17:39:41

在我们的项目中遇到这样一个问题:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。我们以往在 spring hibernate 框架中总是配置一个数据源,因而 sessionFactory dataSource 属性总是指向这个数据源并且恒定不变,所有 DAO 在使用 sessionFactory 的时候都是通过这个数据源访问数据库。但是现在,由于项目的需要,我们的 DAO 在访问 sessionFactory 的时候都不得不在多个数据源中不断切换,问题就出现了:如何让 sessionFactory 在执行数据持久化的时候,根据客户的需求能够动态切换不同的数据源?我们能不能在 spring 的框架下通过少量修改得到解决?是否有什么设计模式可以利用呢?   

问题的分析

我首先想到在 spring applicationContext 中配置所有的 dataSource 。这些 dataSource 可能是各种不同类型的,比如不同的数据库: Oracle SQL Server MySQL 等,也可能是不同的数据源:比如 apache 提供的 org.apache.commons.dbcp.BasicDataSource spring 提供的 org.springframework.jndi.JndiObjectFactoryBean 等。然后 sessionFactory 根据客户的每次请求,将 dataSource 属性设置成不同的数据源,以到达切换数据源的目的。

但是,我很快发现一个问题:当多用户同时并发访问数据库的时候会出现资源争用的问题。这都是“单例模式”惹的祸。众所周知,我们在使用 spring 框架的时候,在 beanFactory 中注册的 bean 基本上都是采用单例模式,即 spring 在启动的时候,这些 bean 就装载到内存中,并且每个 bean 在整个项目中只存在一个对象。正因为只存在一个对象,对象的所有属性,更准确说是实例变量,表现得就如同是个静态变量(实际上“静态”与“单例”往往是非常相似的两个东西,我们常常用“静态”来实现“单例”)。拿我们的问题来说, sessionFactory 在整个项目中只有一个对象,它的实例变量 dataSource 也就只有一个,就如同一个静态变量一般。如果不同的用户都不断地去修改 dataSource 的值,必然会出现多用户争用一个变量的问题,对系统产生隐患。

通过以上的分析,解决多数据源访问问题的关键,就集中在 sessionFactory 在执行数据持久化的时候,能够通过某段代码去根据客户的需要动态切换数据源,并解决资源争用的问题。

问题的解决

(一)            采用 Decorator 设计模式

要解决这个问题,我的思路锁定在了这个 dataSource 上了。如果 sessionFactory 指向的 dataSource 可以根据客户的需求去连接客户所需要的真正的数据源,即提供动态切换数据源的功能,那么问题就解决了。那么我们怎么做呢?去修改那些我们要使用的 dataSource 源码吗?这显然不是一个好的方案,我们希望我们的修改与原 dataSource 代码是分离的。根据以上的分析,使用 GoF 设计模式中的 Decorator 模式(装饰者模式)应当是我们可以选择的最佳方案。

什么是“ Decorator 模式”?简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个 Decorator 套在原有代码外面。当我们使用 Decorator 的时候与原类完全一样,当 Decorator 的某些功能却已经修改为了我们需要修改的功能。 Decorator 模式的结构如图。

我们本来需要修改图中所有具体的 Component 类的一些功能,但却并不是去直接修改它们的代码,而是在它们的外面增加一个 Decorator Decorator 与具体的 Component 类都是继承的 AbstractComponent ,因此它长得和具体的 Component 类一样,也就是说我们在使用 Decorator 的时候就如同在使用 ConcreteComponentA 或者 ConcreteComponentB 一样,甚至那些使用 ConcreteComponentA 或者 ConcreteComponentB 的客户程序都不知道它们用的类已经改为了 Decorator ,但是 Decorator 已经对具体的 Component 类的部分方法进行了修改,执行这些方法的结果已经不同了。

(二)            设计 MultiDataSource

现在回到我们的问题,我们需要对 dataSource 的功能进行变更,但又不希望修改 dataSource 中的任何代码。我这里指的 dataSource 是所有实现 javax.sql.DataSource 接口的类,我们常用的包括 apache 提供的 org.apache.commons.dbcp.BasicDataSource spring 提供的 org.springframework.jndi.JndiObjectFactoryBean 等,这些类我们不可能修改它们本身,更不可能对它们一个个地修改以实现动态分配数据源的功能,同时,我们又希望使用 dataSource sessionFactory 根本就感觉不到这样的变化。 Decorator 模式就正是解决这个问题的设计模式。

首先写一个 Decorator 类,我取名叫 MultiDataSource,通过它来动态切换数据源 。同时在配置文件中将sessionFactory的dataSource属性由原来的某个具体的dataSource改为MultiDataSource。如图:

对比原Decorator 模式, AbstractComponent 是一个抽象类,但在这里我们可以将这个抽象类用接口来代替,即 DataSource 接口,而 ConcreteComponent 就是那些 DataSource 的实现类,如 BasicDataSource JndiObjectFactoryBean 等。 MultiDataSource 封装了具体的dataSource,并实现了数据源动态切换:

java 代码

  1. public   class  MultiDataSource  implements  DataSource {   
  2.      private  DataSource dataSource =  null ;   
  3. public  MultiDataSource(DataSource dataSource){   
  4.          this .dataSource = dataSource;   
  5.     }   
  6.      /* (non-Javadoc)  
  7.      * @see javax.sql.DataSource#getConnection()  
  8.      */   
  9.      public  Connection getConnection()  throws  SQLException {   
  10.          return  getDataSource().getConnection();   
  11.     }   
  12.      //其它DataSource接口应当实现的方法   
  13.   
  14.      public  DataSource getDataSource(){   
  15.          return   this .dataSource;   
  16.         }   
  17.     }   
  18.      public   void  setDataSource(DataSource dataSource) {   
  19.          this .dataSource = dataSource;   
  20.     }   
  21. }   

客户在发出请求的时候,将dataSourceName放到request中,然后把request中的数据源名通过调用new MultiDataSource(dataSource) 时可以告诉 MultiDataSource 客户需要的数据源,就可以实现动态切换数据源了。但细心的朋友会发现这在单例的情况下就是问题的,因为 MultiDataSource 在系统中只有一个对象,它的实例变量 dataSource 也只有一个,就如同一个静态变量一般。正因为如此, 单例模式让许多设计模式都不得不需要更改,这将在我的《“单例”更改了我们的设计模式》中详细讨论。那么,我们在单例模式下如何设计呢?

(三)            单例模式下的 MultiDataSource

在单例模式下,由于我们在每次调用 MultiDataSource 的方法的时候, dataSource 都可能是不同的,所以我们不能将 dataSource 放在实例变量 dataSource 中,最简单的方式就是在方法 getDataSource() 中增加参数,告诉 MultiDataSource 我到底调用的是哪个 dataSource

java 代码
  1. public  DataSource getDataSource(String dataSourceName){   
  2.         log.debug( "dataSourceName:" +dataSourceName);   
  3.          try {   
  4.              if (dataSourceName== null ||dataSourceName.equals( "" )){   
  5.                  return   this .dataSource;   
  6.             }   
  7.              return  (DataSource) this .applicationContext.getBean(dataSourceName);   
  8.         } catch (NoSuchBeanDefinitionException ex){   
  9.              throw   new  DaoException( "There is not the dataSource 
  10.         }   
  11.     }   

值得一提的是,我需要的数据源已经都在spring 的配置文件中注册, dataSourceName 就是其对应的 id

xml 代码
  1. < bean   id = "dataSource1"   
  2.      class = "org.apache.commons.dbcp.BasicDataSource" >   
  3.      < property   name = "driverClassName" >   
  4.          < value > oracle.jdbc.driver.OracleDriver value >   
  5.      property >  
  6.     ......   
  7. bean >   
  8. < bean   id = "dataSource2"   
  9.      class = "org.apache.commons.dbcp.BasicDataSource" >   
  10.      < property   name = "driverClassName" >   
  11.          < value > oracle.jdbc.driver.OracleDriver value >  
  12.      property >     
  13.     ......   
  14. bean >     

为了得到spring ApplicationContext MultiDataSource 类必须实现接口 org.springframework.context.ApplicationContextAware ,并且实现方法:

java 代码
  1. private  ApplicationContext applicationContext =  null ;   
  2. public   void  setApplicationContext(ApplicationContext applicationContext)  throws  BeansException {   
  3.          this .applicationContext = applicationContext;   
  4.     }   

如此这样,我就可以通过this . applicationContext .getBean(dataSourceName) 得到 dataSource 了。

(四)            通过线程传递 dataSourceName

查看以上设计, MultiDataSource 依然无法运行,因为用户在发出请求时,他需要连接什么数据库,其数据源名是放在 request 中的,要将 request 中的数据源名传给 MultiDataSource ,需要经过 BUS DAO ,也就是说为了把数据源名传给 MultiDataSource BUS DAO 的所有方法都要增加 dataSourceName 的参数,这是我们不愿看到的。写一个类,通过线程的方式跳过 BUS DAO 直接传递给 MultiDataSource 是一个不错的设计:

java 代码
  1. public   class  SpObserver {   
  2.      private   static  ThreadLocal local =  new  ThreadLocal();   
  3.      public   static   void  putSp(String sp) {   
  4.         local.set(sp);   
  5.     }   
  6.      public   static  String getSp() {   
  7.          return  (String)local.get();   
  8.     }   
  9. }   

做一个filter ,每次客户发出请求的时候就调用 SpObserver.petSp ( dataSourceName ) ,将 request 中的 dataSourceName 传递给 SpObserver 对象。 最后修改 MultiDataSource 的方法 getDataSource()

java 代码
  1. public  DataSource getDataSource(){   
  2.         String sp = SpObserver.getSp();   
  3.          return  getDataSource(sp);   
  4.     }   

完整的MultiDataSource 代码在附件中。

(五)            动态添加数据源

通过以上方案,我们解决了动态分配数据源的问题,但你可能提出疑问:方案中的数据源都是配置在 spring ApplicationContext 中,如果我在程序运行过程中动态添加数据源怎么办?这确实是一个问题,而且在我们的项目中也确实遇到。 spring ApplicationContext 是在项目启动的时候加载的。加载以后,我们如何动态地加载新的 bean ApplicationContext 中呢?我想到如果用 spring 自己的方法解决这个问题就好了。所幸的是,在查看 spring 的源代码后,我找到了这样的代码,编写了 DynamicLoadBean 类,只要 调用loadBean() 方法,就可以将某个或某几个配置文件中的 bean 加载到 ApplicationContext 中(见附件)。不通过配置文件直接加载对象,在 spring 的源码中也有,感兴趣的朋友可以自己研究。

(六)            spring 中配置

在完成了所有这些设计以后,我最后再唠叨一句。我们应当在 spring 中做如下配置:

xml 代码
  1. < bean   id = "dynamicLoadBean"   class = "com.htxx.service.dao.DynamicLoadBean" > bean >   
  2. < bean   id = "dataSource"   class = "com.htxx.service.dao.MultiDataSource" >   
  3.          < property   name = "dataSource" >   
  4.              < ref   bean = "dataSource1"   />   
  5.          property >   
  6.      bean >   
  7.      < bean   id = "sessionFactory"   class = "org.springframework.orm.hibernate3.LocalSessionFactoryBean" >   
  8.          < property   name = "dataSource" >   
  9.              < ref   bean = "dataSource"   />   
  10.          property >   
  11.         ......   
  12.      bean >   

其中dataSource 属性实际上更准确地说应当是 defaultDataSource ,即 spring 启动时以及在客户没有指定数据源时应当指定的默认数据源。

该方案的优势

 

以上方案与其它方案相比,它有哪些优势呢?

首先,这个方案完全是在 spring 的框架下解决的,数据源依然配置在 spring 的配置文件中, sessionFactory 依然去配置它的 dataSource 属性,它甚至都不知道 dataSource 的改变。唯一不同的是在真正的 dataSource sessionFactory 之间增加了一个 MultiDataSource

其次,实现简单,易于维护。这个方案虽然我说了这么多东西,其实都是分析,真正需要我们写的代码就只有 MultiDataSource SpObserver 两个类。 MultiDataSource 类真正要写的只有 getDataSource() getDataSource(sp) 两个方法,而 SpObserver 类更简单了。实现越简单,出错的可能就越小,维护性就越高。

最后,这个方案可以使单数据源与多数据源兼容。这个方案完全不影响 BUS DAO 的编写。如果我们的项目在开始之初是单数据源的情况下开发,随着项目的进行,需要变更为多数据源,则只需要修改 spring 配置,并少量修改 MVC 层以便在请求中写入需要的数据源名,变更就完成了。如果我们的项目希望改回单数据源,则只需要简单修改配置文件。这样,为我们的项目将增加更多的弹性。

特别说明:实例中的 DynamicLoadBean在web环境下运行会出错,需要将类中AbstractApplicationContext改为 org.springframework.context.ConfigurableApplicationContext。

相关博客:


在前面我写了《如何在》,通过设计模式中的Decorator模式在spring框架中解决多数据源的问题,得到了许多网友的关注。在与网友探讨该问题的过程中,我发现我的方案并不完善,它只解决了一部分问题。

总结多数据源的问题,其实它需要分为以下三种情况:各个数据源的数据结构不同、各个数据源的数据结构相同、各个数据源的数据结构部分相同又有部分不同。对于第二种情况,各个数据源的数据结构相同,我们使用一个sessionFactory,而在sessionFactory中通过MultiDataSource来动态切换数据源,应当是一个不错的方案,既解决了多个sessionFactory对相同的值对象重复装载对内存的浪费,又使数据源的切换对客户程序透明,简化了代码的实现和对客户程序的影响。但是,对于第一种情况,各个数据源的数据结构不同,运用这样的方案存在潜在风险。

对于各个数据源的数据结构不同的情况,使用一个sessionFactory而在这个sessionFactory中动态切换数据源,可能造成数据访问的张冠李戴。譬如,数据源A有表T而数据源B没有,可能造成客户程序在访问表T的时候却尝试去连接数据源B,因为客户程序访问哪个数据源是在程序运行期间由客户程序决定的,因此这样的错误是很难发现的。也许客户程序的一个不经意的错误就可能造成错误。解决这个问题的方法有两个:一是严格要求客户程序不要写错,这当然是可以做到的,但作为框架设计者,另一个解决方法是在框架中就避免出现这样的情况。因此我祭出了MultiSessionFactory的方案来解决各个数据源的数据结构不同的多数据源问题。

问题的分析

MultiDataSource的方案一样,MultiSessionFactory同样是在spring框架下调用ApplicationContextgetBean()方法而不会另外创建beanFacoty,也同样使用Decorator模式来处理切换的问题。MultiSessionFactory的对象关系如图:

在该方案中,SessionFactory就是Hibernateorg.hibernate.SessionFactory接口,Decorator就是MultiSessionFactorySessionFactory1SessionFactory2往往是springorg.springframework.orm.hibernate3.LocalSessionFactoryBean。细心的朋友可能会注意,实际上LocalSessionFactoryBean并不是SessionFactory的实现,这个方案是否有问题呢?这个问题其实也一直困扰了我好久,最后我发现,我们通过ApplicationContextgetBean()得到一个LocalSessionFactoryBean的时候其实并不是真正地得到了它,而是得到了一个SessionFactory,因为springLocalSessionFactoryBean重写了getObject(),使其返回的是SessionFactory。一个简单的明证就是,HibernateDaoSupportsessionFactory属性的类型是SessionFactory,而我们在spring配置的时候注入的却是LocalSessionFactoryBean

方案的实现

在整个这个方案中,我们需要实现的只有MultiSessionFactory类和我们可爱的Spserver,总共就两个类,然后呢就是一些spring的配置,就完成了。

MultiSessionFactory实现了SessionFactory,同时为了得到AplicationContext而实现了ApplicationContextAwareMultiSessionFactory的代码如下:

java 代码
  1. public class MultiSessionFactory implements SessionFactory, ApplicationContextAware {   
  2.     private static final long serialVersionUID = 2064557324203496378L;   
  3.     private static final Log log = LogFactory.getLog(MultiSessionFactory.class);   
  4.     private ApplicationContext applicationContext = null;   
  5.     private SessionFactory sessionFactory = null;   
  6.     public ApplicationContext getApplicationContext() {   
  7.         return applicationContext;   
  8.     }   
  9.     public void setApplicationContext(ApplicationContext applicationContext) {   
  10.        this.applicationContext = applicationContext;   
  11.     }   
  12.     public SessionFactory getSessionFactory(String sessionFactoryName) {   
  13.        log.debug("sessionFactoryName:"+sessionFactoryName);   
  14.        try{   
  15.            if(sessionFactoryName==null||sessionFactoryName.equals("")){   
  16.               return sessionFactory;   
  17.            }   
  18.            return (SessionFactory)this.getApplicationContext().getBean(sessionFactoryName);   
  19.        }catch(NoSuchBeanDefinitionException ex){   
  20.            throw new DaoException("There is not the sessionFactory 
  21.        }   
  22.     }   
  23.   
  24.     public SessionFactory getSessionFactory() {   
  25.        String sessionFactoryName = SpObserver.getSp();   
  26.        return getSessionFactory(sessionFactoryName);   
  27.     }   
  28.   
  29.     public void setSessionFactory(SessionFactory sessionFactory) {   
  30.        this.sessionFactory = sessionFactory;   
  31.     }   
  32.   
  33.     // SessionFactory接口需要实现的方法   
  34.   
  35. ......   
  36.   
  37. }  

MultiSessionFactory的完整代码见我提供的附件。setSessionFactory()实际上是设定的默认sessionFactory,它在spring装载的时候调用,其对应的数据源应当是主数据源,即项目初始化中需要读取初始化数据的数据源。在任何多数据源项目中,都应当有一个存放初始化数据、系统维护数据、用户权限数据的数据源,这就是主数据源。因此MultiSessionFactory的配置应当这样写:

xml 代码
  1. <bean id="sessionFactory" class="com.htxx.service.dao.MultiSessionFactory">  
  2.     <property name="sessionFactory"><ref bean="hostSessionFactory"/>property> 
  3. >  

SpServer的写法与《如何在中的一样,我就不再累赘了。

另外,在spring配置中配置多个数据源,每个数据源对应一个sessionFactory,这个对应的sessionFactory中的值对象应当是该数据源的值对象。客户程序在执行数据访问前,通过调用SpServerputSp()方法,告诉MultiSessionFactory需要切换到哪个sessionFactory,然后执行数据访问。这样,不同数据源的值对象通过放在不同的sessionFactory中,避免了张冠李戴的情况。具体的示例见附件的MultiSessionFactoryTest

另外的方案

也许有些朋友对以上方案还不满意,因为在执行数据访问前毕竟还要多做一步指定sessionFactory的工作。实际上,对于各个数据源的数据结构不同的项目,一个值对象应当使用哪个数据源有一个非常确定的对应关系。如果通过配置文件将值对象与它的sessionFactory对应起来,那么我们在执行数据访问的时候传递的是哪个值对象,MultiSessionFactory马上就可以去找到对应的sessionFactory。这个方案你可以通过AOP来制作一个拦截器拦截所有诸如save()delete()get()load()等方法来实现,也可以扩展HibernateDaoSupport来实现。这样的方案使客户程序甚至都不用知道他是在操作的一个多数据源系统。当然,这个方案感兴趣的朋友可以自己去实现。

另外,在这个方案中的核心是运用Decorator设计模式来解决切换sessionFactory的目的,即MultiSessionFactory的实现。至于通过什么方式来通知MultiSessionFactory应当切换到哪个SessionFactory,可以根据不同项目的情况自由选择。我在这里给大家提供了通过SpOberver和建立值对象与sessionFactory关系的配置文件这两个方案,你也可以有自己的方案解决。

第三种情况的解决方案

前面我已经给出了第一种和第二种情况的解决方案:各个数据源的数据结构不同的情况用MultiSessionFactory解决;各个数据源的数据结构相同的情况用MultiDataSource解决。那么第三种情况,各个数据源的数据结构部分相同又有部分不同,又应当如何解决呢?当然是将MultiSessionFactoryMultiDataSource结合起来解决。对于数据结构不同的部分,其分别创建各自的sessionFactory然后通过MultiSessionFactory来切换,而对于数据结构相同的部分,建立共同的sessionFactory和多个不同的dataSource然后通过MultiDataSource来切换就可以了。

还有的朋友问到这样的方案其事务处理和二级缓存的情况。这个方案是在spring框架下的解决方案,其事务处理的能力也是由spring的能力来决定的。目前spring要处理跨数据库的事务处理是通过JTA来实现的,这种方式在该方案中同样可以实现,朋友们可以试一试。另外,本方案能使用二级缓存吗?当然可以。对于MultiSessionFactory当然没有任何问题,它通过不同的sessionFactory分离开了不同的数据源和值对象,我们可以毫无顾忌地使用。对于MultiDataSource来说,就有点问题了。MultiDataSource使多个数据源使用共同的sessionFactory,因此它仿佛就是将多个数据源在逻辑上合并为一个数据源。正因为如此,我们需要保证对于同一个表在所有数据源中都要主键唯一。什么意思呢?数据源A和数据源B都有表T,如果数据源A中的表T拥有ID001的一条数据,那么在数据源B的表T中就不能有ID001的记录。如果你总是通过MultiDataSource来执行表的插入操作,并且使用uuid.hex生成主键,这当然不会有问题。但如果你有通过其它方式插入表的操作,你应当保证这样的唯一性。另外,对于查询的操作,缓存中存放的既可能是数据源A的数据,也可能是数据源B的数据,因此你应当对数据有一个规划。对于表T的数据,哪些应当插入到数据源A中,哪些应当插入到B中,应当有一个定义。假如是通过不同单位来决定插入哪个数据源,那么在查询数据源A的表T是,应当增加条件只查询数据源A应当有的单位而排除调其它单位。如此这样,你只要注意到这两个问题,你就可以放心大胆地使用二级缓存。
  • (16.4 KB)
  • 描述: 示例文件
  • 下载次数: 2015


先转载一下,以待后用

阅读(1395) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~