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

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&autoReconnect=true&failOverReadOnly=false&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中配置相关的切面切点,


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


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