<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
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秒,即:如果某連線超過一個小時和伺服器端沒有互動,該連線將會被伺服器端kill)。 顯而易見,基於如上設定,按照常規理解,不應該出現“The last packet successfully received from server was xxx,xxx,xxx milliseconds ago”的問題。(當然,當時也排除了人工介入kill掉資料庫連線的可能)。
當“理所應當”的經驗解釋不了問題所在,往往需要跳出可能浮於表面經驗束縛,來一次追根究底。那麼,該問題的真正原因是什麼呢?
當使用druid管理資料來源,結合mysql-jdbc中原生的ReplicationConnection做讀寫分離時,ReplicationConnection代理物件中實際存在master和slaves兩套連線,druid在做連線檢測時候,只能檢測到其中的master連線,如果某個slave連線長時間未使用,會導致連線失效問題。
結合com.mysql.jdbc.Driver原始碼,不難看出mysql-jdbc中獲取連線的主體流程如下:
對於以“jdbc:mysql:replication://”開頭設定的jdbc-url,通過mysql-jdbc獲取到的連線,其實是一個ReplicationConnection的代理物件,預設情況下,“jdbc:mysql:replication://”後的第一個host和port對應master連線,其後的host和port對應slaves連線,而對於存在多個slave設定的場景,預設使用隨機策略進行負載均衡。
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中的主要組成如下圖:
ReplicationConnectionProxy存在masterConnection和slavesConnection兩個實際連線物件,currentConnetion(當前連線)可以切換成mastetConnection或者slavesConnection,切換方式可以通過設定readOnly實現。
業務邏輯中,實現讀寫分離的核心也在於此,簡單來說:使用ReplicationConnection做讀寫分離時,只要做一個“設定connection的readOnly屬性的”aop即可。
基於ReplicationConnectionProxy,業務邏輯中獲取到的Connection代理物件,資料庫存取時的主要邏輯是什麼樣的呢?
對於業務邏輯而言,獲取到的Connection範例,是ReplicationConnection代理物件,該代理物件通過ReplicationConnectionProxy和ReplicationMySQLConnection相互協同完成對資料庫存取的處理,其中ReplicationConnectionProxy在實現 InvocationHandler的同時,還充當對連線管理的角色,核心邏輯如下圖:
對於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屬性設定超時時間。
以上就是MySQL使用ReplicationConnection導致連線失效解決的詳細內容,更多關於MySQL Replication連線失效的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45