今晚加班做完手头上的事情想起来写一篇笔记记录一下前段时间在项目中实现的数据源切换过程吧/
我们项目做的事SASS,所有的客户数据都是在同一个库中的,但是能根据公司区分的。最近我们想给某些VIP客户单独分离库,避免数据之间的交叉感染和提高效率,就需要在项目中根据公司来切换主库和VIP库了。主要的流程还是很简单的,首先我们在请求来临时候先拦截我们vip客户的公司id,根据公司id来制定切换到哪一个库,如果不是VIP就用默认的主库,后续一系列操作请求中都根据这个流程来。话不多说,来看看代码例子吧,首先我们拿了一个springboot小项目做了个演练。后面才在们自己的项目中改造的。
首先在配置数据库xml文件中将原来的dataSource 指向的数据源成两个,当然
这个是项目基础配置,当然像${jdbc.salve.url} ,${jdbc.master.url}这些是配置在每个环境的properties中的,读取出来就好
#mysql salvedatabase setting
jdbc.salve.type=mysql
jdbc.salve.driver=com.mysql.jdbc.Driver
jdbc.salve.url=${jdbc.salve.url}
jdbc.salve.username=${jdbc.salve.username}
jdbc.salve.password=${jdbc.salve.password}
#mysql masterdatabase setting
jdbc.master.type=mysql
jdbc.master.driver=com.mysql.jdbc.Driver
jdbc.master.url=${jdbc.master.url}
jdbc.master.username=${jdbc.master.username}
jdbc.master.password=${jdbc.master.password}
jdbc.url=${jdbc.url}
jdbc.username=${jdbc.username}
jdbc.password=${jdbc.password}
之后我们要改好springContext的配置文件,不再是指向单数据源,值得提的是ThreadLocalRountingDataSource是自己的写的一个类管理数据源的,继承了AbstractRoutingDataSource ,所以才会有targetDataSources,defaultTargetDataSource这些个配置。
其中com.**.comm.aspect.DataSources 这个类主要作用是让spring 加载进两个数据源属性,就像springboot中直接读取配置文件中的配置生成 spring管理的对象道理是一样,分别是MASTER,SLAVE。我们这里用的是枚举。
<bean id="dataSource" class="com.**.comm.aspect.ThreadLocalRountingDataSource">
<property name="targetDataSources" >
<map key-type="com.**.comm.aspect.DataSources">
<entry key="MASTER" value-ref="dataSourceMaster" />
<entry key="SLAVE" value-ref="dataSourceSlave"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSourceMaster"></property>
</bean>
<!--多数据源 -->
<!-- 配置数据源Slave -->
<bean id="dataSourceSlave" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
<property name="driverClassName" value="${jdbc.salve.driver}" />
<!-- 基本属性 url、user、password -->
<property name="url" value="${jdbc.salve.url}" />
<property name="username" value="${jdbc.salve.username}" />
<property name="password" value="${jdbc.salve.password}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${jdbc.pool.init}" />
<property name="minIdle" value="${jdbc.pool.minIdle}" />
<property name="maxActive" value="${jdbc.pool.maxActive}" />
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="60000" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="validationQuery" value="${jdbc.testSql}" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小(Oracle使用)
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" /> -->
<!-- 配置监控统计拦截的filters -->
<property name="filters" value="stat" />
</bean>
<!-- 配置数据源Master -->
<bean id="dataSourceMaster" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
<property name="driverClassName" value="${jdbc.master.driver}" />
<!-- 基本属性 url、user、password -->
<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="${jdbc.pool.init}" />
<property name="minIdle" value="${jdbc.pool.minIdle}" />
<property name="maxActive" value="${jdbc.pool.maxActive}" />
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="60000" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="validationQuery" value="${jdbc.testSql}" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小(Oracle使用)
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" /> -->
<!-- 配置监控统计拦截的filters -->
<property name="filters" value="stat" />
</bean>
以下是那个dataSource枚举类,为什么继承supplier是为了后面在用threadLocal时候好给他一个初始值
public enum DataSources implements Supplier {
MASTER,SLAVE;
@Override
public Object get() {
return MASTER;
}
自己写的一个管理数据源的继承了那个AbstractRoutingDataSource的类,会实时监测当前连接的数据源
public class ThreadLocalRountingDataSource extends AbstractRoutingDataSource {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
protected Object determineCurrentLookupKey() {
/*if(DataSourceTypeManager.get() != null){
logger.debug("当前连接的数据库是: [{}]", DataSourceTypeManager.get().name());
}*/
return DataSourceTypeManager.get();
}
当然下面这个才是最核心的一个,使用的线程绑定当前变量每个线程读取自己的一份存储值实现每次请求告诉它该用的变量。
public class DataSourceTypeManager {
/**
* 默认初始化的数据源是 MASTER
* */
private static Lock lock = new ReentrantLock();
private static final ThreadLocal<DataSources> dataSourceTypes = ThreadLocal.withInitial(DataSources.MASTER);
public static DataSources get(){
DataSources dataSources = dataSourceTypes.get();
return dataSources;
}
public static void set(DataSources dataSourceType){
// dataSourceTypes.remove();
// lock.lock();
dataSourceTypes.set(dataSourceType);
// lock.unlock();
}
public static void useMaster(){
set(DataSources.MASTER);
}
public static void useSlave(){
try {
set(DataSources.SLAVE);
}catch (Exception e){
e.printStackTrace();
}finally {
}
}
/**
* 清除
*/
public static void clear(){
dataSourceTypes.remove();
}
public void reset(){
System.out.println("连接结束后恢复master");
//dataSourceTypes.set(DataSources.MASTER);
}
实际上更多的业务场景是使用的切面,根据方法或者包什么的去读库写库分离啊什么的,所以配置一个切面是少不了的。但是我们业务上要从进来的入口出就开始切换,而且每个接口都带上了公司id信息了所以这个切面并没有用上,我们在请求是就加了一个拦截器,根据公司id手动切换了数据源。
@Aspect
@Component
public class DataSourceAspect {
// 现在是在拦截器中提前完成了切换数据了,此切面暂未启用
@Pointcut("execution( * com.**.mods..mapper.*.*(..))")
public void dataSourceCheck(){
}
@Before("dataSourceCheck()")
public void before(JoinPoint jp){
System.out.println("在执行dataSourceCheck 之前执行了该方法!");
}
@After("dataSourceCheck()")
public void restoreDataSource(JoinPoint point) {
System.out.println("执行完成后重置数据源");
}
}
拦截器的代码也贴上吧:记得在springmvc的配置中要配好拦截url和放行的部分
public class checkCompanyIdInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String companyId = request.getParameter("InterceptorCompanyId");
if(StringUtils.isBlank(companyId)){
System.out.println("请求携带的InterceptorCompanyId="+companyId);
AjaxJsonResponse ajaxJsonResponse = new AjaxJsonResponse(false, "1", "前端的兄弟们,你是不是又忘记了传InterceptorCompanyId(公司id)了?", null);
response.getWriter().write(ajaxJsonResponse.getJsonStr());
return false;
}
String targetCompanyId = Global.getConfig("target.companyId");
if(StringUtils.isNotBlank(targetCompanyId) && targetCompanyId .equals(companyId)){
// 切换到salve库去
DataSourceTypeManager.useSlave();
}else {
// 其他的任何情况都去主库
DataSourceTypeManager.useMaster();
}
System.out.println("请求携带的InterceptorCompanyId=:"+companyId);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// System.out.println("请求携带的InterceptorCompanyId");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// System.out.println("请求携带的companyId");
}