简介
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自动实现,该代码只实现读写分离。
欢迎大家交流讨论。