十九、Connector与Failover协议
? ? Mysql Connector/J支持failover协议:即Client链接失效时,将会尝试与其他host建立链接,这个过程对application是透明的。Failover协议是“Multi-Host”链接模式中最基础的协议,“load balancing”、“replication”、“farbic”协议都基于Failover协议。其URL格式如下:
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
? ? 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格式:
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。
?
? ? LB协议允许在运行时增减hosts,这对二次开发的人来说非常有用,本文将不再赘言。此外还有一个比较重要的参数“loadBalanceStrategy”,比负载均衡的算法,目前只支持两种“random”和“bestResponseTime”,“random”表示随机选择Host,“bestResponseTime”表示尽可能选择响应延迟最小的Host,默认为“random”。
?
? ? driver创建的LoadBalancedConnection是一个逻辑链接,其内部持有一个物理链接列表,即与每个host建立一个Connection。1)当autocommit为false时,在事务的边界方法执行后,比如commit、rollback,将会触发BalanceStrategy从host列表中重新选择新的链接。2)当链接上发生Exception时,比如socket异常,将会导致重新选择链接。3)当autocommit为true时,当链接上执行“loadBalanceAutoCommitStatementThreshold”个statements后(Queries或者updates等),将会导致重新选择链接,默认为0表示“粘性链接,不重新选择链接”。
? ? 选择链接的方式参见LoadBalancedConnectionProxy.pickConnection()。
? ?(参见源码:LoadBalancedConnectionProxy,BalanceStrategy,LoadBalancedConnection)
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格式为:
jdbc:mysql:replication://[master-host]:[port],[slave-host]:[port],.../database?[property=<value>]
?
? ? “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。
?
? ? 在replication模式下,负载均衡的策略默认为“round-robin”,而且由Connection.getReadOnly()值决定;如果read_only为true,Replication Connection将使用“round-robin”模式选择一个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。
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配置(摘要)
<?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> </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>
?
<context:component-scan base-package="com.sample.manager"/><context:component-scan base-package="com.sample.dao"/><tx:annotation-driven /><import resource="spring-dao-test.xml" /><import resource="spring-manager-test.xml" />
?
? ? 2、BaseDao
import org.mybatis.spring.SqlSessionTemplate;public abstract class BaseDao { protected SqlSessionTemplate sqlSessionTemplate; public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) { this.sqlSessionTemplate = sqlSessionTemplate; }}
?
? ? 3、TestDao
@Componentpublic 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
@Componentpublic 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、查询测试
TestDO test = testManager.get(1);testManager.update(test);
?
? ?6、事务测试
transactionTemplate.execute(new TransactionCallback<Boolean>() { @Override public Boolean doInTransaction(TransactionStatus status) { TestDO test = testManager.get(1); testManager.update(test); return true; }});
?
? ?7、原理
? ?代码设计方面,[email protected]?释,dao层使用myBatis操作数据。根据replication原理,对于5、中普通数据操作,@transactional注释中声明的readOnly属性将决定操作使用的链接类型,我们没有其他的方式来设定connection的readOnly属性。如果readOnly为false,则操作在master上执行,否则在slaves发生。对于6、这种手动开启事务的方式,在执行doInTransaction之前,spring将Connection设置为“autocommit=false”、“readOnly=false”,[email protected],最终事务中的所有方法调用均在master上执行,比如testManager.get()方法上声明的readOnly=true将被忽略,get操作仍然在master连接上执行。
?
? ? 所以,如果使用了spring托管事务管理,[email protected]??可实现LB;如果希望操作(read或者write)在master执行,则只需要将readOnly设置为false即可。使用transactionTemplate手动执行事务的(即autocommit=false),[email protected],进而将此事务中的所有操作在master执行。
?
? ? Replication链接对DBCP连接池是透明的,即连接池中的链接管理方式与单host并没有什么区别。
?
? ? 如下原理分析,基于本例和spring事务管理模式(对于基于AOP模式可能不适合),当Spring [email protected],将被拦截器拦截且通过反射机制方式执行(注释最终都是由拦截器驱动,参见TransactionInterceptor),且开启事务,如果readOnly为false时将会强制设置autocommit=false,在方法调用结束后,事务被自动提交。
? ? 1)[email protected],开启事务(SpringManagedTransaction),[email protected],并将此事务TransactionInfo绑定在当前线程。(TransactionInteceptor)
?
? ? 2)根据DataSource创建链接Connection,并将此链接绑定在当前线程(ConnectionHolder)。(DataSourceTransactionManager.doBegin())
?
? ? 3)每个sqlSessionTemplate实例内部都有个代理实例sqlSessionProxy,即通过sqlSessionTemplate执行的方法均由此代理实例执行;在执行操作之前,首先获取sqlSession实例,并将sqlSession绑定在当前线程(SqlSessionHolder,ThreadLocal)。
?
? ? 4)ibatis中使用此sqlSession执行数据库操作,sqlSession执行操作所使用的链接是从ConnectionHolder获取的。(DefaultSqlSession,SpringManagedTransaction)
?
? ? 5)[email protected],则继续循环3)~5),此过程中,所有的方法均公用一个sqlSession实例。
?
? ? 6)和1)对应,将TransactionInfo从当前线程解绑,并提交事务。
?
? ? 7)和3)对应,将SqlSessionHolder从当前线程解绑,并关闭sqlSession。
?
? ? 8)与2)对应,将ConnectionHolder从当前线程中解绑,并将Connection释放到连接池中。
?
? ? [email protected],都会按照上述过程执行;[email protected],[email protected]lSession实例、可能使用不同的Connection。
?
? ? 对于使用transactionTemplate方式手动开启事务的,过程稍微有些不同,在内部类doTransaction方法调用之前,将由spring创建事务、准备connection等与上述保持一致,并在方法执行后提交事务(如果抛出异常在rollback);[email protected]略,所有的dao层方法均使用同一个sqlSession和Connection实例。
?
二十三、Connector J问题小结
? ? 1、选择合适的Driver:对于Failover和LB协议,可以选用NonRegisterDriver;对于replication协议,我们需要使用ReplicationDriver。对于单点host,普通的Driver类即可。
? ? 2、为了便于跟踪replication模式下,LB协议是否生效或者query在哪个节点执行,我们可以开启“general_log”功能:
>SET GLOBAL general_log=1;>select @@global.general_log;
?
? ? 3、autoReconnect属性建议禁用,因为在事务操作中,如果链接重连并不会导致事务回滚而是继续执行,这会带来事务完整性的问题,导致数据不一致。具体原理参见【autoReconnect】
?
【上一篇:MySQL Replication环境构建与运维】
?
参考:
1、https://dev.mysql.com/doc/connector-j/en/connector-j-usagenotes-troubleshooting.html
2、https://dev.mysql.com/doc/connector-j/en/connector-j-multi-host-connections.html
3、https://dev.mysql.com/doc/connector-j/en/connector-j-reference-configuration-properties.html