zoukankan      html  css  js  c++  java
  • 使用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>
    

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

  • 相关阅读:
    结对编程队友个人项目分析
    Android入门3:从Toolbar到Material Design
    Qt串口通信
    AVT Vimba与OpenCV环境配置
    Git远程使用技巧
    Android入门2:从GridView控件使用到自定义Adapter
    Android入门1:使用VideoView和MediController播放视频
    kafka+spark streaming+redis学习
    kafka学习笔记
    安卓获取服务器返回的图片资源路径并下载图片
  • 原文地址:https://www.cnblogs.com/aoshicangqiong/p/8145490.html
Copyright © 2011-2022 走看看