집 >데이터 베이스 >MySQL 튜토리얼 >ReplicationConnection을 사용하여 MySQL로 인한 연결 실패 문제를 해결하는 방법
MySQL 데이터베이스에서 읽기와 쓰기를 분리하는 것은 서비스 품질을 향상시키는 일반적인 방법 중 하나입니다. 기술 솔루션의 경우 sharding-jdbc, Spring의 AbstractRoutingDatasource, MySQL과 같은 성숙한 오픈 소스 프레임워크 또는 솔루션이 많이 있습니다. -Router 등이 있으며 mysql-jdbc의 ReplicationConnection도 지원됩니다.
이 기사에서는 읽기-쓰기 분리의 기술적 선택에 대해 너무 많은 분석을 수행하지 않을 것입니다. 단지 druid를 데이터 소스로 사용하고 읽기-쓰기 분리를 위해 ReplicationConnection과 결합할 때 연결 실패 이유를 탐색하고 간단하고 효과적인 솔루션.
역사적인 이유로 특정 서비스에 연결 실패 예외가 있습니다. 주요 오류 보고서는 다음과 같습니다.
로그에서 이는 연결이 MySQL과 상호 작용하지 않았기 때문에 추론될 수 있습니다. 서버가 오랫동안 연결을 종료하여 일반적인 연결 실패 상황이 발생했습니다.
jdbc 구성
jdbc:mysql:replication://master_host:port,slave_host:port/database_name
druid 구성
testWhileIdle=true(예: 에 유휴 연결 확인);
timeBetweenEvictionRunsMillis=6000L (즉, 연결을 얻는 시나리오의 경우 연결이 1분 이상 유휴 상태이면 확인하고 연결이 유효하지 않으면 삭제됩니다. 재획득).
첨부 파일: DruidDataSource.getConnectionDirect의
처리 논리는 다음과 같습니다.
if (testWhileIdle) { final DruidConnectionHolder holder = poolableConnection.holder; long currentTimeMillis = System.currentTimeMillis(); long lastActiveTimeMillis = holder.lastActiveTimeMillis; long lastExecTimeMillis = holder.lastExecTimeMillis; long lastKeepTimeMillis = holder.lastKeepTimeMillis; if (checkExecuteTime && lastExecTimeMillis != lastActiveTimeMillis) { lastActiveTimeMillis = lastExecTimeMillis; } if (lastKeepTimeMillis > lastActiveTimeMillis) { lastActiveTimeMillis = lastKeepTimeMillis; } long idleMillis = currentTimeMillis - lastActiveTimeMillis; long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis; if (timeBetweenEvictionRunsMillis <= 0) { timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS; } if (idleMillis >= timeBetweenEvictionRunsMillis || idleMillis < 0 // unexcepted branch ) { boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn); if (!validate) { if (LOG.isDebugEnabled()) { LOG.debug("skip not validate connection."); } discardConnection(poolableConnection.holder); continue; } } }
mysql 시간 제한 매개 변수 구성wait_timeout=3600(3600초, 즉: 연결이 1시간 이상 서버와 상호 작용하지 않은 경우) 시간이면 연결은 서버 종료됩니다). 분명히, 위의 구성을 바탕으로 하면, 기존의 이해에 따르면 "서버로부터 성공적으로 수신된 마지막 패킷은 xxx,xxx,xxx 밀리초 전이었습니다"라는 문제가 발생해서는 안 됩니다. (물론 당시에는 데이터베이스 연결을 끊기 위해 수동으로 개입할 가능성도 배제되었습니다.)
'가상' 경험이 문제를 설명할 수 없을 때, 피상적인 경험의 제약에서 벗어나 문제의 근본 원인을 파악해야 하는 경우가 많습니다. 그렇다면 이 문제의 진짜 원인은 무엇일까?
druid를 사용하여 데이터 소스를 관리하고 읽기-쓰기 분리를 위해 mysql-jdbc의 기본 ReplicationConnection과 결합할 때 실제로 druid일 때 ReplicationConnection 프록시 개체에는 마스터와 슬레이브의 두 세트의 연결이 있습니다. 연결 감지를 수행하는 경우 마스터에 연결하기 위해 슬레이브 연결을 오랫동안 사용하지 않으면 연결 실패 문제가 발생하는 것을 감지할 수만 있습니다.
com.mysql.jdbc.Driver 소스 코드와 결합하면 mysql-jdbc에서 연결을 얻는 주요 과정을 보면 어렵지 않다.
"jdbc:mysql:replication://"으로 시작하도록 구성된 jdbc-url과 mysql-jdbc를 통해 얻은 연결은 실제로 "jdbc: mysql:replication://" 첫 번째 호스트와 포트는 마스터 연결에 해당하고 후속 호스트와 포트는 슬레이브 연결에 해당합니다. 여러 슬레이브 구성이 있는 시나리오의 경우 기본적으로 로드 균형 조정을 위해 무작위 전략이 사용됩니다.
ReplicationConnection 프록시 객체는 JDK 동적 프록시를 사용하여 생성됩니다. InvocationHandler의 구체적인 구현은 ReplicationConnectionProxy입니다. 키 코드는 다음과 같습니다.
public static ReplicationConnection createProxyInstance(List<String> masterHostList, Properties masterProperties, List<String> slaveHostList, Properties slaveProperties) throws SQLException { ReplicationConnectionProxy connProxy = new ReplicationConnectionProxy(masterHostList, masterProperties, slaveHostList, slaveProperties); return (ReplicationConnection) java.lang.reflect.Proxy.newProxyInstance(ReplicationConnection.class.getClassLoader(), INTERFACES_TO_PROXY, connProxy); }
데이터베이스 연결 프록시에 대한 주요 구성 요소는 다음과 같습니다. :
ReplicationConnectionProxy에는 masterConnection과slavesConnection이라는 두 개의 실제 연결 개체가 있습니다. currentConnetion(현재 연결)은 masterConnection 또는slavesConnection으로 전환할 수 있습니다. 전환 방법은 readOnly를 설정하여 달성할 수 있습니다.
비즈니스 로직에서도 읽기-쓰기 분리를 구현하는 핵심이 여기에 있습니다. 간단히 말하면, 읽기-쓰기 분리를 위해 ReplicationConnection을 사용할 때 "연결의 readOnly 속성을 설정"하는 aop만 만들면 됩니다.
비즈니스 로직에서 얻은 Connection 프록시 객체인 ReplicationConnectionProxy를 기반으로 데이터베이스 액세스 시 주요 로직은 무엇인가요?
비즈니스 로직의 경우 획득한 Connection 인스턴스는 ReplicationConnection 프록시 객체입니다. 이 프록시 객체는 ReplicationConnectionProxy와 ReplicationMySQLConnection을 통해 서로 협력하여 데이터베이스 액세스 처리를 동시에 완료합니다. 연결 관리 역할도 수행합니다. 핵심 로직은 다음과 같습니다.
对于prepareStatement等常规逻辑,ConnectionMySQConnection获取到当前连接进行处理(普通的读写分离的处理的重点正是在此);此时,重点提及pingInternal方法,其处理方式也是获取当前连接,然后执行pingInternal逻辑。
对于ping()这个特殊逻辑,图中描述相对简单,但主体含义不变,即:对master连接和sleves连接都要进行ping()的处理。
图中,pingInternal流程和druid的MySQ连接检查有关,而ping的特殊处理,也正是解决问题的关键。
druid中对MySQL连接检查的默认实现类是MySqlValidConnectionChecker,其中核心逻辑如下:
public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception { if (conn.isClosed()) { return false; } if (usePingMethod) { if (conn instanceof DruidPooledConnection) { conn = ((DruidPooledConnection) conn).getConnection(); } if (conn instanceof ConnectionProxy) { conn = ((ConnectionProxy) conn).getRawObject(); } if (clazz.isAssignableFrom(conn.getClass())) { if (validationQueryTimeout <= 0) { validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT; } try { ping.invoke(conn, true, validationQueryTimeout * 1000); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof SQLException) { throw (SQLException) cause; } throw e; } return true; } } String query = validateQuery; if (validateQuery == null || validateQuery.isEmpty()) { query = DEFAULT_VALIDATION_QUERY; } Statement stmt = null; ResultSet rs = null; try { stmt = conn.createStatement(); if (validationQueryTimeout > 0) { stmt.setQueryTimeout(validationQueryTimeout); } rs = stmt.executeQuery(query); return true; } finally { JdbcUtils.close(rs); JdbcUtils.close(stmt); } }
对应服务中使用的mysql-jdbc(5.1.45版),在未设置“druid.mysql.usePingMethod”系统属性的情况下,默认usePingMethod为true,如下:
public MySqlValidConnectionChecker(){ try { clazz = Utils.loadClass("com.mysql.jdbc.MySQLConnection"); if (clazz == null) { clazz = Utils.loadClass("com.mysql.cj.jdbc.ConnectionImpl"); } if (clazz != null) { ping = clazz.getMethod("pingInternal", boolean.class, int.class); } if (ping != null) { usePingMethod = true; } } catch (Exception e) { LOG.warn("Cannot resolve com.mysql.jdbc.Connection.ping method. Will use 'SELECT 1' instead.", e); } configFromProperties(System.getProperties()); } @Override public void configFromProperties(Properties properties) { String property = properties.getProperty("druid.mysql.usePingMethod"); if ("true".equals(property)) { setUsePingMethod(true); } else if ("false".equals(property)) { setUsePingMethod(false); } }
同时,可以看出MySqlValidConnectionChecker中的ping方法使用的是MySQLConnection中的pingInternal方法,而该方法,结合上面对ReplicationConnection的分析,当调用pingInternal时,只是对当前连接进行检验。执行检验连接的时机是通过DrduiDatasource获取连接时,此时未设置readOnly属性,检查的连接,其实只是ReplicationConnectionProxy中的master连接。
此外,如果通过“druid.mysql.usePingMethod”属性设置usePingMeghod为false,其实也会导致连接失效的问题,因为:当通过valideQuery(例如“select 1”)进行连接校验时,会走到ReplicationConnection中的普通查询逻辑,此时对应的连接依然是master连接。
题外一问:ping方法为什么使用“pingInternal”,而不是常规的ping?
原因:pingInternal预留了超时时间等控制参数。
在服务中,使用的MySQL JDBC版本是5.1.45,并且使用的Druid版本是1.1.20。经过对其他高版本依赖的了解,依然存在该问题。
修改的工作量主要在于数据源配置和aop调整,但需要一定的整体回归验证成本,鉴于涉及该问题的服务重要性一般,暂不做大调整。
基于原有ReplicationConnection的功能,拓展pingInternal调整为普通的ping,集成原有Driver拓展新的Driver。方案可行,但修改成本不算小。
为简单高效解决问题,选择拓展MySqlValidConnectionChecker,并在druid数据源中加上对应配置即可。拓展如下:
public class MySqlReplicationCompatibleValidConnectionChecker extends MySqlValidConnectionChecker { private static final Log LOG = LogFactory.getLog(MySqlValidConnectionChecker.class); /** * */ private static final long serialVersionUID = 1L; @Override public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception { if (conn.isClosed()) { return false; } if (conn instanceof DruidPooledConnection) { conn = ((DruidPooledConnection) conn).getConnection(); } if (conn instanceof ConnectionProxy) { conn = ((ConnectionProxy) conn).getRawObject(); } if (conn instanceof ReplicationConnection) { try { ((ReplicationConnection) conn).ping(); LOG.info("validate connection success: connection=" + conn.toString()); return true; } catch (SQLException e) { LOG.error("validate connection error: connection=" + conn.toString(), e); throw e; } } return super.isValidConnection(conn, validateQuery, validationQueryTimeout); } }
ReplicatoinConnection.ping()的实现逻辑中,会对所有master和slaves连接进行ping操作,最终每个ping操作都会调用到LoadBalancedConnectionProxy.doPing进行处理,而此处,可在数据库配置url中设置loadBalancePingTimeout属性设置超时时间。
위 내용은 ReplicationConnection을 사용하여 MySQL로 인한 연결 실패 문제를 해결하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!