大量的JavaWeb應用作的是IO密集型任務, 數據庫的壓力較大, 須要分流java
大量的應用場景, 是讀多寫少, 數據庫讀取的壓力更大mysql
一個很天然的思路是使用一主多從的數據庫集羣: 一個是主庫,負責寫入數據;其它都是從庫,負責讀取數據. 主從庫數據同步.spring
mysql原生支持主從複製sql
mysql主(稱master)從(稱slave)複製的原理:
一、master將數據改變記錄到二進制日誌(bin log)中, 這些記錄叫binary log events
二、slave將master的binary log events拷貝到它的中繼日誌(relay log)
三、slave重作中繼日誌中的事件, 將改變反映它本身的數據(數據重演)數據庫
解決讀寫分離的方案大體有兩種:express
1)在應用層給讀/寫分別指定數據庫安全
好處是數據源切換方便, 不用引入其餘組件. 可是不能動態添加數據源.app
2)使用中間件解決ide
好處是源程序不須要作任何改動, 還能夠動態添加數據源. 但中間件會帶來必定的性能損失.性能
目前有mysql-proxy, mycat, altas等
主庫配置
修改my.ini:
#開啓主從複製,主庫的配置
log-bin = mysql3306-bin
#指定主庫serverid
server-id=101
#指定同步的數據庫,若是不指定則同步所有數據庫
binlog-do-db=mydb
執行如下SQL:
SHOW MASTER STATUS;
記錄下Position值,須要在從庫中設置同步起始值。
#受權從庫用戶slave01使用123456密碼登陸本庫
grant replication slave on *.* to 'slave01'@'127.0.0.1' identified by '123456';
flush privileges;
從庫配置
修改my.ini:
#指定serverid
server-id=102
執行如下SQL:
CHANGE MASTER TO
master_host='127.0.0.1',
master_user='slave01',
master_password='123456',
master_port=3306,
master_log_file='mysql3306-bin.000006', #設置主庫時記下的Position
master_log_pos=1120;
#啓動slave同步
START SLAVE;
#查看同步狀態 Slave_IO_Running和Slave_SQL_Running都爲Yes說明同步成功
SHOW SLAVE STATUS;
這裏採用的是應用層的讀寫分離方案
使用AOP, 在執行Service方法前判斷,是使用寫庫仍是讀庫
能夠根據方法名做爲依據判斷,好比說以query、find、get等開頭的就走讀庫,其餘的走寫庫
切面類:
/** * 若是在spring配置了事務的策略,則標記了ReadOnly的方法用從庫Slave, 其它使用主庫Master。 * 若是沒有配置事務策略, 則採用方法名匹配, 以query、find、get開頭的方法用Slave,其它用Master。 */ public class DataSourceAspect { private List<String> slaveMethodPattern = new ArrayList<String>(); //保存有readonly屬性的帶通配符方法名 private static final String[] defaultSlaveMethodStartWith = new String[]{"query", "find", "get" }; private String[] slaveMethodStartWith; //保存有slaveMethodStartWith屬性的方法名頭部 //注入 public void setTxAdvice(TransactionInterceptor txAdvice) throws Exception { if (txAdvice == null) { // 沒有配置事務策略 return; } //從txAdvice獲取策略配置信息 TransactionAttributeSource transactionAttributeSource = txAdvice.getTransactionAttributeSource(); if (!(transactionAttributeSource instanceof NameMatchTransactionAttributeSource)) { return; } //使用反射技術獲取到NameMatchTransactionAttributeSource對象中的nameMap屬性值 NameMatchTransactionAttributeSource matchTransactionAttributeSource = (NameMatchTransactionAttributeSource) transactionAttributeSource; Field nameMapField = ReflectionUtils.findField(NameMatchTransactionAttributeSource.class, "nameMap"); nameMapField.setAccessible(true); //設置該字段可訪問 //獲取nameMap的值 Map<String, TransactionAttribute> map = (Map<String, TransactionAttribute>) nameMapField.get(matchTransactionAttributeSource); //遍歷nameMap for (Map.Entry<String, TransactionAttribute> entry : map.entrySet()) { if (!entry.getValue().isReadOnly()) { // 定義了ReadOnly的策略才加入到slaveMethodPattern continue; } slaveMethodPattern.add(entry.getKey()); } } // 切面 before方法 public void before(JoinPoint point) { // 獲取到當前執行的方法名 String methodName = point.getSignature().getName(); boolean isSlave = false; if (slaveMethodPattern.isEmpty()) { // 沒有配置read-only屬性,採用方法名匹配方式 isSlave = isSlaveByMethodName(methodName); } else { // 配置read-only屬性, 採用通配符匹配 for (String mappedName : slaveMethodPattern) { if (isSlaveByConfigWildcard(methodName, mappedName)) { isSlave = true; break; } } } if (isSlave) { // 標記爲讀庫 DynamicDataSource.markMaster(true); } else { // 標記爲寫庫 DynamicDataSource.markMaster(false); } } // 匹配以指定名稱開頭的方法名, 配置了slaveMethodStartWith屬性, 或使用默認 private Boolean isSlaveByMethodName(String methodName) { return StringUtils.startsWithAny(methodName, getSlaveMethodStartWith()); } // 匹配帶通配符"xxx*", "*xxx" 和 "*xxx*"的方法名, 源自配置了readonly屬性的方法名 protected boolean isSlaveByConfigWildcard(String methodName, String mappedName) { return PatternMatchUtils.simpleMatch(mappedName, methodName); } // 注入 public void setSlaveMethodStartWith(String[] slaveMethodStartWith) { this.slaveMethodStartWith = slaveMethodStartWith; } public String[] getSlaveMethodStartWith() { if(this.slaveMethodStartWith == null){ // 沒有配置slaveMethodStartWith屬性,使用默認 return defaultSlaveMethodStartWith; } return slaveMethodStartWith; } }
Spring的RoutingDataSource
/** * 使用Spring的動態數據源,須要實現AbstractRoutingDataSource * 經過determineCurrentLookupKey方法拿到識別key來判斷選擇讀/寫數據源 * token顯然是多例的, 因此引入ThreadLocal保存 */ public class DynamicDataSource extends AbstractRoutingDataSource { // 讀庫總數 private Integer slaveCount; // 讀庫輪詢計數, 初始爲-1, 本類爲單例, AtomicInteger線程安全 private AtomicInteger counter = new AtomicInteger(-1); // 存儲讀庫的識別key sl1ve01, slave02... 寫庫識別key爲master private List<Object> slaveDataSources = new ArrayList<Object>(); //當前線程的寫庫/讀庫token private static final ThreadLocal<Boolean> tokenHolder = new ThreadLocal<>(); public static void markMaster(boolean isMaster){ tokenHolder.set(isMaster); } @Override protected Object determineCurrentLookupKey() { if (tokenHolder.get()) { return "master"; // 寫庫 } // 輪詢讀庫, 獲得的下標爲:0、一、2... Integer index = counter.incrementAndGet() % slaveCount; if (counter.get() > 99999) { // 以避免超出Integer範圍 counter.set(-1); } return slaveDataSources.get(index); } @Override public void afterPropertiesSet() { super.afterPropertiesSet(); // 父類的resolvedDataSources屬性是private, 須要使用反射獲取 Field field = ReflectionUtils.findField(AbstractRoutingDataSource.class, "resolvedDataSources"); field.setAccessible(true); // 設置可訪問 try { Map<Object, DataSource> resolvedDataSources = (Map<Object, DataSource>) field.get(this); // 讀庫數等於dataSource總數減寫庫數 this.slaveCount = resolvedDataSources.size() - 1; for (Map.Entry<Object, DataSource> entry : resolvedDataSources.entrySet()) { if ("master".equals(entry.getKey())) { continue; } slaveDataSources.add(entry.getKey()); } } catch (Exception e) { e.printStackTrace(); } } }
spring配置文件
<!-- 定義事務策略 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!--全部以query開頭的方法都是隻讀的 --> <tx:method name="query*" read-only="true" /> <!-- readonly屬性 --> <!--其餘方法使用默認事務策略 --> <tx:method name="*" /> </tx:attributes> </tx:advice> <!-- 定義AOP切面處理器 --> <bean class="com.zx.DataSourceAspect" id="dataSourceAspect"> <!-- 注入事務策略 --> <property name="txAdvice" ref="txAdvice"/> <!-- 指定slave方法的前綴(非必須) --> <property name="slaveMethodStartWith" value="query,find,get"/> </bean> <aop:config> <aop:pointcut id="myPointcut" expression="execution(* com.zx.service.*.*(..))" /> <!-- 將切面應用到自定義的切面處理器上,-9999保證該切面優先級最高執行 --> <aop:aspect ref="dataSourceAspect" order="-9999"> <aop:before method="before" pointcut-ref="myPointcut" /> </aop:aspect> </aop:config> <!-- 定義數據源,繼承了spring的動態數據源 --> <bean id="dataSource" class="com.zx.DynamicDataSource"> <!-- 設置多個數據源 --> <property name="targetDataSources"> <map key-type="java.lang.String"> <!-- 這些設置的key和determineCurrentLookupKey方法拿到的key相比對, 根據匹配選擇數據源 --> <entry key="master" value-ref="masterDataSource"/> <!-- value-ref指向數據源 --> <entry key="slave01" value-ref="slave01DataSource"/> <entry key="slave02" value-ref="slave02DataSource"/> <entry key="slave03" value-ref="slave03DataSource"/> </map> </property> <!-- 設置默認的數據源,這裏默認走寫庫 --> <property name="defaultTargetDataSource" ref="masterDataSource"/> </bean>