Mysql Replication與Connector/J原理(四)

十九、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】
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章