简介

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方法的重写):

 public synchronized void setReadOnly(boolean readOnly) throws SQLException {
if (readOnly) {
if (!isSlavesConnection() || this.currentConnection.isClosed()) {
boolean switched = true;
SQLException exceptionCaught = null;
try {
switched = switchToSlavesConnection();
} catch (SQLException e) {
switched = false;
exceptionCaught = e;
}
if (!switched && this.readFromMasterWhenNoSlaves && switchToMasterConnection()) {
exceptionCaught = null; // The connection is OK. Cancel the exception, if any.
}
if (exceptionCaught != null) {
throw exceptionCaught;
}
}
} else {
if (!isMasterConnection() || this.currentConnection.isClosed()) {
boolean switched = true;
SQLException exceptionCaught = null;
try {
switched = switchToMasterConnection();
} catch (SQLException e) {
switched = false;
exceptionCaught = e;
}
if (!switched && switchToSlavesConnectionIfNecessary()) {
exceptionCaught = null; // The connection is OK. Cancel the exception, if any.
}
if (exceptionCaught != null) {
throw exceptionCaught;
}
}
}
this.readOnly = readOnly; /*
* Reset masters connection read-only state if 'readFromMasterWhenNoSlaves=true'. If there are no slaves then the masters connection will be used with
* read-only state in its place. Even if not, it must be reset from a possible previous read-only state.
*/
if (this.readFromMasterWhenNoSlaves && isMasterConnection()) {
this.currentConnection.setReadOnly(this.readOnly);
}
}

加粗的两行代码就是在只读情况下和非只读情况下分别使用从节点和主节点的连接。为此我们可以编写以下代码验证读写分离:

     public static void main(String[] args) {
try {
ReplicationDriver driver = new ReplicationDriver(); Properties p =new Properties();
p.load(TestMasterSlave.class.getResourceAsStream("/db_masterslave.properties"));
Connection conn = driver.connect(p.getProperty("url"), p);
conn.setAutoCommit(false);
conn.setReadOnly(true);
ResultSet rs = conn.createStatement().executeQuery("select count(*) from user");
while(rs.next()) {
System.out.println(rs.getString(1));
}
System.out.println(BeanUtils.describe(conn).get("hostPortPair")); rs.close();
conn.close();
} 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自动实现,该代码只实现读写分离。

欢迎大家交流讨论。

												

SSM/SSH框架的MySQL 读写分离实现的一种简单方法的更多相关文章

  1. 转:Mysql读写分离实现的三种方式

    1 程序修改mysql操作类可以参考PHP实现的Mysql读写分离,阿权开始的本项目,以php程序解决此需求.优点:直接和数据库通信,简单快捷的读写分离和随机的方式实现的负载均衡,权限独立分配缺点:自 ...

  2. mysql读写分离--一主多从,冗余存储

    转载了https://blog.csdn.net/u013421629/article/details/78793966 https://blog.csdn.net/justdb/article/de ...

  3. Mysql读写分离-Amoeba Proxy

    参考:http://www.linuxidc.com/Linux/2015-10/124115.htm 一个完整的MySQL读写分离环境包括以下几个部分: 应用程序client database pr ...

  4. 构建高性能web之路------mysql读写分离实战(转)

    一个完整的mysql读写分离环境包括以下几个部分: 应用程序client database proxy database集群 在本次实战中,应用程序client基于c3p0连接后端的database ...

  5. mysql读写分离实战

    一个完整的MySQL读写分离环境包括以下几个部分: 应用程序client database proxy database集群 在本次实战中,应用程序client基于c3p0连接后端的database ...

  6. 使用Amoeba实现mysql读写分离机制

    Amoeba的实用指南 http://docs.hexnova.com/amoeba/ 如何实现mysql读写分离 : 通常来说有两种方式: 1,应用程序层实现 2,中间件层实现 应用层实现 应用层实 ...

  7. Phalcon框架如何实现读写分离

    Phalcon框架如何实现读写分离 假设你已经在DI容器里注册了俩 db services,如下: <?php // 主库 $di->setShared('dbWrite', functi ...

  8. mysql读写分离(PHP类)

    mysql读写分离(PHP类) 博客分类: php mysql   自己实现了php的读写分离,并且不用修改程序 优点:实现了读写分离,不依赖服务器硬件配置,并且都是可以配置read服务器,无限扩展 ...

  9. amoeba实现MySQL读写分离

    amoeba实现MySQL读写分离 准备环境:主机A和主机B作主从配置,IP地址为192.168.131.129和192.168.131.130,主机C作为中间件,也就是作为代理服务器,IP地址为19 ...

随机推荐

  1. ElasticSearch安装中文分词器IKAnalyzer

    # ElasticSearch安装中文分词器IKAnalyzer  本篇主要讲解如何在ElasticSearch中安装中文分词器IKAnalyzer,拆分的每个词都是我们熟知的词语,从而建立词汇与文档 ...

  2. Git创建远程分支并提交代码到远程分支

    1.可以在VS中新建分支 2.可以通过git branch -r 命令查看远端库的分支情况 这些红色都是远程的分支 3.从已有的分支创建新的分支(如从master分支),创建一个dev分支 (不用vs ...

  3. OpenGLES思维导图

    两本书到头来就只剩下了这三张图了吧.想要原图:https://github.com/wangwangla/biji/blob/master/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8 ...

  4. scala基本语法

    scala基本语法scala函数1 def定义方法2 方法的返回值类型可以省略3 方法体重最后一行计算结果可以返回 return 如果省略方法类型4 方法参数 要指定类型5 如果方法体可以一步搞定 方 ...

  5. postgresql gin索引使用

    由于属于老项目,postgresql使用版本9.6,主要解决‘%name%"查询无法使用索引问题.pg_trgm模块提供函数和操作符测定字母,数字,文本基于三元模型匹配的相似性, 还有支持快 ...

  6. 2018南京现场赛D 模拟退火

    题目链接:https://codeforces.com/gym/101981/attachments 给你n个城市的三维坐标,叫你求得一个坐标使这个坐标到其他城市的最大距离最小,并输出这个距离(距离不 ...

  7. Linux下安装JDK 1.8

    前言 JDK是 JAVA 的软件开发工具包,如果要使用JAVA来进行开发,或者部署基于其开发的应用,那么就需要安装JDK.本次将在Linux下安装JDK及配置环境. 本人环境:CentOS 7.3 6 ...

  8. mongoskin 是让 Node.js 支持 MongoDB 的内嵌访问层。

    mongoskin 是让 Node.js 支持 MongoDB 的内嵌访问层.

  9. python接口自动化测试 - configparser配置文件解析器详细使用

    configparser简介 ConfigParser模块已在Python 3中重命名为configparser 该模块定义了ConfigParser类. ConfigParser类实现一种基本的配置 ...

  10. Python脚本通过ftp协议移植文件

    需求 项目需要定时移植多个客户服务器的文件到公司服务器上,确保文件定时同步和生成监控日志 机制原理 1.客户和公司服务器同时安装vpn,绕过复杂的网关,linux下使用的OpenVPN 2.服务器定时 ...