前言:公司搞活动,主数据库压力太大,决定将压力太大的接口迁移到从库查询
前提:不能对原有业务代码进行改动
多数据源路由器优缺点如下:
优点:
1、在不改变原来逻辑的基础上进行数据源路由,侵入性弱,改动小
缺点:
1、多数据源的数据同步可能会存在一定的延时,请做好评估工作
1.定于数据源枚举类
package com.xxx.xxx.xxx;
/**
* 多数据源枚举类
* ========================
* @author linyantao
* ========================
*/
public enum DataSources {
/**
* 主库
*/
MASTER,
/**
* 从库
*/
SLAVE
}
2.通过TheadLocal来保存每条线程的数据源变量
package com.xxx.xxx.xxx;
/**
* 数据源管理类
* ========================
* @author linyantao
* ========================
*/
public class DataSourceTypeManager {
/**
* 数据源线程池
*/
private static final ThreadLocal<DataSources> dataSourceTypes = new ThreadLocal<DataSources>() {
@Override
protected DataSources initialValue() {
/**
* 默认取主库
*/
return DataSources.MASTER;
}
};
/**
* 获取当前线程数据源
* @return
*/
public static DataSources get() {
return dataSourceTypes.get();
}
/**
* 设置当前线程数据源
* @param dataSourceType
*/
public static void set(DataSources dataSourceType) {
dataSourceTypes.set(dataSourceType);
}
/**
* 重置当前线程数据源
*/
public static void reset() {
dataSourceTypes.set(DataSources.MASTER);
}
/**
* 删除当前线程数据源变量
*/
public static void remove() {
dataSourceTypes.remove();
}
}
3.定义 ThreadLocalRountingDataSource,继承AbstractRoutingDataSource
package com.xxx.xxx.xxx;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 路由数据源
* ========================
* @author linyantao
* ========================
*/
public class ThreadLocalRountingDataSource extends AbstractRoutingDataSource{
@Override
protected Object determineCurrentLookupKey() {
return DataSourceTypeManager.get();
}
}
<!--主库连接-->
<bean id="dataSourceMaster" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.master.url}" />
<property name="username" value="${jdbc.master.username}"/>
<property name="password" value="${jdbc.master.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${druid.initialSize}"/>
<property name="maxActive" value="${druid.maxActive}"/>
<property name="minIdle" value="${druid.minIdle}"/>
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${druid.maxWait}"/>
<!-- 解密密码必须要配置的项 -->
<property name="filters" value="${druid.filters}" />
<property name="connectionProperties" value="${druid.connectionProperties}" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}"/>
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}"/>
<property name="validationQuery" value="${druid.validationQuery}"/>
<property name="testWhileIdle" value="${druid.testWhileIdle}"/>
<property name="testOnBorrow" value="${druid.testOnBorrow}"/>
<property name="testOnReturn" value="${druid.testOnReturn}"/>
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="${druid.poolPreparedStatements}"/>
<property name="maxOpenPreparedStatements" value="${druid.maxOpenPreparedStatements}"/>
</bean>
<!--从库连接-->
<bean id="dataSourceSlave" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.slave.url}" />
<property name="username" value="${jdbc.slave.username}"/>
<property name="password" value="${jdbc.slave.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${druid.initialSize}"/>
<property name="maxActive" value="${druid.maxActive}"/>
<property name="minIdle" value="${druid.minIdle}"/>
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${druid.maxWait}"/>
<!-- 解密密码必须要配置的项 -->
<property name="filters" value="${druid.filters}" />
<property name="connectionProperties" value="${druid.connectionProperties}" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}"/>
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}"/>
<property name="validationQuery" value="${druid.validationQuery}"/>
<property name="testWhileIdle" value="${druid.testWhileIdle}"/>
<property name="testOnBorrow" value="${druid.testOnBorrow}"/>
<property name="testOnReturn" value="${druid.testOnReturn}"/>
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="${druid.poolPreparedStatements}"/>
<property name="maxOpenPreparedStatements" value="${druid.maxOpenPreparedStatements}"/>
</bean>
<bean id="dataSource" class="com.xxx.xxx.xxx.ThreadLocalRountingDataSource">
<property name="defaultTargetDataSource" ref="dataSourceMaster" />
<property name="targetDataSources">
<map key-type="com.xxx.xxx.xxx.DataSources">
<entry key="MASTER" value-ref="dataSourceMaster"/>
<entry key="SLAVE" value-ref="dataSourceSlave"/>
<!-- 这里还可以加多个dataSource -->
</map>
</property>
</bean>
5.jdbc.properties 配置
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.master.url=jdbc:mysql://xxx.xx.xx.xx:3306/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
jdbc.master.username=xxx
jdbc.master.password=O+mjFGxvSCYBHUJYrapflD7RhWzdCpgJ97XA8cNUC2AjKSZCZCqj/CiM+ZoDx4rG7dk4HJDxMw6G874+e4MJRw==
jdbc.slave.url=jdbc:mysql://xxx.xx.xx.xx:3306/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
jdbc.slave.username=xxx
jdbc.slave.password=O+mjFGxvSCYBHUJYrapflD7RhWzdCpgJ97XA8cNUC2AjKSZCZCqj/CiM+ZoDx4rG7dk4HJDxMw6G874+e4MJRw==
#\u914d\u7f6e\u521d\u59cb\u5316\u5927\u5c0f
druid.initialSize=2
#\u914d\u7f6e\u521d\u59cb\u5316\u6700\u5927
druid.minIdle=2
#\u914d\u7f6e\u521d\u59cb\u5316\u6700\u5c0f
druid.maxActive=30
#\u914d\u7f6e\u83b7\u53d6\u8fde\u63a5\u7b49\u5f85\u8d85\u65f6\u7684\u65f6\u95f4
druid.maxWait=60000
druid.testWhileIdle=false
#\u89e3\u5bc6\u5bc6\u7801\u5fc5\u987b\u8981\u914d\u7f6e\u7684\u9879
druid.filters=config
druid.connectionProperties=config.decrypt=true
#\u914d\u7f6e\u95f4\u9694\u591a\u4e45\u624d\u8fdb\u884c\u4e00\u6b21\u68c0\u6d4b,\u68c0\u6d4b\u9700\u8981\u5173\u95ed\u7684\u7a7a\u95f2\u8fde\u63a5,\u5355\u4f4d\u662f\u6beb\u79d2
druid.timeBetweenEvictionRunsMillis=60000
#\u914d\u7f6e\u4e00\u4e2a\u8fde\u63a5\u5728\u6c60\u4e2d\u6700\u5c0f\u751f\u5b58\u7684\u65f6\u95f4,\u5355\u4f4d\u662f\u6beb\u79d2
druid.minEvictableIdleTimeMillis=300000
druid.validationQuery=SELECT 'x'
druid.testWhileIdle=true
druid.testOnBorrow=false
druid.testOnReturn=false
druid.poolPreparedStatements=true
druid.maxOpenPreparedStatements=20
6.切换数据源
默认线程使用的主库,如果要切换到从库,请使用如下语句:
DataSourceTypeManager.set(DataSources.SLAVE);
注:我在做压测的时候有一个现象,当线程池的线程全部打满的时候,后面的请求再进去,会复用前面线程所创建的变量,线程没有进行销毁,这是个很奇怪的现象,我至今没找到完美的解决办法。
我目前采用的方案是在切换从库的时候,加上 try catch finally,在finally这块,加上以下语句:
DataSourceTypeManager.remove();
在使用完当前线程切换的数据源之后,对线程变量进行清空。
这种方式是可以实现,但是我还是觉得不够完美,希望能完美解决的人可以给小弟指点一二,在此谢过。
7.使用SpringBoot当做容器启动的朋友,如果只配置如上这些启动的时候会报错:
报错信息大概意思是:当前有两个数据源,不晓得使用哪一个。
这是由于SpringBoot 跟 Spring jdbc的JAR包冲突导致的,要解决这个问题,在主库上面加上一个属性
<bean id="dataSourceMaster" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close" primary="true">