Master-Slave,Spring,Hibernate,故事曲折离奇,情结跌宕起伏

/** 
*作者:张荣华 
*日期:2008-02-05 
**/ 

前言,这篇文章写于08年12月份,现在发布出来望同行点评 

------------------------------------------------------------ 

本文将要阐述或者讨论的是spring+hibernate和mysql的master-slave模式之间的一些不得不说的故事. 

那么开始之前,按照惯例,我们要介绍一下这个两个东西 
1,Hibernate,按照惯例,我们不介绍大家都知道的东西. 

2, Master-Slave:Mysql的Master-Slave模式是比较常用的数据库lb模型,Master负责数据更新,Slave负责数据读取,在需要备用数据库以及读操作大于写操作的场景下尤其多见.相信熟悉的人一定不在少数.按照惯例,论坛上多次出现的东西,我们只作简要叙述. 


在ahuaxuan之前的文章中,曾经多次提到mysql的master-slave,主要是为了解决在流行的ssh框架和master-slave可以无耦合的整合在一起的问题.我们的目的很简单,就是在无需改动我们的业务代码的前提下使用hibernate + master-slave. 

之前我们讲到,使用jee中使用master-slave可以有几种方式, 
1,mysql-proxy,和应用完全解耦 
2,replicationdriver或者使用replication协议头 
3,多数据源配置,使用多个jdbcTemplate或者多个hibernateTemplate等. 

第一点对应用是最透明的,第二点对业务代码是透明的,第三点需要我们的持久层有较大的改动.同样他们也有各自的优缺点,mysql-proxy在写操作频繁的时候会有一些小问题(一个朋友的公司出现过),replicationdriver和其他driver不能共存,replication协议头比较好,不过和hibernate好像不是很谈得来,而多数据源配置是下下策,ahuaxuan向来讨厌这种做法,如果管理不当,很容易出问题(是指出问题得概率比第一点和第二点大). 

以上三种方案对程序员得要求也是各不相同,mysql-proxy对程序员要求最高,replicationdriver或者协议头其次,多数据源最简单. 

Ahuaxuan没有尝试过mysql-proxy(因为生产环境早已存在,并且配置好,运维不会让人随便动),不过倒确实有尝试过replicationdriver和replication协议头(它们的本质都是一样的,都是使用ReplicationConnection),根据测试,replicationdriver和replication协议头在使用jdbc或者ibatis是没有什么问题,不过和hibernate在一起得时候就有问题了,mysql服务器cpu使用率无故飙高到80%,应用cpu也上升很多.怕怕+惶惶.而且使用replicationdriver和replication还有一点点小缺点,那就是任何一个ReplicationConnection其实是两个connection(master-connection或者slave connection),哇,真是占着xxx不xxx,虽然只用一个,但是另外一个也不能让别人用,ReplicationConnection不能无耻到这个地步.这可是浪费数据库连接数的典范(其实也没啥,不就是多浪费几个连接嘛,小题大作). 

下面我们首先来详细分析一下使用replicationdriver或者replication协议头时的内部细节. 

以下是详细的概要步骤(这词儿用的!): 
1,用户发起一个http请求,tomcat收到请求之后把从线程池中拿到一个线程,由这个请求线程来负责余下的流程(old io模型,new io模型在这个环节上稍微有点变化,但是接下来的不变) 

2,请求线程执行到open session in view filter,拿到一个拿到一个session,通过Threadlocal绑定到该请求线程中. 

3,请求线程执行到service,被事务拦截器拦截,在HibernateTransactionManager中, 同时拿到这个session所依赖的connection,而这个时候拿到的connection是数据库连接池的connection实现,也就是说这个connection是一个代理,该代理的target是ReplicationConnection.接着判断当前的事务的读写设置,如果是只读,那么调用ReplicationConnection#setReadonly方法把connection的readonlyflag设置为true. 

4,线程在执行setReadonly方法的时候,其实是在调换ReplicationConnection中的currentConnection的引用所指向的对象,原来指向master-connection,如果设置为readonly,那么就重新指向slave-connection,不过事情没有这么简单,调换引用之前,需要把master-connection的状态同时赋值给slave-connection.一共有3个状态需要转移,一个是Catalog,还有一个是autocommit,还有一个是Isolation(注意这里,slave-connection拿到了master-connection的isolation,而它自己原来的isolation却没有保存下来). 

5,当前线程退出setReadonly方法,继续在HibernateTransactionManager中游戈,这个时候,准备开始一个事务. 

6, 当前线程经过拦截器的前半部分,进入我们的service(假设没有其他代理对象),开始执行我们的业务方法,包含持久化逻辑(查询操作),这里拿到的connection其实是slave-connection. 

7, 请求线程退出service方法,回到拦截器的后半部分,这里有一个重要的方法,在提交事务之后,需要resetconnection,代码如下: 
Java代码   收藏代码
  1. public static void resetConnectionAfterTransaction(Connection con, Integer previousIsolationLevel) {  
  2.         Assert.notNull(con, "No Connection specified");  
  3.         try {  
  4.             // Reset transaction isolation to previous value, if changed for the transaction.  
  5.             if (previousIsolationLevel != null) {  
  6.                 if (logger.isDebugEnabled()) {  
  7.                     logger.debug("Resetting isolation level of JDBC Connection [" +  
  8.                             con + "] to " + previousIsolationLevel);  
  9.                 }  
  10.                 con.setTransactionIsolation(previousIsolationLevel.intValue());  
  11.             }  
  12.   
  13.             // Reset read-only flag.  
  14.             if (con.isReadOnly()) {  
  15.                 if (logger.isDebugEnabled()) {  
  16.                     logger.debug("Resetting read-only flag of JDBC Connection [" + con + "]");  
  17.                 }  
  18.                 con.setReadOnly(false);  
  19.             }  
  20.         }  
  21.         catch (Throwable ex) {  
  22.             logger.debug("Could not reset JDBC Connection after transaction", ex);  
  23.         }  
  24.     }  

我们可以看到,spring是先把事务开始之前的master-connection 的isolationlevel设置回来,然后再改变ReplicationConnection的currentConnection,拿现在的情况来说,spring把slave-connection的isolation重新设置为事务开始之前的isolation(也就是原始的master-connection的isolation),但是问题是事务之前的isolation是设置在master-connection上的.接着,spring又调用setReadonly方法,把currentConnection引用又指向了master-connection(当然,在这之前还需要把slave-connection的状态复制过来),把slave-connection的isolation(同时还有catalog和autocommit)设置给master-connection.不过这个时候,slave-connection的isolation就变成了master-connnection的isolation了,这也许是有问题的,因为这两个connection在开始的时候,isolation有可能是不一样的,但是一次请求之后,它们的isolation级别就变成一样的了.所以这里的代码应该是先setReadOnly,然后再设置isolation. 

8,请求线程再次拿到了master-connection,那么一旦以下的流程有延迟加载的情况发生,便会使用这个master-connection来执行查询操作(延迟加载难道应该用master-connection吗,显然不是,延迟加载应该用slave-connection,不过由于这里已经出了事务范围,所以ahuaxuan也没有办法来强制使用slave-connection进行延迟加载了). 

9,退出open session in view  filter,从当前请求线程中清空这个session和connection,也就是取消connection和请求线程的绑定.关闭session,并建connection重新返回connection pool. 


10,从请求线程中拿到返回数据,将请求线程返回线程池,并返回数据到客户端. 

总结:使用replicationdriver和replication协议头时,基本上就是以上这个流程,我们可以看到,在上面这个流程中,Master-connection和slave-connection在被交替使用,他们的状态也在整个流程中有2次相互覆盖(而且假设master和slave隔离级别不一样,那么可能目前的spring代码可能会导致一次请求之后改变slave-connection的隔离级别) 


由此看来,replicationdriver和replication协议头和spring+hibernate八字确实不太合啊,那只能另寻出路了,mysql-proxy由于政策原因被否决,那么只能在多数据源上下功夫了. 


那么怎么分析呢,以下是a某人的自言自语:replicationdriver和replication协议头最大的优点是在驱动上做手脚通过代理connection来透明的选择访问master或者是slave,但是也正因为这个特点,导致hibernate无法一开始(在osiv中)就知道使用哪个connection,也导致了以后一系列的connection转换之类,. 

那么如果有办法在osiv中就决定这次请求使用的connection,芑不是很帅气.这样说的话,那么要解决这个问题就是在osiv中确定connection了??,嗯,好像是这么回事,不过在osiv中确定connection好像有点难度啊,咋整啊,那换个思路,在osiv中确定datasource也行啊, 哦,有了. 

想到这里,觉悟了,在osiv中确定多数据源的问题的本质就是hibernate+spring的多数据源问题啊.真是苦海无边,回头是岸呀.之前了解过spring2.0之后多了一个类叫作: AbstractRoutingDataSource.那么我们来看一下它的功能: 
Java代码   收藏代码
  1. Abstract DataSource implementation that routes {@link #getConnection()} calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context.  

这段注释告诉我们可以用一个ThreadLocal把一个key绑定到当前线程,然后通过这个key,可以获得当前线程需要的datasource.又是一个代理,ReplicationConnection是代理slave-connection和master-connection,而AbstractRoutingDataSource是代理master-datasource和slave-datasource.既然Juergen Hoeller大叔把标准的使用方法都告诉我们了,我们还有什么担心的呢,按照他老人家的谆谆教导,我们有了以下实现: 
1,一个AbstractRoutingDataSource类,控制着应该使用哪个targetdatasource. 
Java代码   收藏代码
  1. /** 
  2.  *  
  3.  * @author ahuaxuan 
  4.  * @date 2008-6-7 
  5.  * @version $id$ 
  6.  */  
  7. public class MasterSlaveRoutingDataSource extends AbstractRoutingDataSource{  
  8.     private static transient Log logger = LogFactory.getLog(MasterSlaveRoutingDataSource.class);  
  9.       
  10. //DbType是一个标示符,代表datasource的key  
  11.     private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<DbType>();  
  12.       
  13.     public static void setDbType(DbType type) {  
  14.         contextHolder.set(type);  
  15.     }  
  16.   
  17.     public static DbType getDbType() {  
  18.         return contextHolder.get();  
  19.     }  
  20.       
  21.     public static void clearCustomerType() {  
  22.         contextHolder.remove();  
  23.     }  
  24.       
  25.     protected Object determineCurrentLookupKey() {  
  26.           
  27.         Object o = contextHolder.get();  
  28.         if (logger.isDebugEnabled()) {  
  29.             logger.debug("------- The current data source is " + o);  
  30.         }  
  31.         return o != null ? o : DbType.Slave;  
  32.     }  
  33.   
  34.     public boolean isWrapperFor(Class iface) throws SQLException {  
  35.         return false;  
  36.     }  
  37.   
  38.     public Object unwrap(Class iface) throws SQLException {  
  39.         return null;  
  40.     }  
  41. }  

2,一个filter,用来覆写原来的osiv的doFilterInternal方法. 
Java代码   收藏代码
  1. /** 
  2.  * hiddenPostSearch field means if we use post method to do searching, use slave datasource 
  3.  *  
  4.  * @author ahuaxuan 
  5.  * @date 2008-6-7 
  6.  * @version $id$ 
  7.  */  
  8. public class MsOpenSessionInViewFilter extends OpenSessionInViewFilter{  
  9.   
  10.     private static final String SLAVE_METHOD = "get";  
  11.     private static final String HIDDEN_FIELD_NAME = "hiddenPostSearch_001";  
  12.       
  13.     protected void doFilterInternal(HttpServletRequest request,   
  14.                         HttpServletResponse response, FilterChain filterChain)  
  15.             throws ServletException, IOException {  
  16.         if (SLAVE_METHOD.equals(request.getMethod()) ||   
  17.                 SLAVE_METHOD.equals(request.getParameter(HIDDEN_FIELD_NAME))) {  
  18.               
  19.             MasterSlaveRoutingDataSource.setDbType(DbType.Slave);  
  20.         } else {  
  21.             MasterSlaveRoutingDataSource.setDbType(DbType.Master);  
  22.         }  
  23.         super.doFilterInternal(request, response, filterChain);  
  24.     }  
  25. }  

3,在我们的spring配置文件上加上: 
Java代码   收藏代码
  1. <bean id="dataSource" class="com.xx.MasterSlaveRoutingDataSource">    
  2.      <property name="targetDataSources">    
  3.         <map key-type="com.xx.DbType">    
  4.            <entry key="Master" value-ref="writeDataSource"/>    
  5.            <entry key="Slave" value-ref="readDataSource"/>    
  6.         </map>    
  7.      </property>    
  8.      <property name="defaultTargetDataSource" ref="readDataSource"/>    
  9.    </bean>    

那么这样一来,我们就可以通过http的method来判断该次请求是使用master还是slave.而且在将session,connection绑定到线程之前就确定了使用master还是slave的connection. 

看到这里,地球人都明白了,这种方式和ReplicationConnection里通过readonly来判断使用master-connection还是slave-connection的原理真的是一摸一样啊.小样,别以为穿了个马甲我们就不认识你了. 
AbstractRoutingDataSource类虽然很简单,但是却很有效,之前坛子上也有人写过多数据源问题,原理也是一样的,只不过我们当然会用spring自带的东东啦. 

总结一下: 
本文分为两部分内容,第一部分分析了spring+hibernate在使用opensessioninview的情况下使用replicationdriver或者replication协议头时候的大体流程与内部操作,第二部分分析了spring+hibernate+msyql的master-slave场景下,如何使应用尽可能完美透明的使用mysql的master-slave模式,绕了一圈之后发现动态切换数据源的方法还是比较好的方案,spring2.0之后的版本提供了一个AbstractRoutingDataSource类可以帮助我们快速便捷的实现这个特性. 


注:由于ahuaxuan水平有限,理解难免有错误之处,还望不吝指出,不甚感激. 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值