mysql spring 数据库读写分离

如果一个互联网公司的项目只有一个数据库在支撑,在出现大量访问的时候,页面会无限超时报错,为此,可以尝试去 准备组建主从库,进行读写分离的数据库架构。

1.准备:两个数据库,一个主库 master,一个从库 slave,(主从库之间数据同步用的是中间件,这里暂时不讲,如果有兴趣,可以自己去百度下)。

2.在spring中的ApplicationContext.xml 文件中,配置两个数据源:

数据源(主库)1:

<bean id="master" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="url" value="jdbc:mysql://ip:port/dbname" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="username" value="username" />
<property name="password" value="password" />
        <!--maxTotal: 可以在这个池中同时被分配的有效连接数的最大值,如设置为负数,则不限制-->    
        <property name="maxTotal" value="500"/>  
        <property name="maxWaitMillis" value="600000" /> 
<property name="maxIdle" value="20" />
<property name="minIdle" value="20" />
<property name="initialSize" value="20" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="poolPreparedStatements" value="true" />
<property name="maxOpenPreparedStatements" value="100" />


<property name="removeAbandonedOnBorrow" value="true" />
<!-- 申请连接时执行validationQuery检测连接是否有效,配置为true会降低性能 -->
<property name="testOnBorrow" value="true" />
<!-- 归还连接时执行validationQuery检测连接是否有效,配置为true会降低性能 -->
<property name="testOnReturn" value="false" />
<!-- 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 -->
<property name="testWhileIdle" value="true" />
<!-- 用来检测连接是否有效的sql,要求是一个查询语句,如果validationQuery为 null,testOnBorrow、testOnReturn、testWhileIdle都不起其作用。 -->
<property name="validationQuery" value="SELECT 1 FROM DUAL" />
</bean>


数据源(从库)2:

<bean id="readOnlyDataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="url" value="jdbc:mysql://ip:port/dbname? useUnicode=true&amp;autoReconnect=true&amp;failOverReadOnly=false&amp;characterEncoding=UTF-8" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="username" value="username" />
<property name="password" value="password" />
<!--maxTotal: 可以在这个池中同时被分配的有效连接数的最大值,如设置为负数,则不限制-->    
<property name="maxTotal" value="500"/>  
<property name="maxWaitMillis" value="600000" /> 
<property name="maxIdle" value="20" />
<property name="minIdle" value="20" />
<property name="initialSize" value="20" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="poolPreparedStatements" value="true" />
<property name="maxOpenPreparedStatements" value="100" />
<property name="removeAbandonedOnBorrow" value="true" />
<!-- 申请连接时执行validationQuery检测连接是否有效,配置为true会降低性能 -->
<property name="testOnBorrow" value="true" />
<!-- 归还连接时执行validationQuery检测连接是否有效,配置为true会降低性能 -->
<property name="testOnReturn" value="false" />
<!-- 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 -->
<property name="testWhileIdle" value="true" />
<!-- 用来检测连接是否有效的sql,要求是一个查询语句,如果validationQuery为 null,testOnBorrow、testOnReturn、testWhileIdle都不起其作用。 -->
<property name="validationQuery" value="SELECT 1 FROM DUAL" />
</bean>


3.用java写动态数据源相关策略配置。

在书写动态源的时候,需要继承AbstractRoutingDataSource并,实现determineCurrentLookupKey和afterPropertiesSet方法,

package com.core.database.dynamicDataBase;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;


import com.alibaba.druid.util.ThreadLocalRandom;


/**
  * @Function 动态数据源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {


    private static final Log LOG = LogFactory.getLog(DynamicDataSource.class);
    private static final Long MAX_POOL = Long.MAX_VALUE;


    private Object writeDataSource; // 写数据源
    private List<Object> readDataSources; // 多个读数据源


    private int readDataSourcePollPattern = 0; // 获取读数据源方式,0:随机,1:轮询


    private AtomicLong counter = new AtomicLong(0);
    private final Lock lock = new ReentrantLock();


    @Autowired
    private DataSourceHeartBeat dataSourceHeartBeat;

public void setWriteDataSource(Object writeDataSource) {
        this.writeDataSource = writeDataSource;
    }


    public void setReadDataSources(List<Object> readDataSources) {
        this.readDataSources = readDataSources;
    }


    public void setReadDataSourcePollPattern(int readDataSourcePollPattern) {
        this.readDataSourcePollPattern = readDataSourcePollPattern;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        DataSourceTypeInfoDTO dataSourceTypeInfo = DynamicDataSourceHolder.getDataSource();


        if (dataSourceTypeInfo == null || dataSourceTypeInfo.getDataSourceType() == null
                || dataSourceTypeInfo.getDataSourceType() == DynamicDataSourceGlobalEnum.WRITE || readDataSources.size() <= 0) {
            return DynamicDataSourceGlobalEnum.WRITE.name();
        }


        int index = 1;
        if (readDataSourcePollPattern == 1) {
            // 轮询方式
            long currValue = counter.incrementAndGet();
            if ((currValue + 1) >= MAX_POOL) {
                try {
                    lock.lock();
                    if ((currValue + 1) >= MAX_POOL) {
                        counter.set(0);
                    }
                } finally {
                    lock.unlock();
                }
            }
            index = (int) (currValue % readDataSources.size());
        } else {
            // 随机方式
            index = ThreadLocalRandom.current().nextInt(0, readDataSources.size());
        }
        return dataSourceTypeInfo.getDataSourceType().name() + index;
    }


    @Override
    public void afterPropertiesSet() {
        if(writeDataSource == null) {  
            throw new IllegalArgumentException("property 'writeDataSource' is required");  
        } 
        setDefaultTargetDataSource(writeDataSource);
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        targetDataSources.put(DynamicDataSourceGlobalEnum.WRITE.name(), writeDataSource);
        if (CollectionUtils.isNotEmpty(this.readDataSources)) {
            for (int i = 0; i < readDataSources.size(); i++) {
                targetDataSources.put(DynamicDataSourceGlobalEnum.READ.name() + i, readDataSources.get(i));
            }
        }
        setTargetDataSources(targetDataSources);


        super.afterPropertiesSet();
    }


    /**
     * 
      * startDyHeartBeat(启动心跳)
     */
    public void startDyHeartBeat() {
        dataSourceHeartBeat.initDataSources(writeDataSource, readDataSources);
        dataSourceHeartBeat.startHeartBeat();
    }


    /**
     * 
      * refreshDataSources(刷新数据源)
      * @param dataSources
     */
    public void refreshDataSources(List<Object> dataSources) {
        boolean isSame = false;
        if (this.readDataSources.size() == dataSources.size()) {
            if (this.readDataSources.containsAll(dataSources)) {
                isSame = true;
            }
        }


        if (!isSame) {
            this.readDataSources.clear();
            if (CollectionUtils.isNotEmpty(dataSources)) {
                for (Object ds : dataSources) {
                    this.readDataSources.add(ds);
                }
            }
            
            Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
            targetDataSources.put(DynamicDataSourceGlobalEnum.WRITE.name(), writeDataSource);
            if (CollectionUtils.isNotEmpty(this.readDataSources)) {
                for (int i = 0; i < readDataSources.size(); i++) {
                    targetDataSources.put(DynamicDataSourceGlobalEnum.READ.name() + i, readDataSources.get(i));
                }
            }
            setTargetDataSources(targetDataSources);
            super.afterPropertiesSet();
            
            logger.info("dataSources has changed");
        }
        logger.info("refersh success");
    }
}

根据以上的类,注册bean并注入数据源,进行数据源切换。

<bean id="dataSource" class="com.core.database.dynamicDataBase.DynamicDataSource">
<property name="writeDataSource" ref="master" />
<property name="readDataSources">
<list>
<ref bean="readOnlyDataSource" />
</list>
</property>
<!--轮询方式 -->
<property name="readDataSourcePollPattern" value="1" />
<property name="defaultTargetDataSource" ref="master" />
</bean>


4.数据库心跳配置

package com.core.database.dynamicDataBase;


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;

/**
  * @Function 类功能说明
  */
public class DataSourceHeartBeat {


    /**
     * 日志记录 *
     */
    private static final Log LOG = LogFactory.getLog(DataSourceHeartBeat.class);
    private String[] noticePhones = null;
    
    @Autowired
    private DynamicDataSource dynamicDataSource;
    
    
    private BasicDataSource writeDataSource; // 写数据源
    private List<BasicDataSource> readDataSources; // 多个只读数据源
    
    private ExecutorService executorPool = Executors.newFixedThreadPool(30);
    
    /**
     * 
      * startHeartBeat(开启数据库监控心跳,90s执行一次数据库测试连接)
      * @return
     */
    public boolean startHeartBeat() {
        if (noticePhones == null) {
            ResourceBundle bundle = ResourceBundle.getBundle("system");
            if (bundle.containsKey("db_exception_notice_phones")) {
                noticePhones = bundle.getString("db_exception_notice_phones").split(",");
            }
        }
        
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            
            @Override
            public void run() {
                //测试写库
                Future<Boolean> writeFuture = executorPool.submit(new CheckDBConnectCallable(writeDataSource)); 
                
                //测试只读数据库
                Map<Integer, Future<Boolean>> readFutureMap = new HashMap<Integer, Future<Boolean>>();
                List<Object> normalReadDS = new ArrayList<Object>();
                for (int i = 0; i < readDataSources.size(); i++) {
                    readFutureMap.put(i, executorPool.submit(new CheckDBConnectCallable(readDataSources.get(i))));
                }
                
                try {
                    if (writeFuture.get()) {
                        LOG.info("测试写库连接成功");
                    }
                } catch (Exception e) {
                    LOG.error(e.getLocalizedMessage(), e);
                }
                
                for (Entry<Integer, Future<Boolean>> entry : readFutureMap.entrySet()) {
                    try {
                        if (entry.getValue().get()) {
                            normalReadDS.add(readDataSources.get(entry.getKey()));
                        }
                    } catch (Exception e) {
                        LOG.error(e.getLocalizedMessage(), e);
                    }
                }
                
                if (normalReadDS.size() == readDataSources.size()) {
                    LOG.info("测试所有只读库连接成功");
                }
                
                dynamicDataSource.refreshDataSources(normalReadDS);
                
            }
        }, 5000, 90000);
        
        return true;
    }
    
    /**
     * 
      * initReadOnlyDataSources(初始化只读数据库实例)
      * @param readDataSources
     */
    public void initDataSources(Object writeDataSource, List<Object> readDataSources) {
        this.writeDataSource = (BasicDataSource) writeDataSource;
        this.readDataSources = new ArrayList<BasicDataSource>();
        if (CollectionUtils.isNotEmpty(readDataSources)) {
            for (Object rds : readDataSources) {
                this.readDataSources.add((BasicDataSource) rds);
            }
        }
    }
    
    /**
     * 
      * destory(停止线程池)
     */
    public void destory() {
        try {
            if (!executorPool.isShutdown()) {
                executorPool.shutdown();
            }
            if (!executorPool.awaitTermination(60000, TimeUnit.MILLISECONDS)) {
                executorPool.shutdownNow();
            }
        } catch (Exception e) {
            LOG.error(e.getLocalizedMessage(), e);
            executorPool.shutdownNow();
        }
        LOG.info("executorPool destory success");
    }
}


在以上的基上,在xml中添加心跳bean

<!-- 数据库心跳 -->
    <bean id="dataSourceHeartBeat" class="com.core.database.dynamicDataBase.DataSourceHeartBeat" destroy-method="destory"/>


5.配置事务

<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">   
        <property name="dataSource" ref="dataSource"></property>
        </bean>
        <tx:annotation-driven transaction-manager="transactionManager"/>


6.配置切面相关数据
package com.core.database.dynamicDataBase;


import java.lang.reflect.Method;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;


/**
  * @Function 类功能说明
  */
public class DataSourceAspect {
    private static final Log LOG = LogFactory.getLog(DynamicDataSource.class);

    /**
     * 
      * pointCut(这里用一句话描述这个方法的作用)
     */
    public void pointCut() {
    };

    /**
     * 
      * before(aop before)
      * @param point
     */
    public void before(JoinPoint point) {
        DataSourceTypeInfoDTO dataSourceTypeInfo = new DataSourceTypeInfoDTO();
        dataSourceTypeInfo.setMethodInfo(point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName());
        if (isReadOnly(point)) {
            dataSourceTypeInfo.setDataSourceType(DynamicDataSourceGlobalEnum.READ);
        } else {
            dataSourceTypeInfo.setDataSourceType(DynamicDataSourceGlobalEnum.WRITE);
        }
        DynamicDataSourceHolder.putDataSource(dataSourceTypeInfo);
    }


    /**
     * 
      * after(aop after)
      * @param point
     */
    public void after(JoinPoint point) {
        DynamicDataSourceHolder.clearDataSource();
    }

    /**
      * isReadOnly(判断是否为读库)
      * @param point
      * @return
     */
    private Boolean isReadOnly(JoinPoint point) {
        // 若方法上有注解,则直接选择指定的数据源
        Object target = point.getTarget();
        String methodName = point.getSignature().getName();
        Class<?>[] classz = target.getClass().getInterfaces();
        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
        try {
            Method m = classz[0].getMethod(methodName, parameterTypes);
            if (m != null && m.isAnnotationPresent(DataSource.class)) {
                return DynamicDataSourceGlobalEnum.READ.equals(m.getAnnotation(DataSource.class).value());
            }
        } catch (Exception e) {
            LOG.error(e.getLocalizedMessage(), e);
        }


        // 方法名以query、find、get开头的方法名走从库
        return StringUtils.startsWithAny(methodName, new String[] { "query", "find", "get"});
    }
}

在xml中配置相关的切面切点

<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<bean id="dynamicDataSourceAspect" class="com.core.database.dynamicDataBase.DataSourceAspect" />
<aop:config>
<aop:aspect id="cdatasource" ref="dynamicDataSourceAspect">
<aop:pointcut id="tx" expression="execution(* com.zzq.mapper..*.*(..))"/>
<aop:before pointcut-ref="tx" method="before"/>
<aop:after pointcut-ref="tx" method="after"/>
</aop:aspect>
</aop:config>


读写分离的相关配置和代码流程大概就如上所诉,其实说白了,就是一下几个步骤,了解清楚了每个步骤,这个东西也不是很神秘的东西了。

1.准备:两个数据库
2.在spring中的ApplicationContext.xml 文件中,配置两个数据源
3.用java写动态数据源相关策略配置,注册bean并注入数据源,进行数据源切换。
4.数据库心跳配置,在xml中添加心跳bean
5.配置事务
6.配置切面相关数据,在xml中配置相关的切面切点,

在调用特定类的时候,进入切点,置换数据源,为了线程间不影响,需要启用本地化线程


如果有什么问题,请指正。


相关源码:http://download.csdn.net/download/dddkl007/10173830


============欢迎关注公众号(zzqJava)=========





  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ai小强

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值