论坛首页 Java企业应用论坛

在应用层通过spring解决数据库读写分离

浏览 29290 次
精华帖 (0) :: 良好帖 (1) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2012-11-08   最后修改:2012-11-20

 

 

如何配置mysql数据库的主从?

单机配置mysql主从:http://my.oschina.net/god/blog/496

 

常见的解决数据库读写分离有两种方案

1、应用层

http://neoremind.net/2011/06/spring实现数据库读写分离

目前的一些解决方案需要在程序中手动指定数据源,比较麻烦,后边我会通过AOP思想来解决这个问题。

 

2、中间件

mysql-proxyhttp://hi.baidu.com/geshuai2008/item/0ded5389c685645f850fab07

Amoeba for MySQLhttp://www.iteye.com/topic/188598http://www.iteye.com/topic/1113437

 

此处我们介绍一种在应用层的解决方案,通过spring动态数据源和AOP来解决数据库的读写分离。

 

该方案目前已经在一个互联网项目中使用了,而且可以很好的工作。

 

该方案目前支持

一读多写;当写时默认读操作到写库、当写时强制读操作到读库。

 

考虑未来支持

读库负载均衡、读库故障转移等。

 

使用场景

不想引入中间件,想在应用层解决读写分离,可以考虑这个方案;

建议数据访问层使用jdbcibatis,不建议hibernate

 

优势

应用层解决,不引入额外中间件;

在应用层支持『当写时默认读操作到写库』,这样如果我们采用这种方案,在写操作后读数据直接从写库拿,不会产生数据复制的延迟问题;

应用层解决读写分离,理论支持任意数据库。

 

缺点

1、不支持@Transactional注解事务,此方案要求所有读方法必须是read-only=true,因此如果是@Transactional,这样就要求在每一个读方法头上加@Transactional 且readOnly属性=true,相当麻烦。 :oops: 

2、必须按照配置约定进行配置,不够灵活。


两种方案



方案1:当只有读操作的时候,直接操作读库(从库);

        当在写事务(即写主库)中读时,也是读主库(即参与到主库操作),这样的优势是可以防止写完后可能读不到刚才写的数据;

 

此方案其实是使用事务传播行为为:SUPPORTS解决的。

 


方案2:当只有读操作的时候,直接操作读库(从库);

        当在写事务(即写主库)中读时,强制走从库,即先暂停写事务,开启读(读从库),然后恢复写事务。

此方案其实是使用事务传播行为为:NOT_SUPPORTS解决的。

 

核心组件

cn.javass.common.datasource.ReadWriteDataSource:读写分离的动态数据源,类似于AbstractRoutingDataSource,具体参考javadoc

cn.javass.common.datasource.ReadWriteDataSourceDecision:读写库选择的决策者,具体参考javadoc

cn.javass.common.datasource.ReadWriteDataSourceProcessor:此类实现了两个职责(为了减少类的数量将两个功能合并到一起了):读/写动态数据库选择处理器、通过AOP切面实现读/写选择,具体参考javadoc

 

具体配置

1、数据源配置

1.1、写库配置

  	<bean id="writeDataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource">
		<property name="alias" value="writeDataSource"/>
		<property name="driver" value="${write.connection.driver_class}" />
		<property name="driverUrl" value="${write.connection.url}" />
		<property name="user" value="${write.connection.username}" />
		<property name="password" value="${write.connection.password}" />
		<property name="maximumConnectionCount" value="${write.proxool.maximum.connection.count}"/>
		<property name="minimumConnectionCount" value="${write.proxool.minimum.connection.count}" />
		<property name="statistics" value="${write.proxool.statistics}" />
		<property name="simultaneousBuildThrottle" value="${write.proxool.simultaneous.build.throttle}"/>
	</bean>

 

1.2、读库配置

    <bean id="readDataSource1" class="org.logicalcobwebs.proxool.ProxoolDataSource">
        <property name="alias" value="readDataSource"/>
        <property name="driver" value="${read.connection.driver_class}" />
        <property name="driverUrl" value="${read.connection.url}" />
        <property name="user" value="${read.connection.username}" />
        <property name="password" value="${read.connection.password}" />
        <property name="maximumConnectionCount" value="${read.proxool.maximum.connection.count}"/>
        <property name="minimumConnectionCount" value="${read.proxool.minimum.connection.count}" />
        <property name="statistics" value="${read.proxool.statistics}" />
        <property name="simultaneousBuildThrottle" value="${read.proxool.simultaneous.build.throttle}"/>
    </bean> 

1.3、读写动态库配置   

通过writeDataSource指定写库,通过readDataSourceMap指定从库列表,从库列表默认通过顺序轮询来使用读库,具体参考javadoc

    <bean id="readWriteDataSource" class="cn.javass.common.datasource.ReadWriteDataSource">
        <property name="writeDataSource" ref="writeDataSource"/>
        <property name="readDataSourceMap">
           <map>
              <entry key="readDataSource1" value-ref="readDataSource1"/>
              <entry key="readDataSource2" value-ref="readDataSource1"/>
              <entry key="readDataSource3" value-ref="readDataSource1"/>
              <entry key="readDataSource4" value-ref="readDataSource1"/>
           </map>
        </property>
    </bean> 

 

2XML事务属性配置

所以读方法必须是read-only(必须,以此来判断是否是读方法)。

    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="save*" propagation="REQUIRED" />
            <tx:method name="add*" propagation="REQUIRED" />
            <tx:method name="create*" propagation="REQUIRED" />
            <tx:method name="insert*" propagation="REQUIRED" />
            <tx:method name="update*" propagation="REQUIRED" />
            <tx:method name="merge*" propagation="REQUIRED" />
            <tx:method name="del*" propagation="REQUIRED" />
            <tx:method name="remove*" propagation="REQUIRED" />
            
            <tx:method name="put*" read-only="true"/>
            <tx:method name="query*" read-only="true"/>
            <tx:method name="use*" read-only="true"/>
            <tx:method name="get*" read-only="true" />
            <tx:method name="count*" read-only="true" />
            <tx:method name="find*" read-only="true" />
            <tx:method name="list*" read-only="true" />
            
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice> 

 

3、事务管理器

事务管理器管理的是readWriteDataSource

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="readWriteDataSource"/>
    </bean> 

 

4、读/写动态数据库选择处理器

根据之前的txAdvice配置的事务属性决定是读/写,具体参考javadoc

forceChoiceReadWhenWrite:用于确定在如果目前是写(即开启了事务),下一步如果是读,是直接参与到写库进行读,还是强制从读库读,具体参考javadoc

    <bean id="readWriteDataSourceTransactionProcessor" class="cn.javass.common.datasource.ReadWriteDataSourceProcessor">
       <property name="forceChoiceReadWhenWrite" value="false"/>
    </bean> 

 

5、事务切面和读/写库选择切面

    <aop:config expose-proxy="true">
        <!-- 只对业务逻辑层实施事务 -->
        <aop:pointcut id="txPointcut" expression="execution(* cn.javass..service..*.*(..))" />
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
        
        <!-- 通过AOP切面实现读/写库选择 -->
        <aop:aspect order="-2147483648" ref="readWriteDataSourceTransactionProcessor">
           <aop:around pointcut-ref="txPointcut" method="determineReadOrWriteDB"/>
        </aop:aspect>
    </aop:config> 

1、事务切面一般横切业务逻辑层;

2、此处我们使用readWriteDataSourceTransactionProcessor的通过AOP切面实现读/写库选择功能,order=Integer.MIN_VALUE(即最高的优先级),从而保证在操作事务之前已经决定了使用读/写库。

 

6、测试用例

只要配置好事务属性(通过read-only=true指定读方法)即可,其他选择读/写库的操作都交给readWriteDataSourceTransactionProcessor完成。

 

可以参考附件的:

cn.javass.readwrite.ReadWriteDBTestWithForceChoiceReadOnWriteFalse

cn.javass.readwrite.ReadWriteDBTestWithNoForceChoiceReadOnWriteTrue

 

 

 

可以下载附件的代码进行测试,具体选择主/从可以参考日志输出。

 

暂不想支持@Transactional注解式事务。

 

PS:欢迎拍砖指正。   

 

 

 

 

   发表时间:2012-11-11  
楼主这个解决方案非常不错啊,刚好解决了我现在的难题。
我先下载回来看看代码。
感谢楼主的分享。
0 请登录后投票
   发表时间:2012-11-11  
eivenchan 写道
楼主这个解决方案非常不错啊,刚好解决了我现在的难题。
我先下载回来看看代码。
感谢楼主的分享。

太好了,欢迎反馈我会及时更新。
0 请登录后投票
   发表时间:2012-11-11  
不错~我现在也在研究数据库的读写分离问题
0 请登录后投票
   发表时间:2012-11-11  
zhuzi1982 写道
不错~我现在也在研究数据库的读写分离问题

欢迎试用及反馈   有问题欢迎留言探讨
0 请登录后投票
   发表时间:2012-11-12  
拜托能不能不要再写5年前已经讨论清楚的事情?5年前Readonly大大就指出这事儿的最好解决方案就是com.mysql.jdbc.ReplicationDriver
0 请登录后投票
   发表时间:2012-11-13   最后修改:2012-11-13
downpour 写道
拜托能不能不要再写5年前已经讨论清楚的事情?5年前Readonly大大就指出这事儿的最好解决方案就是com.mysql.jdbc.ReplicationDriver

嗯,谢谢,刚看了一下07年你的帖子:http://www.iteye.com/topic/143714
1、不过如果是sqlserver或者postgresql呢?比如我朋友的公司他们使用sqlserver,这个就用不上了。当然也可以使用中间件。
2、写事务读时需要读写库的情况,这种情况中间件搞不定,还得应用层做。
希望指点下。
我的这个方案也是利用read-only属性完成的。
0 请登录后投票
   发表时间:2012-11-13   最后修改:2012-11-13
downpour 写道
拜托能不能不要再写5年前已经讨论清楚的事情?5年前Readonly大大就指出这事儿的最好解决方案就是com.mysql.jdbc.ReplicationDriver

 

看了下源代码:

  public synchronized void close()
    throws SQLException
  {
    this.masterConnection.close();
    this.slavesConnection.close();
  }


不知道如果masterConnection关闭时 如果超时 会不会造成slavesConnection不释放的情况? 有没有这种可能?

  public synchronized void setReadOnly(boolean readOnly)
    throws SQLException
  {
    if (readOnly) {
      if (this.currentConnection != this.slavesConnection) {
        switchToSlavesConnection();
      }
    }
    else if (this.currentConnection != this.masterConnection)
      switchToMasterConnection();
  }


如果在一个事务中:
1、写
2、读
3、写(此处写我认为会写到从库)

另一个:
http://www.iteye.com/topic/205926

ahuaxuan 写道
如果事务开始之前设置readonly,那么首先使用的connection应该是slaveconnection,那么也就是说,开始事务的connection是slaveconnection,在事务中connection被设置了非readonly,那么也就是说执行操作的connection就变成了masterconnection,最后事务提交的时候还是slaveconnection创建的事务进行的提交


用此法,我认为最好所有读方法传播行为必须是NOT_SUPPORTS 且 readonly=true。

 

0 请登录后投票
   发表时间:2012-11-13  
sqlserver的问题,答案是显而易见的,不过实在是没有义务来指点,自己去想。

在应用层来处理读写分离是一种愚蠢而又迫不得已的做法。从大局看,与数据库有关的东西,放置到越底层肯定越好,所以才会致力于MySQL Proxy的研究,而Driver的改造是在Proxy之上,层次上已经往上抬了,如果再抬到应用层,总会在一定程度上造成一些应用程序的限制条件(比如现在应用层的方案就是限制在事务或者方法签名的层面上)。

为什么以前MySQL的查询缓冲区很多时候都被disable(处于命中率的考虑),然而近年来这种观点在被不断修正,并采取各种各样的方法来对此进行软件和硬件上的优化呢?就是考虑到我刚才说的一点,软件设计以解耦为重要目标之一,数据库的东西要是数据库自己能解决一定是交给数据库去搞,迫不得已才由应用层去扩展。

PS 你写了那么多文章,只有2,3篇我觉得有价值,其余的都毫无意义。最好在写文章的时候先考虑一下最佳实践再发出来,不要把好几年前的冷饭炒了又炒。很多时候看了你的文章,初学者会被领到一条不归路上去。
0 请登录后投票
   发表时间:2012-11-13   最后修改:2012-11-13
downpour 写道
sqlserver的问题,答案是显而易见的,不过实在是没有义务来指点,自己去想。

在应用层来处理读写分离是一种愚蠢而又迫不得已的做法。从大局看,与数据库有关的东西,放置到越底层肯定越好,所以才会致力于MySQL Proxy的研究,而Driver的改造是在Proxy之上,层次上已经往上抬了,如果再抬到应用层,总会在一定程度上造成一些应用程序的限制条件(比如现在应用层的方案就是限制在事务或者方法签名的层面上)。

为什么以前MySQL的查询缓冲区很多时候都被disable(处于命中率的考虑),然而近年来这种观点在被不断修正,并采取各种各样的方法来对此进行软件和硬件上的优化呢?就是考虑到我刚才说的一点,软件设计以解耦为重要目标之一,数据库的东西要是数据库自己能解决一定是交给数据库去搞,迫不得已才由应用层去扩展。

PS 你写了那么多文章,只有2,3篇我觉得有价值,其余的都毫无意义。最好在写文章的时候先考虑一下最佳实践再发出来,不要把好几年前的冷饭炒了又炒。很多时候看了你的文章,初学者会被领到一条不归路上去。

谢谢。
引用
sqlserver的问题,答案是显而易见的,不过实在是没有义务来指点,自己去想。
我知道你的意思,还是从底层着手。
引用
从大局看,与数据库有关的东西,放置到越底层肯定越好
这个肯定赞同,能从应用层抽取到底层当然最好。但是有些时候无法做到完全底层的,比如『ReplicationDriver 如果在一个事务中:1、写   2、读 3、写(此处写我认为会写到从库)』。

最好在写文章的时候先考虑一下最佳实践再发出来,不要把好几年前的冷饭炒了又炒。
嗯,谢谢指点,我就想把心里想的东西写出来,憋着也难受。

引用
很多时候看了你的文章,初学者会被领到一条不归路上去。
我当然不希望这样, 。有问问题的我尽我所能回答,只要有问的我一定会回答!

我现在这个阶段就想把我想到的发出来,接受大家批评和指正,我不想去误人子弟。
如果说『我把初学者会被领到一条不归路上去』,论坛就是讨论的地方,如果我不对就是不对,我接受批评。如果我把一些想法憋在肚子里,我怎么知道是对是错?

非常感谢你的赐教,我会努力的。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics