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

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,代码如下:
public static void resetConnectionAfterTransaction(Connection con, Integer previousIsolationLevel) {
		Assert.notNull(con, "No Connection specified");
		try {
			// Reset transaction isolation to previous value, if changed for the transaction.
			if (previousIsolationLevel != null) {
				if (logger.isDebugEnabled()) {
					logger.debug("Resetting isolation level of JDBC Connection [" +
							con + "] to " + previousIsolationLevel);
				}
				con.setTransactionIsolation(previousIsolationLevel.intValue());
			}

			// Reset read-only flag.
			if (con.isReadOnly()) {
				if (logger.isDebugEnabled()) {
					logger.debug("Resetting read-only flag of JDBC Connection [" + con + "]");
				}
				con.setReadOnly(false);
			}
		}
		catch (Throwable ex) {
			logger.debug("Could not reset JDBC Connection after transaction", ex);
		}
	}

我们可以看到,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.那么我们来看一下它的功能:
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.
/**
 * 
 * @author ahuaxuan
 * @date 2008-6-7
 * @version $id$
 */
public class MasterSlaveRoutingDataSource extends AbstractRoutingDataSource{
	private static transient Log logger = LogFactory.getLog(MasterSlaveRoutingDataSource.class);
	
//DbType是一个标示符,代表datasource的key
	private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<DbType>();
	
	public static void setDbType(DbType type) {
		contextHolder.set(type);
	}

	public static DbType getDbType() {
		return contextHolder.get();
	}
	
	public static void clearCustomerType() {
		contextHolder.remove();
	}
	
	protected Object determineCurrentLookupKey() {
		
		Object o = contextHolder.get();
		if (logger.isDebugEnabled()) {
			logger.debug("------- The current data source is " + o);
		}
		return o != null ? o : DbType.Slave;
	}

	public boolean isWrapperFor(Class iface) throws SQLException {
		return false;
	}

	public Object unwrap(Class iface) throws SQLException {
		return null;
	}
}


2,一个filter,用来覆写原来的osiv的doFilterInternal方法.
/**
 * hiddenPostSearch field means if we use post method to do searching, use slave datasource
 * 
 * @author ahuaxuan
 * @date 2008-6-7
 * @version $id$
 */
public class MsOpenSessionInViewFilter extends OpenSessionInViewFilter{

	private static final String SLAVE_METHOD = "get";
	private static final String HIDDEN_FIELD_NAME = "hiddenPostSearch_001";
	
	protected void doFilterInternal(HttpServletRequest request, 
						HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		if (SLAVE_METHOD.equals(request.getMethod()) || 
				SLAVE_METHOD.equals(request.getParameter(HIDDEN_FIELD_NAME))) {
			
			MasterSlaveRoutingDataSource.setDbType(DbType.Slave);
		} else {
			MasterSlaveRoutingDataSource.setDbType(DbType.Master);
		}
		super.doFilterInternal(request, response, filterChain);
	}
}

3,在我们的spring配置文件上加上:
<bean id="dataSource" class="com.xx.MasterSlaveRoutingDataSource">  
     <property name="targetDataSources">  
        <map key-type="com.xx.DbType">  
           <entry key="Master" value-ref="writeDataSource"/>  
           <entry key="Slave" value-ref="readDataSource"/>  
        </map>  
     </property>  
     <property name="defaultTargetDataSource" ref="readDataSource"/>  
   </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水平有限,理解难免有错误之处,还望不吝指出,不甚感激.
分享到:
评论
9 楼 ahuaxuan 2010-12-09  
blackhost 写道
对于楼主的这种方式,我有一个不小的疑问,盼望楼主解答!

这个多数据源的配置,取决于用户在MasterSlaveRoutingDataSource中时,根据Key获取是从master还是从Slave取Connection。

问题是,假如两个不同的用户,不同的Session,在取Connection钱,分别作了Master和Slave的请求,假如两个请求非常快。比如如下:

A-》Master-》getHiberateTemplate();
       b-》Slave->getHiberateTemplate();

在A实际获取Connection之前,B抢先改变了MasterSlaveRoutingDataSource的key值,则A就取到了Slave的Connnection,而实际上A期望得到的是Master

请问这个问题不会发生吗?

key是指绑定到thread中的那个key,两个请求是两个线程,他们不会相互影响的。
8 楼 blackhost 2010-09-21  
对于楼主的这种方式,我有一个不小的疑问,盼望楼主解答!

这个多数据源的配置,取决于用户在MasterSlaveRoutingDataSource中时,根据Key获取是从master还是从Slave取Connection。

问题是,假如两个不同的用户,不同的Session,在取Connection钱,分别作了Master和Slave的请求,假如两个请求非常快。比如如下:

A-》Master-》getHiberateTemplate();
       b-》Slave->getHiberateTemplate();

在A实际获取Connection之前,B抢先改变了MasterSlaveRoutingDataSource的key值,则A就取到了Slave的Connnection,而实际上A期望得到的是Master

请问这个问题不会发生吗?
7 楼 niyunjiu 2009-03-15  
ahuaxuan 写道

生产环境没有用mysql-proxy,现在运维估计也不敢去动生产环境,现在是master-slave模式,具体是访问master还是slave这个都是由程序控制的

谢谢
6 楼 ahuaxuan 2009-03-09  
生产环境没有用mysql-proxy,现在运维估计也不敢去动生产环境,现在是master-slave模式,具体是访问master还是slave这个都是由程序控制的
5 楼 niyunjiu 2009-03-09  
ahuaxuan 写道

我们没有用mysql-proxy,replicationdriver 是支持事务的,ameoba 我没有用过,不过对它我还是比较感兴趣的

谢谢!!我周末查证过,mysql-proxy是支持事务的,你们生产环境不是在用mysql-prxy吗?呵呵
4 楼 ahuaxuan 2009-03-06  
我们没有用mysql-proxy,replicationdriver 是支持事务的,ameoba 我没有用过,不过对它我还是比较感兴趣的
3 楼 niyunjiu 2009-03-06  
请问博主,
1、到目前为止,你们的mysql-proxy还稳定吗?
2、replicationdriver 支持事务吗?我们用mysql innodb引擎,所以要求支持事务。
ameoba类似于mysql-proxy,比mysql-proxy好,唯一的缺点就是不支持事务。

谢谢!
2 楼 ahuaxuan 2009-03-02  
进入事务得方法不一定会设置成非只读,以为有只读事务,也就是说事务还是有得,延迟加载得时候应该选择slave得datasource,我也是这样做得
1 楼 zhang_ly520 2009-03-02  
最近也在做这方便的研究,看了文章后受益良多。
因为对于spring底层了解的不多,我想知道要是在spring中配置了事务管理,拦截了所有需要事务的方法的话,哪么进入事务的方法是否都被设置成“非只读”呢?哪么其他的方法,比如“get”都不进入事务也就都是“只读”,哪么延迟加载的时候是不是应该使用slave的连接,否则我所有的方法可能都是通过master连接执行的。
不知道能看懂我说的不?

相关推荐

Global site tag (gtag.js) - Google Analytics