通過昨天學習的自定義配置注釋的知識,探索了解一下web主從數據庫的配置:
背景:
主從數據庫:主要是數據上的讀寫分離;
數據庫的讀寫分離的好處?
1. 將讀操作和寫操作分離到不同的數據庫上,避免主服務器出現性能瓶頸;
2. 主服務器進行寫操作時,不影響查詢應用服務器的查詢性能,降低阻塞,提高並發;
3. 數據擁有多個容災副本,提高數據安全性,同時當主服務器故障時,可立即切換到其他服務器,提高系統可用性;
讀寫分離的基本原理就是讓主數據庫處理事務性增、改、刪操作(INSERT、UPDATE、DELETE)操作,而從數據庫處理SELECT查詢操作。數據庫復制被用來把事務性操作導致的變更同步到其他從數據庫。以SQL為例,主庫負責寫數據、讀數據。讀庫僅負責讀數據。每次有寫庫操作,同步更新到讀庫。寫庫就一個,讀庫可以有多個,采用日志同步的方式實現主庫和多個讀庫的數據同步。
配置步驟:
此文暫時不包含數據同步問題,數據同步移步
1.配置數據源
<bean id="masterDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" > <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="${database.master.jdbcUrl}"></property> <property name="user" value="${database.master.user}" /> <property name="password" value="${database.master.password}" /> <property name="maxPoolSize" value="${database.master.maxPoolSize}"></property> <property name="minPoolSize" value="${database.master.minPoolSize}"></property> <property name="maxIdleTime" value="${database.master.maxIdleTime}"></property> <property name="idleConnectionTestPeriod" value="${database.master.idleConnectionTestPeriod}"></property> </bean> <bean id="slaveDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" > <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="${database.slave.jdbcUrl}"></property> <property name="user" value="${database.slave.user}" /> <property name="password" value="${database.slave.password}" /> <property name="maxPoolSize" value="${database.slave.maxPoolSize}"></property> <property name="minPoolSize" value="${database.slave.minPoolSize}"></property> <property name="maxIdleTime" value="${database.slave.maxIdleTime}"></property> <property name="idleConnectionTestPeriod" value="${database.slave.idleConnectionTestPeriod}"></property> </bean>
2.配置切換數據源類
xml
<-- 此類繼承了AbstractRoutingDataSource 類,且 AbstractRoutingDataSource類 為Spring jdbc中提供的類,需要重寫其中的determineCurrentLookupKey()方法,獲取當前切換到的數據庫源名稱--> <bean id="dataSource" class="com.imzhitu.admin.common.dataSourceMasterSlave.DynamicDataSource"> <property name="targetDataSources"><-- 將數據源置入到類中,通過之后的aop攔截到的數據庫名稱,匹配到指定的數據源進而鏈接 --> <map key-type="java.lang.String"> <entry key="master" value-ref="masterDataSource"/> <entry key="slave" value-ref="slaveDataSource"/> </map> </property> <property name="defaultTargetDataSource" ref="masterDataSource"/> </bean>
DynamicDataSource.java
package com.imzhitu.admin.common.dataSourceMasterSlave; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 動態數據源,動態獲取數據源的實現 * */ public class DynamicDataSource extends AbstractRoutingDataSource{ /** * 用戶返回當且切換到的數據庫 */ @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceHolder.getDataSource();//DynamicDataSourceHolder有獲取和設置當前數據庫的方法get & put } }
DynamicDataSourceHolder.java
package com.imzhitu.admin.common.dataSourceMasterSlave; /** * 動態數據源holder * */ public class DynamicDataSourceHolder { public static final ThreadLocal<String> holder = new ThreadLocal<String>(); public static void putDataSource(String name) { holder.set(name); } public static String getDataSource() { return holder.get(); } }
AbstractRoutingDataSource.java <簡化版>
/* * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.jdbc.datasource.lookup; import java.sql.Connection; import java.sql.SQLException; import java.util.HashMap; import java.util.Map; import javax.sql.DataSource; import org.springframework.beans.factory.InitializingBean; import org.springframework.jdbc.datasource.AbstractDataSource; import org.springframework.util.Assert; /** * Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()} * calls to one of various target DataSources based on a lookup key. The latter is usually * (but not necessarily) determined through some thread-bound transaction context. * * @author Juergen Hoeller * @since 2.0.1 * @see #setTargetDataSources * @see #setDefaultTargetDataSource * @see #determineCurrentLookupKey() */ public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { private Map<Object, Object> targetDataSources;//所有數據源,在xml中有相應的配置 private Object defaultTargetDataSource;//默認數據源,在xml中有相應的配置private Map<Object, DataSource> resolvedDataSources;//將targetDataSources值傳入其中,做值的傳遞 private DataSource resolvedDefaultDataSource;//同targetDataSources /** * Specify the map of target DataSources, with the lookup key as key. * The mapped value can either be a corresponding {@link javax.sql.DataSource} * instance or a data source name String (to be resolved via a * {@link #setDataSourceLookup DataSourceLookup}). * <p>The key can be of arbitrary type; this class implements the * generic lookup process only. The concrete key representation will * be handled by {@link #resolveSpecifiedLookupKey(Object)} and * {@link #determineCurrentLookupKey()}. */ public void setTargetDataSources(Map<Object, Object> targetDataSources) { this.targetDataSources = targetDataSources; } /** * Specify the default target DataSource, if any. * <p>The mapped value can either be a corresponding {@link javax.sql.DataSource} * instance or a data source name String (to be resolved via a * {@link #setDataSourceLookup DataSourceLookup}). * <p>This DataSource will be used as target if none of the keyed * {@link #setTargetDataSources targetDataSources} match the * {@link #determineCurrentLookupKey()} current lookup key. */ public void setDefaultTargetDataSource(Object defaultTargetDataSource) { this.defaultTargetDataSource = defaultTargetDataSource; } /** * Specify whether to apply a lenient fallback to the default DataSource * if no specific DataSource could be found for the current lookup key. * <p>Default is "true", accepting lookup keys without a corresponding entry * in the target DataSource map - simply falling back to the default DataSource * in that case. * <p>Switch this flag to "false" if you would prefer the fallback to only apply * if the lookup key was {@code null}. Lookup keys without a DataSource * entry will then lead to an IllegalStateException. * @see #setTargetDataSources * @see #setDefaultTargetDataSource * @see #determineCurrentLookupKey() */ public void setLenientFallback(boolean lenientFallback) { this.lenientFallback = lenientFallback; } /** * Set the DataSourceLookup implementation to use for resolving data source * name Strings in the {@link #setTargetDataSources targetDataSources} map. * <p>Default is a {@link JndiDataSourceLookup}, allowing the JNDI names * of application server DataSources to be specified directly. */ public void setDataSourceLookup(DataSourceLookup dataSourceLookup) { this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup()); } @Override public void afterPropertiesSet() { if (this.targetDataSources == null) { throw new IllegalArgumentException("Property 'targetDataSources' is required"); }
//*** 將targetDataSources的值傳遞給resolvedDataSources this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size()); for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) { Object lookupKey = resolveSpecifiedLookupKey(entry.getKey()); DataSource dataSource = resolveSpecifiedDataSource(entry.getValue()); this.resolvedDataSources.put(lookupKey, dataSource); }if (this.defaultTargetDataSource != null) { this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource); }
//*** } /** * Resolve the given lookup key object, as specified in the * {@link #setTargetDataSources targetDataSources} map, into * the actual lookup key to be used for matching with the * {@link #determineCurrentLookupKey() current lookup key}. * <p>The default implementation simply returns the given key as-is. * @param lookupKey the lookup key object as specified by the user * @return the lookup key as needed for matching */ protected Object resolveSpecifiedLookupKey(Object lookupKey) { return lookupKey; } /** * Resolve the specified data source object into a DataSource instance. * <p>The default implementation handles DataSource instances and data source * names (to be resolved via a {@link #setDataSourceLookup DataSourceLookup}). * @param dataSource the data source value object as specified in the * {@link #setTargetDataSources targetDataSources} map * @return the resolved DataSource (never {@code null}) * @throws IllegalArgumentException in case of an unsupported value type */ protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException { if (dataSource instanceof DataSource) { return (DataSource) dataSource; } else if (dataSource instanceof String) { return this.dataSourceLookup.getDataSource((String) dataSource); } else { throw new IllegalArgumentException( "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource); } }
/** * Retrieve the current target DataSource. Determines the * {@link #determineCurrentLookupKey() current lookup key}, performs * a lookup in the {@link #setTargetDataSources targetDataSources} map, * falls back to the specified * {@link #setDefaultTargetDataSource default target DataSource} if necessary. * @see #determineCurrentLookupKey() */
//通過調用determineCurrentLookupKey()獲取當前數據源名稱,並匹配到相應的數據源返回;
//此方法在本類的 getConnection() 方法中調用,獲取當前數據源的連接Connection,從而進行數據庫操作 protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; } /** * Determine the current lookup key. This will typically be * implemented to check a thread-bound transaction context. * <p>Allows for arbitrary keys. The returned key needs * to match the stored lookup key type, as resolved by the * {@link #resolveSpecifiedLookupKey} method. */
//獲取當前數據源的名稱;在本類的子類中需要重寫此方法 protected abstract Object determineCurrentLookupKey(); }
3.配置AOP,自定義注釋,獲取訪問是應該鏈接的數據庫源名
xml
<!-- 配置數據庫注解aop --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> <bean id="manyDataSourceAspect" class="com.imzhitu.admin.common.dataSourceMasterSlave.DataSourceAspect"/> <aop:config> <aop:aspect id="dataSourceCut" ref="manyDataSourceAspect"><-- 切面類 --> <aop:pointcut expression="execution(* com.imzhitu.admin..*.mapper.*.*(..))" id="dataSourceCutPoint"/><-- 配置切點 --> <aop:before pointcut-ref="dataSourceCutPoint" method="before"/><-- 切面執行方法 --> </aop:aspect> </aop:config>
DataSourceAspect.java
package com.imzhitu.admin.common.dataSourceMasterSlave; import java.lang.reflect.Method; import org.apache.log4j.Logger; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; /** * 數據源動態選擇切面 * */ public class DataSourceAspect { private Logger log = Logger.getLogger(DataSourceAspect.class); public void before(JoinPoint point){ Object target = point.getTarget(); String method = point.getSignature().getName(); Class<?>[] classz = target.getClass().getInterfaces(); Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes(); try { Method m = classz[0].getMethod(method, parameterTypes); if ( m != null && m.isAnnotationPresent(DataSource.class)) { DataSource data = m.getAnnotation(DataSource.class);//獲取訪問mapper中的注釋 DynamicDataSourceHolder.putDataSource(data.value());//獲取注釋中的value值,確定訪問的數據源 if(log.isDebugEnabled()){ log.debug("DataSourceAspect:======================="+data.value()); } } } catch (Exception e) { // handle exception e.printStackTrace(); } } }
DataSource.java<-- 自定義注釋 -->
package com.imzhitu.admin.common.dataSourceMasterSlave; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 數據庫annotation定義 * @DataSource('master') / @DataSource('slave') * */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DataSource { String value();//唯一值,所以注釋中沒有寫@DataSource(value = 'master');也可以寫成 String value() defalut "master";即默認訪問主數據庫
}
mapper.java<讀寫分離>
package com.imzhitu.admin.ztworld.mapper; import java.util.List; import org.apache.ibatis.annotations.Param; import com.hts.web.common.pojo.HTWorldSubtitleDto; import com.imzhitu.admin.common.dataSourceMasterSlave.DataSource; import com.imzhitu.admin.common.pojo.ZTWorldSubtitle; public interface SubtitleMapper { @DataSource("slave") public List<HTWorldSubtitleDto> queryCacheSubtitle(@Param("transTo")String transTo, @Param("limit")Integer limit); @DataSource("slave") public List<ZTWorldSubtitle> queryTitles(ZTWorldSubtitle title); @DataSource("slave") public long queryTotal(ZTWorldSubtitle title); @DataSource("master") public void saveSubtitle(ZTWorldSubtitle title); @DataSource("master") public void update(ZTWorldSubtitle title); @DataSource("master") public void deleteByIds(Integer[] ids); @DataSource("master") public void updateSerialById(@Param("id")Integer id, @Param("serial")Integer serial); }
數據流轉順序:
1.xml<aop>攔截到數據源名稱
2.執行切面DataSourceAspect中的before方法,將數據源名稱放入 DynamicDataSourceHolder中
3.Spring JDBC調用determineCurrentLookupKey()方法<DynamicDataSource中重寫AbstractRoutingDataSource類中的方法> ,從DynamicDataSourceHolder取出當前的數據庫名稱,並返回
4.AbstractRoutingDataSource類中determineTargetDataSource()方法調用determineCurrentLookupKey()匹配到指定的數據庫,並建立鏈接,即為切換到相應的數據庫;
5.在指定的數據庫中執行相應的sql
總結:
1.注釋自定義
2.Spring JDBC中 AbstractRoutingDataSource
3.xml中數據源配置,aop配置
以上三者為主從數據庫實現的核心技術