mysql实现读写分离

面试时常被问到aop在你们项目中哪些地方用到了,我的回答是事物、日志,再问就回答不上来,最近看了读写分离的视频,发现读写分离也是aop的应用场景之一。

spring提供的AbstractRoutingDataSource会在方法afterPropertiesSet中,将数据源从targetDataSources属性中取出来,put进Map<Object, DataSource> resolvedDataSources

@Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());
        for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
            Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
            DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
            this.resolvedDataSources.put(lookupKey, dataSource);
        }
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }

调用service方法的时候,通过lookupKey 在resolvedDataSources中get到对应的dataSource

/**
     * Retrieve the current target DataSource. Determines the
     * {@link #determineCurrentLookupKey() current lookup key}, performs
     * a lookup in the {@link #setTargetDataSources targetDataSources} map,
     * falls back to the specified
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
     * @see #determineCurrentLookupKey()
     */
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }

添加一个aop切面,在service之前判断如果方法名以"query", "find", "get"开头,就走从库,否则走写库

/**
 * 定义数据源的AOP切面,通过该Service的方法名判断是应该走读库还是写库
 * 
 * @author zhijun
 *
 */
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开头的方法名走从库
        return StringUtils.startsWithAny(methodName, "query", "find", "get");
    }

}

/**
 * 
 * 使用ThreadLocal技术来记录当前线程中的数据源的key
 * 
 * @author zhijun
 *
 */
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);
    }

}

定义动态数据源,继承Spring提供的AbstractRoutingDataSource类,实现determineCurrentLookupKey方法,根据lookupKey从resolvedDataSources中get到对应的数据源

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

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

}

配置DynamicDataSource类,项目启动时自动为AbstractRoutingDataSource类注入读、写数据源属性到targetDataSources属性,spring的AbstractRoutingDataSource类再从targetDataSources中put进resolvedDataSources

<!-- 定义数据源,使用自己实现的数据源 -->
    <bean id="dataSource" class="com.taotao.manage.datasource.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>

配置读(从)、写(主)数据源的数据库连接信息,

jdbc.master.driver=com.mysql.jdbc.Driver
jdbc.master.url=jdbc:mysql://127.0.0.1:3381/taotao?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
jdbc.master.username=root
jdbc.master.password=root


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

 

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

2019年8月15日后续更新:学习读写分离刚开始一直没有遇到使用场景,直到目前公司项目数据库由oracle切换成mysql后,数百万级的查询效率过慢,于是在必要字段添加了索引,查询速度问题解决。
但是后续批量插入数据时,插入几万条数据要耗时很久,经过排查问题是索引导致批量插入很慢,于是删除了索引。
这样查询速度又变的难以忍受,于是后续查询的问题决定采用读写分离的方案解决

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值