十九、Connector與Failover協議
Mysql Connector/J支持failover協議:即Client鏈接失效時,將會嘗試與其他host建立鏈接,這個過程對application是透明的。Failover協議是“Multi-Host”鏈接模式中最基礎的協議,“load balancing”、“replication”、“farbic”協議都基於Failover協議。其URL格式如下:
Java代碼 收藏代碼
jdbc:mysql://[primary-host]:[port],[secondary-host]:[port],.../[database]?[property=<value>]&[property=<value>]
Host列表中有兩種hosts:primary(master)和secondaries(slaves),priamry需要位於hosts列表的第一個;當創建一個新的connection時,driver會首先嚐試與primary鏈接,如果primary鏈接異常,將會依次與secondaries建立鏈接直到成功爲止;即使與primary的鏈接失效,但是driver並不會丟失primary的特殊狀態:比如它的訪問模式、優先級等。
Failover協議支持如下屬性:
1)failOverReadOnly
2)secondsBeforeRetryMaster
3)queriesBeforeRetryMaster
4)retriesAllDown:當所有的hosts都無法連接時,輪詢重試的最大次數,默認爲120。重試次數達到閾值仍然無法獲取有效鏈接,將會拋出SQLException。
5)autoReconnect
6)autoReconnectForPools
“failOverReadOnly”用於控制connection中“read_only”參數,即當primary鏈接失效時,failover到secondary時鏈接的默認訪問模式,此值默認爲“true”,即與secondary的鏈接訪問模式爲“只讀”;通常情況下,primary的鏈接訪問模式爲“read/write”,即read_only爲false。Connector J在failover時可以修改read_only的值,當然application在獲取鏈接後也可以調整此值。無論何時,failover到primary的鏈接總是“read/write”模式;當Client與primary的鏈接失效時(可能primary節點並沒有失效),如果failOverReadOnly爲false,表示與secondary的連接上允許發生“write”操作,這極有可能是危險的,因爲slaves上如果被Client寫入數據將導致replicaiton集羣中的數據不一致,除非slaves節點上已經設定了全局“read_only=true”。
在failover發生後,Driver會定期檢測primary的鏈接狀況,如果發現primary的鏈接恢復正常,那麼客戶端鏈接也將會“Fallback”(回落)到primary上,即此後的read/write將在primary鏈接上發生(與secondary的鏈接將會保持空閒,或者斷開)。“secondsBeforeRetryMaster”表示driver每隔多少秒將重試master的鏈接,“quriesBeforeRetryMaster”表示執行多少次queries後重試master的鏈接,如果重試成功,則將會“Fallback”到primary上;可以將上述兩個屬性設置爲“0”來關閉自動Fallback。
當primary的鏈接失效後,觸發failover,此時driver將依次與hosts列表中的secondary建立鏈接,直到與某個Secondary鏈接成功,否則將所有的secondaries都嘗試完畢,然後繼續從頭開始,直到與其中一個secondary鏈接成功。如果所有的hosts都無法鏈接,driver最多重試“retriesAllDown”次(默認爲120)。
“autoReconnect”、“autoReconnectForPools”這兩個參數用於控制“重連”特性,即當一個session(或者事務)操作過程中,如果鏈接異常,driver不會拋出Exception而是嘗試重新鏈接並繼續執行;如果你的操作是非事務性的(Query或者變更MyISAM表),reconnect通常不會帶來問題;但是如果是一個事務操作,在事務中的多個操作過程中發生重連,有可能會對session狀態造成破壞,從而導致數據不一致性問題,即使開啓“autocommit=false”時仍然不能避免問題,事實上重連之後並不會rollback原來中斷的事務,而是繼續進行,參見【autoReconnect】。此參數選項將會在未來的版本中被移除,也有可能不同版本的Connector J中實現會有不同,建議開發者禁用此特性。
Failover協議,它解決了:當master失效後,Driver能夠透明的與其他secondary建立鏈接,當master恢復後,能夠自動的Fallback到primary。對於application而言,可以利用“replication”架構的優勢,以提高可用性;MySQL實例的故障或者恢復,不需要調整application代碼。
參見源碼:FailoverConnectionProxy,它爲代理類,即Connection上的所有操作方法均有此proxy代理。
二十、Connector與Load Balancing(LB)
Failover協議並沒有提供“load balancing”特性,即讀(寫)操作總是隻發生在一個host上。對於“replication”、“多Master”架構,我們通常希望“負載均衡”、“讀寫分離”等高級特性,這就是Load Balancing協議所能解決的。Load Balancing可以將read/write負載,分佈在多個MySQL實例上,這些MySQL實例通常爲Cluster架構或者Replication架構,本文主要講解“Replication”架構相關的知識。LB協議基於“Failover協議”,即具備Failover特性,其URL格式:
Java代碼 收藏代碼
jdbc:mysql:loadbalance://[host]:[port],[host]:[port],...[/database]?[property=<value>]&[property=<value>]
LB協議支持2個配置選項:
1、loadBalanceConnectionGroup:將來自不同資源(hosts)的鏈接進行分組,通常每個LB協議都需要一個group名字,所有的LB鏈接都共享一個group名稱,特別是當JVM進程上有多個LB Datasource時。
2、loadBalanceEnableJMX:當定義group名稱後,可以將管理鏈接的方式暴露給JMX。此後可以通過JMXBean(LoadBalanceConnectionGroupManagerMBean)相關接口來管理和操作:比如運行時增減hosts列表等。
3、loadBalanceStrategy:負載均衡的策略,默認值爲“random”;其支持簡寫值爲“random”、“bestResponseTime”(最小響應時間優先);對於其他負載均衡策略,需要指定類的全名。請參見BalanceStrategry的實現類。
4、loadBalanceBlacklistTimeout:爲loadBalanceStrategy服務,如果某個節點請求時返回SQLException,那麼此host將會被添加到黑名單中,在此後的timeout時間內下一次選擇時將不會再被選中。默認值爲0,表示“立即過期”,即每次重新選擇連接時都將參與選擇(本次選擇時加入黑名單後,將不會在本次選擇時考慮)。其中內部機制比較簡單,在獲取新連接時,首先獲取並檢查黑名單列表,此時會檢測timeout時間是否過期,如果過期將從黑名單列表中移除,並允許此host參與本次選擇。(參見LoadBalanceConnectionProxy.getGlobalBlacklist())
LB協議允許在運行時增減hosts,這對二次開發的人來說非常有用,本文將不再贅言。
driver創建的LoadBalancedConnection是一個邏輯鏈接,其內部持有一個物理鏈接列表,即與每個host建立一個Connection。1)當autocommit爲false時,在事務的邊界方法執行後,比如commit、rollback,將會觸發BalanceStrategy從host列表中重新選擇新的鏈接。2)當鏈接上發生Exception時,比如socket異常,將會導致重新選擇鏈接。3)當autocommit爲true時,當鏈接上執行“loadBalanceAutoCommitStatementThreshold”個statements後(Queries或者updates等),將會導致重新選擇鏈接,默認爲0表示“粘性鏈接,不重新選擇鏈接”。(這是負載均衡的一種補償措施)
選擇鏈接的方式參見LoadBalancedConnectionProxy.pickConnection()。(以及其invokeMore()方法,注意本文中提到的*ConnectionProxy類均爲代理類,JAVA動態代理,代理對應的*Connection方法)
(參見源碼:LoadBalancedConnectionProxy,BalanceStrategy,LoadBalancedConnection)
Java代碼 收藏代碼
Driver driver = new NonRegisteringDriver();
Properties properties = new Properties();
properties.put("user",USER);
properties.put("password",PASSWORD);
properties.put("loadBalanceAutoCommitStatementThreshold","2");
Connection connection = driver.connect("jdbc:mysql:loadbalance://127.0.0.1:3306,127.0.0.1:4306,127.0.0.1:5306/mydb", properties);
ResultSet resultSet = connection.createStatement().executeQuery("SELECT * FROM test where id = 1");
while (resultSet.next()) {
Date created = resultSet.getDate(2);
System.out.println(DateFormatUtils.format(created, "yyyy-MM-dd HH:mm:ss"));
}
resultSet.close();
connection.close();//將導致LoadBalancedConnection中所有的連接關閉
LoadBalancedConnectionProxy爲代理類,Connection上的所有方法操作均其代理,因此它可以在執行方法時選擇“內部的物理鏈接”執行,達到LB的效果。
二十一、Connector與Replication
Replication是MySQL最常見、最有效的架構模式之一,Replication協議基於Failover與LB,只適用與replicaiton架構,主要用於解決replication架構下“讀寫分流”、“負載均衡”等問題。其URL格式爲:
Java代碼 收藏代碼
jdbc:mysql:replication://[master-host]:[port],[slave-host]:[port],.../database?[property=<value>]
1、“allowMasterDownConnections=true”屬性表示當master失效時可以繼續創建其Connections對象,但是Connection的訪問模式爲“read_only=true”,當master有效後會調用Connection.setReadOnly(false)方法修改其訪問模式,在此之前如果通過Connection提交writes操作將會拋出異常,此屬性默認值爲false。“allowSlavesDownConnections=true”表示當所有的slaves都失效時是否也能創建相應的Connections對象,訪問模式也是“read_only=true”,此鏈接有效之前如果通過Connection發送read操作將會拋出異常,此屬性默認值爲false。“readFromMasterNoSlaves”表示如果所有的slaves都不可用,read操作將在master連接上發生,此屬性默認值爲false。
2、“readFromMasterWhenNoSlaves”表示當所有的slaves都失效(異常)時,是否允許將read請求轉發給master。默認爲false,建議設置爲true。
在replication模式下,默認slaves的負載均衡的策略與LoadBalance協議保持一致,而且由Connection.getReadOnly()值決定;如果read_only爲true,Replication Connection將使用“隨機”模式選擇一個slave鏈接,如果所有的slaves都失效,此時將使用master鏈接(由readFromMasterNoSlaves參數控制)。如果你需要提交write請求或者read實時數據,那麼需要將read_only=false,那麼read/write操作將在master鏈接上發生。master鏈接上除了可以接受read_only外,還可以指定autocommit和事務隔離級別。
還有一個比較重要的參數“replicationConnectionGroup”,我們爲一組master-slaves指定唯一的group名稱,此後即可通過“ReplicationConnectionGroupManager”來跟蹤鏈接以及動態管理hosts列表,即允許開發者在運行時動態調整hosts拓撲結構。(比如addSlaveHost,promoteSlaveToMaster等等)。
ReplicationDrvier創建的Connection類型爲ReplicationConnection,ReplicationConnection也由ReplicationConnectionProxy代理執行;每個ReplicationConnection對應一個proxy實例,每個proxy內部都持有一個masterConnection和類型爲LoadBalancedConnection的slavesConnection,因此slaves鏈接具備LB特性。如果read_only爲false,那麼在執行操作方法時,proxy將選擇masterConnection;否則將從slavesConnections中選擇一個。每個ReplicationConnection均會與所有的host建立一個物理鏈接,這一點需要清楚,這會導致每個MySQL實例(master、slave節點)都持有相同的連接數。
參見源碼:ReplicationConnection,ReplicationDriver,ReplicationConnectionProxy。
Java代碼 收藏代碼
ReplicationDriver driver = new ReplicationDriver();
Properties properties = new Properties();
properties.put("user",USER);
properties.put("password",PASSWORD);
Connection connection = driver.connect("jdbc:mysql:replication://127.0.0.1:3306,127.0.0.1:4306,127.0.0.1:5306/mydb", properties);
connection.setAutoCommit(true);
connection.setReadOnly(true);
ResultSet resultSet = connection.createStatement().executeQuery("SELECT * FROM test where id = 1");
while (resultSet.next()) {
Date created = resultSet.getDate(2);
System.out.println(DateFormatUtils.format(created,"yyyy-MM-dd HH:mm:ss"));
}
resultSet.close();
connection.close();
二十二、Spring環境下的Replication設計(讀寫分離)
接下來,我們用Spring 4.x、mybatis框架,基於DBCP線程池,構建一個replication客戶端代碼模板,本例基於connector/j使用5.1.38版本,mysql爲5.7。
1、spring配置(摘要)
Java代碼 收藏代碼
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd"
default-autowire="byName">
<bean id="commonDataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.ReplicationDriver"></property>
<property name="url" value="jdbc:mysql:replication://127.0.0.1:3306,127.0.0.1:4306,127.0.0.1:5306/mydb?useUnicode=true&characterEncoding=UTF-8&autoReconnect=false&useSSL=false&failOverReadOnly=true&loadBalanceStrategy=random&readFormMasterNoSlaves=true"></property>
<property name="username" value="test"></property>
<property name="password" value="test"></property>
<property name="maxTotal" value="12"></property>
<property name="maxIdle" value="2"></property>
<property name="minIdle" value="2"></property>
<property name="maxWaitMillis" value="30000"></property>
<property name="defaultAutoCommit" value="true"></property>
<property name="defaultReadOnly" value="false"></property><!-- 必須爲false,否則@transactional中的readOnly將無法正常工作 -->
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="commonDataSource" />
<property name="configLocation" value="classpath:sqlmap-config.xml"></property>
<!-- <property name="dataSource" ref="dataSource" /> -->
</bean>
<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="commonDataSource"/>
</bean>
<bean name="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager" />
<property name="isolationLevelName" value="ISOLATION_READ_COMMITTED"/>
<property name="timeout" value="30"/>
</bean>
<!-- core api,必須爲prototype -->
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate" scope="prototype">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
</beans>
Java代碼 收藏代碼
<context:component-scan base-package="com.sample.manager"/>
<context:component-scan base-package="com.sample.dao"/>
<!-- 非常重要,否則@transactional將無法生效 -->
<tx:annotation-driven />
<import resource="spring-dao-test.xml" />
<import resource="spring-manager-test.xml" />
2、BaseDao
Java代碼 收藏代碼
import org.mybatis.spring.SqlSessionTemplate;
public abstract class BaseDao {
protected SqlSessionTemplate sqlSessionTemplate;
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSessionTemplate = sqlSessionTemplate;
}
}
3、TestDao
Java代碼 收藏代碼
@Component
public class TestDaoImpl extends BaseDao implements TestDao {
@Override
public TestDO get(int id) {
return this.sqlSessionTemplate.selectOne("TestMapper.get",id);
}
@Override
public void update(TestDO test) {
this.sqlSessionTemplate.update("TestMapper.update",test);
}
}
4、TestManager
Java代碼 收藏代碼
@Component
public class TestManagerImpl implements TestManager {
@Autowired
private TestDao testDao;
@Override
@Transactional(readOnly = true)
public TestDO get(int id) {
return this.testDao.get(id);
}
@Override
@Transactional(readOnly = false)
public void update(TestDO test) {
this.testDao.update(test);
}
}
5、查詢測試
Java代碼 收藏代碼
TestDO test = testManager.get(1);
testManager.update(test);
6、事務測試
Java代碼 收藏代碼
transactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
TestDO test = testManager.get(1);
testManager.update(test);
return true;
}
});
7、原理
代碼設計方面,我們通常在manager層的接口方法上使用@transactional註釋,dao層使用myBatis操作數據。根據replication原理,對於5、中普通數據操作,@transactional註釋中聲明的readOnly屬性將決定操作使用的鏈接類型,我們沒有其他的方式來設定connection的readOnly屬性。如果readOnly爲false,則操作在master上執行,否則在slaves發生。對於6、這種手動開啓事務的方式,在執行doInTransaction之前,spring將Connection設置爲“autocommit=false”、“readOnly=false”,忽略事務操作內部所有方法的@transactional,最終事務中的所有方法調用均在master上執行,比如testManager.get()方法上聲明的readOnly=true將被忽略,get操作仍然在master連接上執行。
所以,如果使用了spring託管事務管理,那麼在query操作上使用@transactional指定readOnly爲true即可實現LB(在Master與Slaves之間LB,請求仍然可能發給Master);如果希望操作(read或者write)在master執行,則只需要將readOnly設置爲false即可。使用transactionTemplate手動執行事務的(即autocommit=false),事務將忽略@transactional註釋,進而將此事務中的所有操作在master執行。
Replication鏈接對DBCP連接池是透明的,即連接池中的鏈接管理方式與單host並沒有什麼區別。
如下原理分析,基於本例和spring事務管理模式(對於基於AOP模式可能不適合),當Spring Bean調用@transactional註釋的方法時,將被攔截器攔截且通過反射機制方式執行(註釋最終都是由攔截器驅動,參見TransactionInterceptor),且開啓事務,如果readOnly爲false時將會強制設置autocommit=false,在方法調用結束後,事務被自動提交。
1)Spring基於反射機制攔截@Transactionnal,開啓事務(SpringManagedTransaction),並根據@Transactional設定事務的屬性,並將此事務TransactionInfo綁定在當前線程。(TransactionInteceptor)
2)根據DataSource創建鏈接Connection,並將此鏈接綁定在當前線程(ConnectionHolder)。(DataSourceTransactionManager.doBegin())
3)每個sqlSessionTemplate實例內部都有個代理實例sqlSessionProxy,即通過sqlSessionTemplate執行的方法均由此代理實例執行;在執行操作之前,首先獲取sqlSession實例,並將sqlSession綁定在當前線程(SqlSessionHolder,ThreadLocal)。
4)ibatis中使用此sqlSession執行數據庫操作,sqlSession執行操作所使用的鏈接是從ConnectionHolder獲取的。(DefaultSqlSession,SpringManagedTransaction)
5)如果@Transactional方法中有多個dao層方法調用,則繼續循環3)~5),此過程中,所有的方法均公用一個sqlSession實例。
6)和1)對應,將TransactionInfo從當前線程解綁,並提交事務。
7)和3)對應,將SqlSessionHolder從當前線程解綁,並關閉sqlSession。
8)與2)對應,將ConnectionHolder從當前線程中解綁,並將Connection釋放到連接池中。
每調用一個@Transactional方法,都會按照上述過程執行;即如果你一個方法中調用了多個@Transactional方法,這意味着它們在不同的事務中執行、使用不同的sqlSession實例、可能使用不同的Connection。
對於使用transactionTemplate方式手動開啓事務的,過程稍微有些不同,在內部類doTransaction方法調用之前,將由spring創建事務、準備connection等與上述保持一致,並在方法執行後提交事務(如果拋出異常在rollback);doInTransaction中所調用的方法上的@Transactional將被忽略,所有的dao層方法均使用同一個sqlSession和Connection實例。
二十三、Connector J問題小結
1、選擇合適的Driver:對於Failover和LB協議,可以選用NonRegisterDriver;對於replication協議,我們需要使用ReplicationDriver。對於單點host,普通的Driver類即可。
2、爲了便於跟蹤replication模式下,LB協議是否生效或者query在哪個節點執行,我們可以開啓“general_log”功能:
Java代碼 收藏代碼
>SET GLOBAL general_log=1;
>select @@global.general_log;
3、autoReconnect屬性建議禁用,因爲在事務操作中,如果鏈接重連並不會導致事務回滾而是繼續執行,這會帶來事務完整性的問題,導致數據不一致。具體原理參見【autoReconnect】