2019獨角獸企業重金招聘Python工程師標準>>>
最近在做保證金余額查詢優化,在項目啟動時候需要把余額全量加載到本地緩存,因為需要全量查詢所有騎手的保證金余額,為了不影響主數據庫的性能,考慮把這個查詢走從庫。所以涉及到需要在一個項目中配置多數據源,并且能夠動態切換。
設計總體思路
Spring-Boot+AOP方式實現多數據源切換,繼承AbstractRoutingDataSource實現數據源動態的獲取,在service層使用注解指定數據源。
步驟
一、多數據源配置
在application.properties中,我們的配置是這樣的
#主數據源druid.master.url=jdbc:mysql://url/masterdb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNulldruid.master.username=xxxdruid.master.password=123druid.master.driver-class-name=com.mysql.jdbc.Driverdruid.master.max-wait=5000druid.master.max-active=100druid.master.test-on-borrow=truedruid.master.validation-query=SELECT 1#從數據源druid.slave.url=jdbc:mysql://url/slavedb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNulldruid.slave.username=xxxdruid.slave.password=123druid.slave.driver-class-name=com.mysql.jdbc.Driverdruid.slave.max-wait=5000druid.slave.max-active=100druid.slave.test-on-borrow=truedruid.slave.validation-query=SELECT 1
讀取配置
<!-- master數據源 --><bean primary="true" id="masterdb" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"><!-- 基本屬性 url、user、password --><property name="driverClassName" value="com.mysql.jdbc.Driver"/><property name="url" value="${druid.master.url}"/><property name="username" value="${druid.master.username}"/><property name="password" value="${druid.master.password}"/><!-- 配置初始化最大 --><property name="maxActive" value="${druid.master.max-active}"/><!-- 配置獲取連接等待超時的時間 --><property name="maxWait" value="${druid.master.max-wait}"/><property name="validationQuery" value="${druid.master.validation-query}"/><property name="testOnBorrow" value="${druid.master.test-on-borrow}"/></bean><!-- slave數據源 --><bean primary="true" id="slavedb" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"><!-- 基本屬性 url、user、password --><property name="driverClassName" value="com.mysql.jdbc.Driver"/><property name="url" value="${druid.slave.url}"/><property name="username" value="${druid.slave.username}"/><property name="password" value="${druid.slave.password}"/><!-- 配置初始化大小、最小、最大 --><property name="maxActive" value="${druid.slave.max-active}"/><!-- 配置獲取連接等待超時的時間 --><property name="maxWait" value="${druid.slave.max-wait}"/><property name="validationQuery" value="${druid.slave.validation-query}"/><property name="testOnBorrow" value="${druid.slave.test-on-borrow}"/></bean><!-- 動態數據源,根據service接口上的注解來決定取哪個數據源 --><bean id="dataSource" class="datasource.DynamicDataSource"><property name="targetDataSources"><map key-type="java.lang.String"><entry key="slave" value-ref="slavedb"/><entry key="master" value-ref="masterdb"/></map></property><property name="defaultTargetDataSource" ref="masterdb"/></bean><!-- Spring JdbcTemplate --><bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"><property name="dataSource" ref="dataSource" /></bean><!-- Spring事務管理器 --><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource" /></bean><bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"><property name="transactionManager" ref="transactionManager"/></bean><tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" order="2" /><!-- depositdbSqlSessionFactory --><bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><property name="dataSource" ref="dataSource" /><property name="mapperLocations" value="classpath*:mapper-xxdb/*Mapper*.xml" /></bean><bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"><property name="basePackage" value="xxdb.mapper"/><property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/></bean>
二. 數據源動態切換類
動態數據源切換是基于AOP的,所以我們需要聲明一個AOP切面,并在切面前做數據源切換,切面完成后移除數據源名稱。
@Order(1) //設置AOP執行順序(需要在事務之前,否則事務只發生在默認庫中)@Aspect@Componentpublic class DataSourceAspect {private Logger logger = LoggerFactory.getLogger(this.getClass());//切點@Pointcut("execution(* com.xxx.service.*.*(..))")public void aspect() { }@Before("aspect()")private void before(JoinPoint point) {Object target = point.getTarget();String method = point.getSignature().getName();Class<?> classz = target.getClass();// 獲取目標類Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();try {Method m = classz.getMethod(method, parameterTypes);if (m != null && m.isAnnotationPresent(MyDataSource.class)) {MyDataSource data = m.getAnnotation(MyDataSource.class);logger.info("method :{},datasource:{}",m.getName() ,data.value().getName());JdbcContextHolder.putDataSource(data.value().getName());// 數據源放到當前線程中}} catch (Exception e) {logger.error("get datasource error ",e);//默認選擇masterJdbcContextHolder.putDataSource(DataSourceType.Master.getName());// 數據源放到當前線程中}}@AfterReturning("aspect()")public void after(JoinPoint point) {JdbcContextHolder.clearDataSource();}}
三、數據源管理類
public class JdbcContextHolder {private final static ThreadLocal<String> local = new ThreadLocal<>();public static void putDataSource(String name) {local.set(name);}public static String getDataSource() {return local.get();}public static void clearDataSource() {local.remove();}}
四、動態數據源
spring為我們提供了AbstractRoutingDataSource,即帶路由的數據源。繼承后我們需要實現它的determineCurrentLookupKey(),該方法用于自定義實際數據源名稱的路由選擇方法,由于我們將信息保存到了ThreadLocal中,所以只需要從中拿出來即可。
public class DynamicDataSource extends AbstractRoutingDataSource {private Logger logger = LoggerFactory.getLogger(this.getClass());@Overrideprotected Object determineCurrentLookupKey() {String dataSource = JdbcContextHolder.getDataSource();logger.info("數據源為{}",dataSource);return dataSource;}}
五、數據源注解和枚舉
我們切換數據源時,一般都是在調用具體接口的方法前實現,所以我們定義一個方法注解,當AOP檢測到方法上有該注解時,根據注解中value對應的名稱進行切換。
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface MyDataSource {DataSourceType value();}
public enum DataSourceType {// 主表Master("master"),// 從表Slave("slave");private String name;private DataSourceType(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}}
六、切點注解
由于我們的動態數據源配置了默認庫,所以如果方法是操作默認庫的可以不需要注解,如果要操作非默認數據源,我們需要在方法上添加@MyDataSource("數據源名稱")注解,這樣就可以利用AOP實現動態切換了
@Componentpublic class xxxServiceImpl {@Resourceprivate XxxMapperExt xxxMapperExt;@MyDataSource(value= DataSourceType.Slave)public List<Object> getAll(){return xxxMapperExt.getAll();}}