日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

MySQL數(shù)據(jù)庫讀寫分離,是提高服務(wù)質(zhì)量的常用手段之一,而對(duì)于技術(shù)方案,有很多成熟開源框架或方案,例如:sharding-jdbc、spring中的AbstractRoutingDatasource、MySQL-Router等,而mysql-jdbc中的ReplicationConnection亦可支持。本文暫不對(duì)讀寫分離的技術(shù)選型做過多的分析,只是探索在使用druid作為數(shù)據(jù)源、結(jié)合ReplicationConnection做讀寫分離時(shí),連接失效的原因,并找到一個(gè)簡(jiǎn)單有效的解決方案。

  • 問題背景

由于歷史原因,某幾個(gè)服務(wù)出現(xiàn)連接失效異常,關(guān)鍵報(bào)錯(cuò)如下:

MySQL使用ReplicationConnection導(dǎo)致的連接失效分析與解決

 

從日志不難看出,這是由于該連接長(zhǎng)時(shí)間未和MySQL服務(wù)端交互,服務(wù)端已將連接關(guān)閉,典型的連接失效場(chǎng)景。

涉及的主要配置如下:

jdbc配置

jdbc:mysql:replication://master_host:port,slave_host:port/database_name

druid配置

testWhileIdle=true(即,開啟了空閑連接檢查);


timeBetweenEvictionRunsMillis=6000L(即,對(duì)于獲取連接的場(chǎng)景,如果某連接空閑時(shí)間超過1分鐘,將會(huì)進(jìn)行檢查,如果連接無效,將拋棄后重新獲取)。

附:
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超時(shí)參數(shù)配置

wait_timeout=3600(3600秒,即:如果某連接超過一個(gè)小時(shí)和服務(wù)端沒有交互,該連接將會(huì)被服務(wù)端kill)。

顯而易見,基于如上配置,按照常規(guī)理解,不應(yīng)該出現(xiàn)“The last packet successfully received from server was xxx,xxx,xxx milliseconds ago”的問題。(當(dāng)然,當(dāng)時(shí)也排除了人工介入kill掉數(shù)據(jù)庫連接的可能)。

當(dāng)“理所應(yīng)當(dāng)”的經(jīng)驗(yàn)解釋不了問題所在,往往需要跳出可能浮于表面經(jīng)驗(yàn)束縛,來一次追根究底。那么,該問題的真正原因是什么呢?

  • 本質(zhì)原因

當(dāng)使用druid管理數(shù)據(jù)源,結(jié)合mysql-jdbc中原生的ReplicationConnection做讀寫分離時(shí),ReplicationConnection代理對(duì)象中實(shí)際存在master和slaves兩套連接,druid在做連接檢測(cè)時(shí)候,只能檢測(cè)到其中的master連接,如果某個(gè)slave連接長(zhǎng)時(shí)間未使用,會(huì)導(dǎo)致連接失效問題。

  • 原因分析

mysql-jdbc中,數(shù)據(jù)庫驅(qū)動(dòng)對(duì)連接的處理過程

結(jié)合com.mysql.jdbc.Driver源碼,不難看出mysql-jdbc中獲取連接的主體流程如下:

MySQL使用ReplicationConnection導(dǎo)致的連接失效分析與解決

 

對(duì)于以“jdbc:mysql:replication://”開頭配置的jdbc-url,通過mysql-jdbc獲取到的連接,其實(shí)是一個(gè)ReplicationConnection的代理對(duì)象,默認(rèn)情況下,“jdbc:mysql:replication://”后的第一個(gè)host和port對(duì)應(yīng)master連接,其后的host和port對(duì)應(yīng)slaves連接,而對(duì)于存在多個(gè)slave配置的場(chǎng)景,默認(rèn)使用隨機(jī)策略進(jìn)行負(fù)載均衡。

ReplicationConnection代理對(duì)象,使用JDK動(dòng)態(tài)代理生成的,其中InvocationHandler的具體實(shí)現(xiàn),是
ReplicationConnectionProxy,關(guān)鍵代碼如下:

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的重要組成

關(guān)于數(shù)據(jù)庫連接代理,
ReplicationConnectionProxy中的主要組成如下圖:

MySQL使用ReplicationConnection導(dǎo)致的連接失效分析與解決

 


ReplicationConnectionProxy存在masterConnection和slavesConnection兩個(gè)實(shí)際連接對(duì)象,currentCo.NETion(當(dāng)前連接)可以切換成mastetConnection或者slavesConnection,切換方式可以通過設(shè)置readOnly實(shí)現(xiàn)。業(yè)務(wù)邏輯中,實(shí)現(xiàn)讀寫分離的核心也在于此,簡(jiǎn)單來說:使用ReplicationConnection做讀寫分離時(shí),只要做一個(gè)“設(shè)置connection的readOnly屬性的”aop即可。

基于
ReplicationConnectionProxy,業(yè)務(wù)邏輯中獲取到的Connection代理對(duì)象,數(shù)據(jù)庫訪問時(shí)的主要邏輯是什么樣的呢?

ReplicationConnection代理對(duì)象處理過程

對(duì)于業(yè)務(wù)邏輯而言,獲取到的Connection實(shí)例,是ReplicationConnection代理對(duì)象,該代理對(duì)象通過
ReplicationConnectionProxy和ReplicationMySQLConnection相互協(xié)同完成對(duì)數(shù)據(jù)庫訪問的處理,其中ReplicationConnectionProxy在實(shí)現(xiàn) InvocationHandler的同時(shí),還充當(dāng)對(duì)連接管理的角色,核心邏輯如下圖:

MySQL使用ReplicationConnection導(dǎo)致的連接失效分析與解決

 

對(duì)于prepareStatement等常規(guī)邏輯,ConnectionMySQConnection獲取到當(dāng)前連接進(jìn)行處理(普通的讀寫分離的處理的重點(diǎn)正是在此);此時(shí),重點(diǎn)提及pingInternal方法,其處理方式也是獲取當(dāng)前連接,然后執(zhí)行pingInternal邏輯。

對(duì)于ping()這個(gè)特殊邏輯,圖中描述相對(duì)簡(jiǎn)單,但主體含義不變,即:對(duì)master連接和sleves連接都要進(jìn)行ping()的處理。

圖中,pingInternal流程和druid的MySQ連接檢查有關(guān),而ping的特殊處理,也正是解決問題的關(guān)鍵。

druid數(shù)據(jù)源對(duì)MySQ連接的檢查

druid中對(duì)MySQL連接檢查的默認(rèn)實(shí)現(xiàn)類是
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);

    }




}

對(duì)應(yīng)服務(wù)中使用的mysql-jdbc(5.1.45版),在未設(shè)置“druid.mysql.usePingMethod”系統(tǒng)屬性的情況下,默認(rèn)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);
    }
}

同時(shí),可以看出
MySqlValidConnectionChecker中的 ping方法使用的是MySQLConnection中的pingInternal方法,而該方法,結(jié)合上面對(duì)ReplicationConnection的分析,當(dāng)調(diào)用pingInternal時(shí),只是對(duì)當(dāng)前連接進(jìn)行檢驗(yàn)。執(zhí)行檢驗(yàn)連接的時(shí)機(jī)是通過DrduiDatasource獲取連接時(shí),此時(shí)未設(shè)置readOnly屬性,檢查的連接,其實(shí)只是ReplicationConnectionProxy中的master連接。

此外,如果通過“druid.mysql.usePingMethod”屬性設(shè)置usePingMeghod為false,其實(shí)也會(huì)導(dǎo)致連接失效的問題,因?yàn)椋寒?dāng)通過valideQuery(例如“select 1”)進(jìn)行連接校驗(yàn)時(shí),會(huì)走到ReplicationConnection中的普通查詢邏輯,此時(shí)對(duì)應(yīng)的連接依然是master連接。

題外一問 :ping方法為什么使用“pingInternal”,而不是常規(guī)的ping?原因:pingInternal預(yù)留了超時(shí)時(shí)間等控制參數(shù)。

  • 解決方式

調(diào)整依賴版本

服務(wù)中使用的mysql-jdbc版本為5.1.45,druid版本為1.1.20。經(jīng)過對(duì)其他高版本依賴的了解,依然存在該問題。

修改讀寫分離實(shí)現(xiàn)

修改的工作量主要在于數(shù)據(jù)源配置和aop調(diào)整,但需要一定的整體回歸驗(yàn)證成本,鑒于涉及該問題的服務(wù)重要性一般,暫不做大調(diào)整。

拓展mysql-jdbc驅(qū)動(dòng)

基于原有ReplicationConnection的功能,拓展pingInternal調(diào)整為普通的ping,集成原有Driver拓展新的Driver。方案可行,但修改成本不算小。

基于druid,拓展MySQL連接檢查

為簡(jiǎn)單高效解決問題,選擇拓展
MySqlValidConnectionChecker, 并在 druid 數(shù)據(jù)源中加上對(duì)應(yīng)配置即可。拓展 如下:

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()的實(shí)現(xiàn)邏輯中,會(huì)對(duì)所有master和slaves連接進(jìn)行ping操作,最終每個(gè)ping操作都會(huì)調(diào)用到LoadBalancedConnectionProxy.doPing進(jìn)行處理,而此處,可在數(shù)據(jù)庫配置url中設(shè)置loadBalancePingTimeout屬性設(shè)置超時(shí)時(shí)間。

分享到:
標(biāo)簽:MySQL
用戶無頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定