使用Spring基于应用层实现读写分离(一)基础版

背景

我们一般应用对数据库而言都是“读多写少”,也就说对数据库读取数据的压力比较大,有一个思路就是说采用数据库集群的方案, 
其中一个是主库,负责写入数据,我们称之为:写库; 
其它都是从库,负责读取数据,我们称之为:读库;

那么,对我们的要求是: 
1、 读库和写库的数据一致; 
2、 写数据必须写到写库; 
3、 读数据必须到读库;

方案

解决读写分离的方案有两种:应用层解决和中间件解决。

应用层解决

这里写图片描述

优点: 
1、 多数据源切换方便,由程序自动完成; 
2、 不需要引入中间件; 
3、 理论上支持任何数据库; 
缺点: 
1、 由程序员完成,运维参与不到; 
2、 不能做到动态增加数据源;

中间件解决

这里写图片描述

优点: 
1、 源程序不需要做任何改动就可以实现读写分离; 
2、 动态添加数据源不需要重启程序;

缺点: 
1、 程序依赖于中间件,会导致切换数据库变得困难; 
2、 由中间件做了中转代理,性能有所下降;

相关中间件产品使用: 
mysql-proxy:http://hi.baidu.com/geshuai2008/item/0ded5389c685645f850fab07 
Amoeba for MySQL:http://www.iteye.com/topic/188598http://www.iteye.com/topic/1113437

使用Spring基于应用层实现

原理

这里写图片描述

在进入Service之前,使用AOP来做出判断,是使用写库还是读库,判断依据可以根据方法名判断,比如说以query、find、get等开头的就走读库,其他的走写库。

代码实现

DynamicDataSource

package com.song.facebook.readWriteSplit;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
 * 定义动态数据源,实现通过集成Spring提供的AbstractRoutingDataSource,只需要实现determineCurrentLookupKey方法;
 * 由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成;
 * @author songzl
 *
 */
public class DynamicDataSource extends  AbstractRoutingDataSource{

	@Override
	protected Object determineCurrentLookupKey() {
		//使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源key
		return DynamicDataSourceHolder.getDataSourceKey();
	}
}

DynamicDataSourceHolder

package com.song.facebook.readWriteSplit;

/**
 * 使用ThreadLocal技术来记录当前线程中的数据源的key
 * @author songzl
 *
 */
public class DynamicDataSourceHolder {
	//写库对应的数据源key
    private static final String MASTER = "master";

    //读库对应的数据源key
    private static final String SLAVE = "slave";

    //使用ThreadLocal记录当前线程的数据源key
    private static final ThreadLocal<String> holder = new ThreadLocal<String>();
    
    /**
     * 设置数据源key
     * @param key
     */
    public static void putDataSourceKey(String key) {
        holder.set(key);
    }

    /**
     * 获取数据源key
     * @return
     */
    public static String getDataSourceKey() {
        return holder.get();
    }

    /**
     * 标记写库
     */
    public static void markMaster(){
        putDataSourceKey(MASTER);
    }

    /**
     * 标记读库
     */
    public static void markSlave(){
        putDataSourceKey(SLAVE);
    }
}

  DataSourceAspect

package com.song.facebook.readWriteSplit;

import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;

/**
 * 定义数据源的AOP切面,通过该Service的方法名判断是应该走读库还是写库
 * @author songzl
 *
 */
public class DataSourceAspect {

	/**
     * 在进入Service方法之前执行
     * @param point 切面对象
     */
    public void before(JoinPoint point) {
        // 获取到当前执行的方法名
        String methodName = point.getSignature().getName();
        if (isSlave(methodName)) {
            // 标记为读库
            DynamicDataSourceHolder.markSlave();
        } else {
            // 标记为写库
            DynamicDataSourceHolder.markMaster();
        }
    }
    /**
     * 判断是否为读库
     * @param methodName
     * @return
     */
    private Boolean isSlave(String methodName) {
        // 方法名以query、find、get开头的方法名走从库
    	String[] searchStrings = new String[]{"query","find","get"};
        return StringUtils.startsWithAny(methodName, searchStrings);
    }
}

配置实现:jdbc.properties配置2个数据源

##配置俩个数据源,实现读写分离
jdbc.master.driver=com.mysql.jdbc.Driver
jdbc.master.url=jdbc:mysql://127.0.0.1:3306/mybatis_1128?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
jdbc.master.username=root
jdbc.master.password=123456


jdbc.slave01.driver=com.mysql.jdbc.Driver
jdbc.slave01.url=jdbc:mysql://127.0.0.1:3307/mybatis_1128?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
jdbc.slave01.username=root
jdbc.slave01.password=123456

配置多个mysql实例:环境Windows7、mysql5.7

1.将下载的文件解压到自己要安装的目录,我这里解压到目录:E:ProgramData,并且拷贝俩份放到同目录下:

2、找到my-default.ini文件,修改此文件的内容:

basedir:mysql位置 
datadir:数据存储位置 
port:mysql使用的端口号 
character-set-server:服务端所用编码配置

3、以管理员身份打开CMD,到mysql存放位置,我这里是[E:ProgramDatamysql-5.7.12-winx64-3306in],进入mysql的bin位子,输入如下命令:

mysqld install mysql-master --defaults-file="E:ProgramDatamysql-5.7.12-winx64-3306my-default.ini"

注:–defaults-file后的内容为你自己的my-default.ini文件位置,切记一定要以管理员身份打开cmd不然无法创建成功;

4、执行后会看到输入:Service successfully installed. 如下:

6、这个时候,mysql已经安装完成了,现在我们来初始化它,输入如下命令(执行此命令需要几分钟):

mysqld.exe --defaults-file="E:ProgramDatamysql-5.7.12-winx64-3306my-default.ini" --initialize --explicit_defaults_for_timestamp

注:–defaults-file后的内容为你自己的my-default.ini文件位,执行后没有任何输出(有内容输出就错了)

7、现在来启动它,输入命令:net start mysql-master,执行后输出:

 

这就这证明我们已将安装好了mysql,接下来我们来连上mysql服务,输入命令mysql -u root -P3306 -p,这个时候需要密码,打开mysql目录下的data文件夹下.err结尾的文件,搜索A temporary password is generated for root@localhost:,后面就是密码,输入密码,登录成功,自己可以根据自己的需要修改密码,修改密码的命令如下:

set password for root@localhost = password('123456');

这样我们就把密码改为了123456,重复安装3307即可,至此mysql安装和多实例已经完了。

定义连接池、DataSource、配置事务管理以及动态切换数据源切面:readWriteSplit.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:tx="http://www.springframework.org/schema/tx" 
	xmlns:aop="http://www.springframework.org/schema/aop" 
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="
		http://www.springframework.org/schema/context
		http://www.springframework.org/schema/context/spring-context-3.0.xsd
		http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
		http://www.springframework.org/schema/tx 
		http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
		http://www.springframework.org/schema/aop 
		http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
	<!-- 实现读写分离配置数据源 -->
	<!-- 定义数据源,使用自己实现的数据源 -->
	<bean id="readWriteDataSource" class="com.song.facebook.readWriteSplit.DynamicDataSource">
	    <!-- 设置多个数据源 -->
	    <property name="targetDataSources">
	        <map key-type="java.lang.String">
	            <!-- 这个key需要和程序中的key一致 -->
	            <entry key="master" value-ref="masterDataSource"/>
	            <entry key="slave" value-ref="slave01DataSource"/>
	        </map>
	    </property>
	    <!-- 设置默认的数据源,这里默认走写库 -->
	    <property name="defaultTargetDataSource" ref="masterDataSource"/>
	</bean>
	<!-- 实现读写分离的配置连接池 -->
	<!-- 配置写库的连接池  -->
	<bean id="masterDataSource" class="com.jolbox.bonecp.BoneCPDataSource"
	    destroy-method="close">
	    <!-- 数据库驱动 -->
	    <property name="driverClass" value="${jdbc.master.driver}" />
	    <!-- 相应驱动的jdbcUrl -->
	    <property name="jdbcUrl" value="${jdbc.master.url}" />
	    <!-- 数据库的用户名 -->
	    <property name="username" value="${jdbc.master.username}" />
	    <!-- 数据库的密码 -->
	    <property name="password" value="${jdbc.master.password}" />
	    <!-- 检查数据库连接池中空闲连接的间隔时间,单位是分,默认值:240,如果要取消则设置为0 -->
	    <property name="idleConnectionTestPeriod" value="60" />
	    <!-- 连接池中未使用的链接最大存活时间,单位是分,默认值:60,如果要永远存活设置为0 -->
	    <property name="idleMaxAge" value="30" />
	    <!-- 每个分区最大的连接数 -->
	    <property name="maxConnectionsPerPartition" value="150" />
	    <!-- 每个分区最小的连接数 -->
	    <property name="minConnectionsPerPartition" value="5" />
	</bean>
	<!-- 配置读库的连接池 -->
	<bean id="slave01DataSource" class="com.jolbox.bonecp.BoneCPDataSource"
	    destroy-method="close">
	    <!-- 数据库驱动 -->
	    <property name="driverClass" value="${jdbc.slave01.driver}" />
	    <!-- 相应驱动的jdbcUrl -->
	    <property name="jdbcUrl" value="${jdbc.slave01.url}" />
	    <!-- 数据库的用户名 -->
	    <property name="username" value="${jdbc.slave01.username}" />
	    <!-- 数据库的密码 -->
	    <property name="password" value="${jdbc.slave01.password}" />
	    <!-- 检查数据库连接池中空闲连接的间隔时间,单位是分,默认值:240,如果要取消则设置为0 -->
	    <property name="idleConnectionTestPeriod" value="60" />
	    <!-- 连接池中未使用的链接最大存活时间,单位是分,默认值:60,如果要永远存活设置为0 -->
	    <property name="idleMaxAge" value="30" />
	    <!-- 每个分区最大的连接数 -->
	    <property name="maxConnectionsPerPartition" value="150" />
	    <!-- 每个分区最小的连接数 -->
	    <property name="minConnectionsPerPartition" value="5" />
	</bean>
	
	<!-- 定义事务管理器 -->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<!-- 将id为readWriteDataSource的自定义的动态数据源注入 -->
	    <property name="dataSource" ref="readWriteDataSource" />
	</bean>
	
	<!-- 定义事务策略 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!--定义查询方法都是只读的 -->
            <tx:method name="query*" read-only="true" />
            <tx:method name="find*" read-only="true" />
            <tx:method name="get*" read-only="true" />

            <!-- 主库执行操作,事务传播行为定义为默认行为 -->
            <tx:method name="save*" propagation="REQUIRED" />
            <tx:method name="update*" propagation="REQUIRED" />
            <tx:method name="delete*" propagation="REQUIRED" />

            <!--其他方法使用默认事务策略 -->
            <tx:method name="*" />
        </tx:attributes>
    </tx:advice>
    
    <!-- 定义AOP切面处理器 -->
    <bean id="dataSourceAspect" class="com.song.facebook.readWriteSplit.DataSourceAspect" />
    <aop:config>
        <!-- 定义切面,所有的service的所有方法 -->
        <aop:pointcut id="txPointcut" expression="execution(* xx.xxx.xxxxxxx.service.*.*(..))" />
        <!-- 应用事务策略到Service切面 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>

        <!-- 将切面应用到自定义的切面处理器上,-9999保证该切面优先级最高执行 -->
        <aop:aspect ref="dataSourceAspect" order="-9999">
            <aop:before method="before" pointcut-ref="txPointcut" />
        </aop:aspect>
    </aop:config>
</beans>

特别注意下:com.jolbox.bonecp.BoneCPDataSource的jar地址提供给大家,http://maven.outofmemory.cn/com.jolbox/bonecp/0.8.0.RELEASE/(原创未提供,找了半天才找到)

或者在pom.xml文件中添加这个也可:

<dependency>
	    <groupId>com.jolbox</groupId>
	    <artifactId>bonecp</artifactId>
	    <version>0.8.0.RELEASE</version>
</dependency>

到此为止,第一版构建完毕。

原文地址:https://www.cnblogs.com/aoshicangqiong/p/8145490.html