簡介
MySQL已經是使用最為廣泛的一種數據庫,往往實際使用過程中,為實現高可用及高性能,項目會采用主叢復制的方式實現讀寫分離。MySQL本身支持復制,通過簡單的配置即可實現一主多從的配置,具體實現可參考https://www.cnblogs.com/luckcs/articles/6295992.html(GTID模式)。一主多從從數據庫的層次解決了讀寫分離的問題,主庫負責讀寫操作,而從庫只負責讀取操作。但要在應用中實現讀寫分離,還需要中間層的支持。本文主要討論基於Java的讀寫分離的實現。
MySQL實現讀寫分離的方法有很多,比如MySQL Proxy,Ameoba等,本文從流行的Spring+Hibernate或者Spring+Mybatis(本文實際演示例子)出發,使用一種簡單的方式實現基本的讀寫分離功能。
環境
主節點:192.168.56.102:3306
從節點1:192.168.56.101:3306
從節點2:192.168.56.107:3306
Java開發環境:OpenJDK1.8 ,Centos,Eclipse
使用框架見代碼的pom
基本原理
利用mysql JDBC驅動包中的ReplicationDriver實現讀寫分離和輪詢策略的負載均衡(讀操作)
MySQL JDBC驅動包中的ReplicationDriver為主從復制的MySQL環境提供JDBC支持,其基本原理為根據是否對連接對象setReadOnly(true)決定實際采用主節點(讀寫)或者是從節點(讀)的連接,其核心源碼為(ReplicationConnectionProxy中對Connection的setReadOnly方法的重寫):
1 public synchronized void setReadOnly(boolean readOnly) throws SQLException { 2 if (readOnly) { 3 if (!isSlavesConnection() || this.currentConnection.isClosed()) { 4 boolean switched = true; 5 SQLException exceptionCaught = null; 6 try { 7 switched = switchToSlavesConnection(); 8 } catch (SQLException e) { 9 switched = false; 10 exceptionCaught = e; 11 } 12 if (!switched && this.readFromMasterWhenNoSlaves && switchToMasterConnection()) { 13 exceptionCaught = null; // The connection is OK. Cancel the exception, if any. 14 } 15 if (exceptionCaught != null) { 16 throw exceptionCaught; 17 } 18 } 19 } else { 20 if (!isMasterConnection() || this.currentConnection.isClosed()) { 21 boolean switched = true; 22 SQLException exceptionCaught = null; 23 try { 24 switched = switchToMasterConnection(); 25 } catch (SQLException e) { 26 switched = false; 27 exceptionCaught = e; 28 } 29 if (!switched && switchToSlavesConnectionIfNecessary()) { 30 exceptionCaught = null; // The connection is OK. Cancel the exception, if any. 31 } 32 if (exceptionCaught != null) { 33 throw exceptionCaught; 34 } 35 } 36 } 37 this.readOnly = readOnly; 38 39 /* 40 * Reset masters connection read-only state if 'readFromMasterWhenNoSlaves=true'. If there are no slaves then the masters connection will be used with 41 * read-only state in its place. Even if not, it must be reset from a possible previous read-only state. 42 */ 43 if (this.readFromMasterWhenNoSlaves && isMasterConnection()) { 44 this.currentConnection.setReadOnly(this.readOnly); 45 } 46 }
加粗的兩行代碼就是在只讀情況下和非只讀情況下分別使用從節點和主節點的連接。為此我們可以編寫以下代碼驗證讀寫分離:
1 public static void main(String[] args) { 2 try { 3 ReplicationDriver driver = new ReplicationDriver(); 4 5 Properties p =new Properties(); 6 p.load(TestMasterSlave.class.getResourceAsStream("/db_masterslave.properties")); 7 Connection conn = driver.connect(p.getProperty("url"), p); 8 conn.setAutoCommit(false); 9 conn.setReadOnly(true); 10 ResultSet rs = conn.createStatement().executeQuery("select count(*) from user"); 11 while(rs.next()) { 12 System.out.println(rs.getString(1)); 13 } 14 System.out.println(BeanUtils.describe(conn).get("hostPortPair")); 15 16 rs.close(); 17 conn.close(); 18 } catch (Exception e) { 19 e.printStackTrace(); 20 }
對應數據庫配置為:
url=jdbc:mysql:replication://192.168.56.102:3306,192.168.56.101:3306,192.168.56.107:3306/test1 user=root password=root autoReconnect=true roundRobinLoadBalance=true
System.out.println(BeanUtils.describe(conn).get("hostPortPair"));這行代碼主要用來顯示實際連接的主機和端口,通過多次運行我們可以發現其真實連接會在101和107之間切換。
基於以上實驗,我們引入SSM框架,項目完整的pom依賴為
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.44</version> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils-core</artifactId> <version>1.8.3</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.3.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>4.3.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.3.9.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.44</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.10</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.4</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.3</version> </dependency>
我們的目標是通過Spring的聲明式事務,在事務的方法為readonly時,自動切換到從節點以實現讀寫分離。為直接能操作連接,我們使用了spring的JDBC TransactionManager,spring配置中事務相關代碼為:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.ReplicationDriver"></property>
<property name="url" value="jdbc:mysql:replication://192.168.56.102:3306,192.168.56.101:3306,192.168.56.107:3306/test1?autoReconnect=true&roundRobinLoadBalance=true"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="initialSize" value="3"></property>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
<bean id="transactionManager" class="util.MyTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="transactionAdvice">
<tx:attributes>
<tx:method name="find*" read-only="true"/>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="*" propagation="SUPPORTS"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:advisor advice-ref="transactionAdvice" pointcut="execution (* biz.*.*(..))"/>
</aop:config>
這個MyTransactionManager就是我們真正改造使SSM能夠實現基於Spring本身實現讀寫分離的地方,具體實現代碼為:
/**
* 為實現及直觀顯示讀寫分離的具體情況,重寫了DataSourceTransactionManager,並在事務定義為只讀時調用了連接的setReadOnly方法
*
* @author root
*
*/
public class MyTransactionManager extends DataSourceTransactionManager {
@Override
protected void prepareTransactionalConnection(Connection con, TransactionDefinition definition)
throws SQLException {
// TODO Auto-generated method stub
super.prepareTransactionalConnection(con, definition);
if (definition.isReadOnly()) {
con.setReadOnly(true);//只讀事物,設置連接的readOnly促使底層的ReplicationDriver選擇正確的節點
try {
System.out.println("a readonly transaction:" +
BeanUtils.describe(((DruidPooledConnection)con).getConnection()).get("hostPortPair"));
} catch (Exception e) {
e.printStackTrace();
}
}else {
try {
System.out.println("a write transaction:" +
BeanUtils.describe(((DruidPooledConnection)con).getConnection()).get("hostPortPair"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
經過這么小小的改造,如果是在Spring中聲明了read-only為true的事務,我們就可以得到definition.isReadOnly()的結果為true,通過設置con的ready only為true,讓底層的MySQL的ReplicationDriver做出選擇,從而實現純讀取的方法直接從其他節點實現。運行程序,多在業務層調用幾次find開始的方法,我們會發現實際的讀取節點一直在改變,
本方案未在生產環境中測試,僅作為一種讀寫分離的嘗試。通過簡單的邏輯分析可知,所有包含讀寫操作的事務,所有數據操作都會發生在主節點上,只有標識為純讀取的業務方法的數據操作才會被均衡到只讀節點,理論上是沒有問題。需要注意的是,如果我們使用的是HibernateTransactionManager,則實現方式不同。
如果MySQL服務器采用的方案不是主從復制,而是MySQL Cluster,則只需要提供一個讀寫端口和一個只讀端口,負載均衡由MySQL Cluster自動實現,該代碼只實現讀寫分離。
歡迎大家交流討論。
