1. 程式人生 > >SSM整合系列之 配置多資料來源 利用AOP動態切換資料來源 實現讀寫分離

SSM整合系列之 配置多資料來源 利用AOP動態切換資料來源 實現讀寫分離

摘要:在開發的專案中大都都會使用讀寫分離的技術,本人目前開發的專案接觸到的都是主從複製(一主一從),就是一個Master資料庫,一個Slave資料庫。主庫負責資料插入、更新和實時資料查詢,從庫庫負責非實時資料查詢。在實際專案應用中,都是讀多寫少,而讀取資料通常比較複雜而且耗時,SQL語句需要各種優化。採用讀寫分離技術可以有效緩解資料庫的壓力,加快響應速度,提升使用者體驗。如果隨著業務不斷擴充套件,資料不斷增加,那麼可以擴充套件多個從節點(一主多從),使用負載均衡,減輕每個從庫的查詢壓力。本文將從一主一從說開去。
SpringAop面向切面程式設計在本文就不詳細介紹,如果不清楚可自行百度Google。

1.專案搭建
可以參考本系列文章,部落格地址:https://blog.csdn.net/caiqing116/article/details/84573166
或者直接下載專案,git地址:https://github.com/gitcaiqing/SSM_DEMO.git
2.搭建MySQL主從複製
在寫本文的時候我特地搭建了2個MySQL服務,埠號分別是3306,3308
搭建MySQL主從複製可參考:https://blog.csdn.net/caiqing116/article/details/84995472
3.專案配置
(1)config/jdbc.config配置

#連線驅動
jdbc.driverClassName=com.mysql.jdbc.Driver

#埠號3306的資料來源
jdbc.url = jdbc\:mysql\://localhost\:3306/db_ssmdemo?useUnicode\=true&characterEncoding\=UTF-8&allowMultiQueries\=true
jdbc.username = root
jdbc.password = 123456

#埠號3308的資料來源
jdbc.3308.url = jdbc\:mysql\://localhost\:3308/db_ssmdemo?useUnicode\=true&characterEncoding\=UTF-8&allowMultiQueries\=true
jdbc.3308.username = root
jdbc.3308.password = 123456

#定義初始連線數 
jdbc.initialSize=2 
#定義最大連線數 
jdbc.maxActive=20
#定義最大空閒 
jdbc.maxIdle=20
#定義最小空閒 
jdbc.minIdle=1
#定義最長等待時間 
jdbc.maxWait=60000
#驗證資料庫連線的有效性
jdbc.validationQuery=select 1

(2)spring/mybatis.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd">
	
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="mapperLocations">
			 <list>
                <value>classpath:sql/*.xml</value>
            </list>
		</property>
	</bean>

	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com.ssm.mapper" />
		<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
	</bean>
	<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" scope="prototype">
  		<constructor-arg index="0" ref="sqlSessionFactory" />
	</bean>
	
	<!-- 使用annotation定義事務,使用cglib代理,解決同一service中事務方法相互呼叫的 巢狀事務失效問題 -->
	<tx:annotation-driven transaction-manager="transactionManager"  proxy-target-class="true"/>
	<!--事務配置 -->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>
	
</beans>

(3)spring/dataAccessContext.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"  xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
		http://www.springframework.org/schema/aop
		http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
	<description>資料庫、事務配置</description>
	
	<!-- 埠號3306的資料來源(主節點)-->
	<bean id="dataSource3306" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
		<property name="driverClassName" value="${jdbc.driverClassName}" />
		<property name="url" value="${jdbc.url}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
		<property name="initialSize" value="${jdbc.initialSize}" />
		<property name="maxActive" value="${jdbc.maxActive}" />
		<property name="maxIdle" value="${jdbc.maxIdle}" />
		<property name="minIdle" value="${jdbc.minIdle}" />
		<property name="maxWait" value="${jdbc.maxWait}"></property>
		<property name="validationQuery" value="${jdbc.validationQuery}" />
		<!-- 監控資料庫 -->
		<!--<property name="filters" value="mergeStat" />-->
		<property name="filters" value="stat" /> 
		<property name="connectionProperties" value="druid.stat.mergeSql=true" />  
	</bean>
	
	<!-- 埠號3308的資料來源(從節點) -->
	<bean id="dataSource3308" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
		<property name="driverClassName" value="${jdbc.driverClassName}" />
		<property name="url" value="${jdbc.3308.url}" />
		<property name="username" value="${jdbc.3308.username}" />
		<property name="password" value="${jdbc.3308.password}" />
		<property name="initialSize" value="${jdbc.initialSize}" />
		<property name="maxActive" value="${jdbc.maxActive}" />
		<property name="maxIdle" value="${jdbc.maxIdle}" />
		<property name="minIdle" value="${jdbc.minIdle}" />
		<property name="maxWait" value="${jdbc.maxWait}"></property>
		<property name="validationQuery" value="${jdbc.validationQuery}" />
		<!-- 監控資料庫 -->
		<!--<property name="filters" value="mergeStat" />-->
		<property name="filters" value="stat" /> 
		<property name="connectionProperties" value="druid.stat.mergeSql=true" />  
	</bean>
	
	<!-- 資料來源,需要自定義類繼承AbstractRoutingDataSource,實現determineCurrentLookupKey -->
	<bean id="dataSource" class="com.ssm.datasource.DynamicDataSource">
		<!-- 設定預設資料來源 -->
		<property name="defaultTargetDataSource" ref="dataSource3306"></property>
		<!-- 設定多個數據源,後臺切換資料來源key與這裡key配置需要一致 -->
		<property name="targetDataSources">
			<map key-type="java.lang.String">
				<entry key="dataSource3306" value-ref="dataSource3306"/>
				<entry key="dataSource3308" value-ref="dataSource3308"/>
			</map>
		</property>
          <!-- 實現類自定義屬性DynamicDataSource賦值切換從庫的方法名字首 -->
		<property name="methodPrefix">
			<map key-type="java.lang.String">
				<entry key="slave">
					<!-- "list","count","find","get","select","query" 等,根據開發人員方法命名習慣配置 -->
					<list>
						<value>list</value>
						<value>count</value>
						<value>find</value>
						<value>get</value>
						<value>select</value>
						<value>query</value>
					</list>
				</entry>
			</map>
		</property>
	</bean>
    
</beans>

(5)修改spring/applicationContext.xml 配置啟用AOP註解

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:context="http://www.springframework.org/schema/context"  
	   xmlns:tx="http://www.springframework.org/schema/tx"
	   xmlns:task="http://www.springframework.org/schema/task" 
	   xmlns:cache="http://www.springframework.org/schema/cache" 
	   xmlns:aop="http://www.springframework.org/schema/aop"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd 
		http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd 
		http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd ">

	<!--引入配置屬性檔案 -->
	<context:property-placeholder location="classpath*:config/*.properties" />
	
	<!-- 包掃描,掃描切面 -->
	<context:component-scan base-package="com.ssm,com.ssm.aspect">
		<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
	</context:component-scan>
	
	<!-- 基於註解 使AspectJ註解起 作用:自動為匹配的類生成代理物件 -->
  	<aop:aspectj-autoproxy proxy-target-class="true"/>
	
    <!-- 執行緒池配置 -->
    <bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
		<!-- 核心執行緒數 -->	 
		<property name="corePoolSize" value="5"/>
		<!-- 最大執行緒數 -->  
		<property name="maxPoolSize" value="50"/>
		<!-- 佇列最大長度 -->
		<property name="queueCapacity" value="1000"/>
		<!-- 執行緒池維護執行緒所允許的空閒時間,預設為60s -->
		<property name="keepAliveSeconds" value="6"/>
	</bean>
</beans>

4.自定義類繼承AbstractRoutingDataSource,實現determineCurrentLookupKey,並且自定義屬性methodPrefix用於設定切換到從節點的方法名字首
AbstractRoutingDataSource的相關原始碼在我的另一篇文章中有介紹,參考博文:https://blog.csdn.net/caiqing116/article/details/84979682
我們實現determineCurrentLookupKey方法,返回值為dataSource3306或dataSource3308,定義屬性Map<String,List> methodPrefix用於方法名字首賦值,具體實現如下:

package com.ssm.datasource;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 定義動態資料來源,整合spring提供的AbstractRoutingDataSource,實現determineCurrentLookupKey
 * @author https://blog.csdn.net/caiqing116
 */
public class DynamicDataSource extends AbstractRoutingDataSource{
	
	private static final Logger log = LoggerFactory.getLogger(DynamicDataSource.class);
	
	/*
	 * 定義一個方法字首methodPrefix屬性,通過配置檔案賦值,該map儲存資料來源和方法字首的值形如形如
	 * {"slave",["list","count","find","get","select","query","等不一一列舉"]}
	 * 這裡的方法字首根據開發人員的習慣可自行配置
	*/
	public static Map<String,List<String>> methodPrefix = new HashMap<String, List<String>>();
	
	//設定切換到從庫的方法字首
	@SuppressWarnings("static-access")
	public void setMethodPrefix(Map<String, List<String>> methodPrefix) {
		this.methodPrefix = methodPrefix;
	}

	@Override
	protected Object determineCurrentLookupKey() {
		Object dataType = DataSourceContextHolder.getDataType();
		log.info("當前資料來源:{}",dataType);
		return dataType;
	}
 
}

5.藉助ThreadLocal類,通過ThreadLocal類傳遞引數設定資料來源,並且新增方法isSlave根據引數判斷是否切換到從節點

package com.ssm.datasource;

import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;

/**
 * 切換資料來源,清除資料來源資訊等
 * @author https://blog.csdn.net/caiqing116
 */
public class DataSourceContextHolder {
	
	
	//定義資料來源標識和配置檔案dataAccessContext.xml配置的bean id一致
	public static final String DATASOURCE = "dataSource3306";
	public static final String DATASOURCE3308 = "dataSource3308";
	//定義從庫標識,和配置
	public static final String SLAVE = "slave";

	private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
	
	/**
	 * 設定當前資料來源
	 * @param dataType 資料元型別 DATASOURCE | DATASOURCE3308
	 */
	public static void setDatatype(String dataType) {
		contextHolder.set(dataType);
	}
	
	/**
	 * 獲取當前資料來源
	 * @return
	 */
	public static String getDataType(){
		return contextHolder.get();
	}
	
	/**
	 * 清除
	 */
	public static void clear() {
		contextHolder.remove();
	}
	
	/**
	 * 切到3306埠資料來源
	 */
	public static void mark3306() {
		setDatatype(DATASOURCE);
	}
	
	/**
	 * 切到3308埠資料來源
	 */
	public static void mark3308() {
		setDatatype(DATASOURCE3308);
	}

	/**
	 * 判斷是否進入從庫
	 * @return
	 */
	public static boolean isSlave(String methodName) {
		//未配置切換到從庫的方法名字首,返回false,預設切換到主庫
		Map<String, List<String>> methodPrefix = DynamicDataSource.methodPrefix;
		if(methodPrefix == null) {
			return false;
		}
		List<String> methodPrefixs = methodPrefix.get(SLAVE);
		if(methodPrefixs.isEmpty()) {
			return false;
		}
		//方法名字首和配置的相同則切換到從庫
		if(StringUtils.startsWithAny(methodName, methodPrefixs.toArray(new CharSequence[methodPrefixs.size()]))) {
			return true;
		}
		return false;
	}
	
}

6.定義切面類
建立包com.ssm.aspect,建立類DataSourceAspect.java,具體實現如下

package com.ssm.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.ssm.datasource.DataSourceContextHolder;

/**
 * 定義一個數據源切換的切面
 * @aspect 註釋對於在類路徑中自動檢測是不夠的:為了達到這個目的,您需要新增一個單獨的@component註解
 * @author https://blog.csdn.net/caiqing116 
 */
@Aspect
@Component
public class DataSourceAspect {

	private static final Logger log = LoggerFactory.getLogger(DataSourceAspect.class);
	
	/**
	 * 定義切面
	 * 跟資料來源相關的操作都是在service包下
	 */
	@Pointcut("execution(* com.ssm.service.*.*(..))")
	public void servicePoint(){}
	
	/**
	 * 根據切入點進行資料來源的切換
	 */
	@Before("servicePoint()")
	public void before(JoinPoint joinpoint){
		String className = joinpoint.getTarget().getClass().getName();
		String methodName = joinpoint.getSignature().getName();
		//判斷如果方法名開頭符合從庫,則切換到從庫(埠號3308的服務)
		if(DataSourceContextHolder.isSlave(methodName)) {
			log.info("執行類:"+className+" 方法:"+methodName+",切換到從庫");
			DataSourceContextHolder.mark3308();
		}else {
			log.info("執行類:"+className+" 方法:"+methodName+",切換到主庫");
			DataSourceContextHolder.mark3306();
		}
	}
}

7.測試
編寫增刪查改Service和實現類,這些在之前的文章中有介紹,就不重複介紹了

com/ssm/service/BasicUserService.java
package com.ssm.service;

import java.util.List;

import com.ssm.entity.BasicUser;
import com.ssm.entity.Page;

/**
 * 使用者Service
 * @author https://blog.csdn.net/caiqing116
 *
 */
public interface BasicUserService {
	
	Integer insert(BasicUser basicUser);
	
	Integer deleteById(Integer id);
	
	BasicUser selectById(Integer id);
	
	Integer updateById(BasicUser basicUser);
	
	BasicUser selectByUsername(String username);
}

com/ssm/service/impl/BasicUserServiceImpl.java

package com.ssm.service.impl;


import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.ssm.entity.BasicUser;
import com.ssm.entity.Page;
import com.ssm.mapper.BasicUserMapper;
import com.ssm.service.BasicUserService;

/**
 * 使用者Service實現類
 * @author https://blog.csdn.net/caiqing116
 *
 */
@Service
public class BasicUserServiceImpl implements BasicUserService{

	@Autowired
	private BasicUserMapper basicUserMapper;
	
	/**
	 * 插入使用者
	 */
	public Integer insert(BasicUser basicUser) {
		return basicUserMapper.insertSelective(basicUser);
	}

	/**
	 * 根據id刪除
	 */
	public Integer deleteById(Integer id) {
		return basicUserMapper.deleteByPrimaryKey(id);
	}

	/**
	 * 根據id查詢
	 */
	public BasicUser selectById(Integer id) {
		return basicUserMapper.selectByPrimaryKey(id);
	}

	/**
	 * 根據id更新
	 */
	public Integer updateById(BasicUser basicUser) {
		return basicUserMapper.updateByPrimaryKeySelective(basicUser);
	}

	/**
	 * 根據使用者名稱查詢
	 */
	public BasicUser selectByUsername(String username) {
		return basicUserMapper.selectByUsername(username);
	}

}

Service測試類

package com.ssm.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.ssm.entity.BasicUser;
import com.ssm.util.EncryptKit;
import com.ssm.util.UuidUtil;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:spring/*.xml","classpath:servlet/*.xml" })
public class BasicUserServiceTest {
	
	private static final Logger log = LoggerFactory.getLogger(BasicUserServiceTest.class);
	
	@Autowired
	private BasicUserService basicUserService;

	@Test
	public void testInsert() {
		BasicUser basicUser = new BasicUser();
		basicUser.setUtype(1);
		basicUser.setUserid(UuidUtil.getUuid());
		basicUser.setUsername("嘆稀奇");
		basicUser.setRealname("嘆稀奇");
		basicUser.setPassword(EncryptKit.MD5("123456"));
		basicUser.setAge(18);
		//手動切換資料來源測試程式碼
		//切換到3308插入資料
		/*DataSourceContextHolder.clear();
		DataSourceContextHolder.mark3306();
		int result = basicUserService.insert(basicUser);
		DataSourceContextHolder.clear();*/
		//自動切換資料來源測試
		int result = basicUserService.insert(basicUser);
		log.info("basicUser:"+basicUser);
		log.info("插入行數:"+result);
	}

	@Test
	public void testDeleteById() {
		int result = basicUserService.deleteById(1);
		log.info("刪除行數:"+result);
	}

	@Test
	public void testSelectById() {
		BasicUser basicUser = basicUserService.selectById(1);
		log.info("basicUser:"+basicUser);
	}

	@Test
	public void testUpdateById() {
		BasicUser basicUser = new BasicUser();
		basicUser.setId(1);
		basicUser.setAge(19);
		int result = basicUserService.updateById(basicUser);
		log.info("更新行數:"+result);
	}

	@Test
	public void testSelectByUsername() {
		String username = "墨傾池";
		BasicUser basicUser = basicUserService.selectByUsername(username);
		log.info("basicUser:"+basicUser);
	}

}

在測試中,我們看下日誌檔案的列印
測試方法: testSelectById,日誌如下:

[INFO] 執行類:com.ssm.service.impl.BasicUserServiceImpl 方法:selectById,切換到從庫 
[INFO] 當前資料來源:dataSource3308 

測試方法:insert,日誌如下:

[INFO] 執行類:com.ssm.service.impl.BasicUserServiceImpl,方法:insert,切換到主庫 
[INFO] 當前資料來源:dataSource3306 

在測試插入的時候,我們可以分別查詢下主庫和從庫的資料,可以驗證主從是否正常同步,如下圖說明讀寫分離是成功的
在這裡插入圖片描述
在這裡插入圖片描述

8.Service層呼叫內部方法無法切換資料來源問題

/**
 * 根據id查詢
 */
public BasicUser selectById(Integer id) {
	//該方法內部含有一些其他業務處理,如插入刪除更新操作等等需要,需要切換到主節點
	//在這裡進行了Service層內部方法呼叫
	//一般理解,這裡會切換到從庫,實際是不會的
	log.info("執行刪除操作開始");
	this.deleteById(3);
	log.info("執行刪除操作結束");
	
	return basicUserMapper.selectByPrimaryKey(id);
}

執行測試方法testSelectById,日誌如下:

[INFO] 執行類:com.ssm.service.impl.BasicUserServiceImpl 方法:selectById,切換到從庫 
[INFO] 執行刪除操作開始 
[INFO] 進入方法deleteById..... 
[INFO] 執行刪除操作結束 
[INFO] 當前資料來源:dataSource3308 
[INFO] basicUser:BasicUser [id=1, userid=bc8bbfb770ee4f2c9ba0f988a7a92d4f, utype=1, username=墨傾池, password=E10ADC3949BA59ABBE56E057F20F883E, headimg=null, realname=雲天望垂, sex=null, age=19, mobile=null, email=null, credate=Fri Dec 07 14:50:20 CST 2018, upddate=null] 

可以看出在進入方法deleteById並沒有切換資料來源。這是為什麼呢,原來Service中如此呼叫並非呼叫的是代理類中的方法,然而必須要呼叫代理類才會被切進去。
在這裡插入圖片描述
在這裡插入圖片描述

既然只有呼叫代理類的方法才能切入,那我們就拿到代理類,再進行呼叫。修改selectById方法。

/**
 * 根據id查詢
 */
public BasicUser selectById(Integer id) {
	//該方法內部含有一些其他業務處理,如插入刪除更新操作等等需要,需要切換到主節點
	//在這裡進行了Service層內部方法呼叫
	//一般理解,這裡會切換到從庫,實際是不會的
	log.info("執行刪除操作開始");
	//this.deleteById(3);
	
	//獲取代理類
	BasicUserService proxy = ((BasicUserService)AopContext.currentProxy());
	proxy.deleteById(3);
	log.info("執行刪除操作結束");
	
	return basicUserMapper.selectByPrimaryKey(id);
}

執行後報錯,提示需要設定expose-proxy屬性為true,將代理暴露出來

java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.
at org.springframework.aop.framework.AopContext.currentProxy(AopContext.java:64)
at com.ssm.service.impl.BasicUserServiceImpl.selectById(BasicUserServiceImpl.java:59)
at com.ssm.service.impl.BasicUserServiceImpl$$FastClassBySpringCGLIB$$f982c283.invoke(<generated>)

我們修改下spring/applicationContext.xml

<!-- 基於註解 使AspectJ註解起 作用:自動為匹配的類生成代理物件 -->
<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>

再次執行我們檢視BasicUserService物件為代理物件,如下圖
在這裡插入圖片描述
檢視日誌如下,執行刪除操作的時候資料來源從從庫切到了主庫

[INFO] 執行類:com.ssm.service.impl.BasicUserServiceImpl 方法:selectById,切換到從庫 
[INFO] 執行刪除操作開始 
[INFO] 執行類:com.ssm.service.impl.BasicUserServiceImpl 方法:deleteById,切換到主庫 
[INFO] 進入方法deleteById..... 
[INFO] 執行刪除操作結束 
[INFO] 當前資料來源:dataSource3306