蛋疼的mysql_ping()以及MYSQL_OPT_RECONNECT

From: https://www.felix021.com/blog/read.php?2102php

昨天@Zind同窗找到我以前的一篇blog(已經修改),裏面提到了mysql_ping和MYSQL_OPT_RECONNECT的一些事情。

之因此寫那篇blog,是由於去年寫的一些代碼遇到了「2006:MySQL server has gone away」錯誤。這個問題是由於wait_timeout這個參數的默認值是28800,也就是說,若是一個鏈接連續8個小時沒有任何請求,那麼Server端就會把它斷開。在測試環境中一個晚上沒有請求很正常……因而次日早上來的時候就發現這個錯誤了。

其實我有考慮這個問題的,真的……由於我知道php裏面有個函數叫作mysql_ping(),PHP手冊上說:「mysql_ping() 檢查到服務器的鏈接是否正常。若是斷開,則自動嘗試鏈接。本函數可用於空閒好久的腳原本檢查服務器是否關閉了鏈接,若是有必要則從新鏈接上。」

回想起來,之前真是很傻很天真。根據MySQL官方C API裏mysql_ping()的文檔:"Checks whether the connection to the server is working. If the connection has gone down and auto-reconnect is enabled an attempt to reconnect is made. ... Auto-reconnect is disabled by default. To enable it, call mysql_options() with the MYSQL_OPT_RECONNECT option",也就是說,它實際上還依賴於MYSQL_OPT_RECONNECT這個配置,而這個配置默認(自5.0.3開始)是關閉的!

雖然想起來很憤怒很蛋疼,不過看到 libmysql/client.c: mysql_init() 裏的註釋就淡定了:html

引用
By default we don't reconnect because it could silently corrupt data (after reconnection you potentially lose table locks, user variables, session variables (transactions but they are specifically dealt with in mysql_reconnect()).  This is a change: < 5.0.3 mysql->reconnect was set to 1 by default. 



好吧,既然有問題,那就正視它。解決辦法是調用 mysql_options ,將MYSQL_OPT_RECONNECT設置爲1:mysql

char value = 1;
mysql_options(mysql, MYSQL_OPT_RECONNECT, &value);



可是!! 在mysql 5.0.19 以前,mysql->reconnect = 0 這一句是放在 mysql_real_connect() 裏面的!也就是說,若是你不能像處理其餘選項同樣,而是必須在mysql_real_connect()以前設置MYSQL_OPT_RECONNECT,坑爹啊!

好吧好吧,總之,關於坑的問題暫告一段落,結論就是,不論是哪一個版本,若是你想要啓用自動重連,最好都是在mysql_real_connect()以後,反正不會錯。

而後這篇的重點來了(前面彷佛太羅嗦了點):MYSQL_OPT_RECONNECT的文檔裏頭說了,這個選項是用來啓用/禁用(當發現鏈接斷開時的)自動重連,那麼,MYSQL何時會發現連接斷開呢?

這個問題可能太大了,不過不妨先去追一下,mysql_ping()作了啥。

下載源碼 http://cdn.mysql.com/Downloads/MySQL-5.1/mysql-5.1.67.tar.gz ,解壓之後ctags -R,再vim -t mysql_ping ,立刻就定位到了,彷佛太簡單了點:sql

int STDCALL
mysql_ping(MYSQL *mysql)
{
  int res; 
  DBUG_ENTER("mysql_ping");
  res= simple_command(mysql,COM_PING,0,0,0);        //試着向服務器發送一個ping包
  if (res == CR_SERVER_LOST && mysql->reconnect)    //若是server掛了,而mysql->reconnect爲true
    res= simple_command(mysql,COM_PING,0,0,0);      //再ping一次??
  DBUG_RETURN(res);
}



好吧,看來關鍵在於這個simple_command了。ctrl+],原來是這樣:vim

# define simple_command(mysql, command, arg, length, skip_check) \
  (*(mysql)->methods->advanced_command)(mysql, command, 0, 0, arg, length, skip_check, NULL)



好吧,先去追一下MYSQL,裏頭有個 const struct st_mysql_methods *methods ,再追一下 st_mysql_methods ....安全

typedef struct st_mysql_methods
{
  my_bool (*read_query_result)(MYSQL *mysql);
  my_bool (*advanced_command)(MYSQL *mysql, enum enum_server_command command,
                  const unsigned char *header, unsigned long header_length,
                  const unsigned char *arg, unsigned long arg_length,
                  my_bool skip_check, MYSQL_STMT *stmt);
  ......


坑爹啊!又是這種鳥代碼!蛋疼的C語言!struct只有屬性沒有方法!沒辦法,只能暴力了:服務器

引用
find -name '*.c' -exec /bin/grep '{}' -Hne 'mysql->methods *=' ';'
./libmysql_r/client.c:1907:  mysql->methods= &client_methods;
./sql-common/client.c:1907:  mysql->methods= &client_methods;
./libmysql/client.c:1907:  mysql->methods= &client_methods;
./libmysqld/libmysqld.c:120:  mysql->methods= &embedded_methods;
./sql/client.c:1907:  mysql->methods= &client_methods;



果斷追到client_methods:session

static MYSQL_METHODS client_methods=
{
  cli_read_query_result,                      /* read_query_result */
  cli_advanced_command,                        /* advanced_command */
  ...


也就是說simple_command最後調用了cli_advanced_command這個函數。前面的 simple_command(mysql,COM_PING,0,0,0) 至關因而調用了 cli_advanced_command(mysql, COM_PING, 0, 0, 0, 0, 0, NULL) 。

這個函數作了啥呢。。。其實也不復雜:
1. 設置默認返回值爲1 (意外出錯goto時被返回)
2. 設置sigpipe的handler(以便忽略它)
3. 若是 mysql->net.vio == 0 ,那麼調用mysql_reconnect重連,失敗的話就返回1
4. mysql沒準備好,返回1
5. 清除以前的信息(錯誤碼、緩衝區、affected_rows)等等
6. 調用net_write_command將命令發送給server,若是失敗:
    6.1 檢查錯誤信息,若是是由於發送包太大,goto end
    6.2 調用end_server(mysql)關閉鏈接
    6.3 調用mysql_reconnect嘗試重連,若是失敗goto end
    6.4 再次調用net_write_command將命令發送給server,失敗則goto end
7. 設置result = 0(發送成功)
8. 若是參數中要求檢查server的返回,則讀取一個packet進行檢查(失敗的話就result=1)
9. (end標籤) 
10. 恢復sigpipe
11. 返回result

能夠看到,這裏兩次調用了mysql_reconnect,但都是有條件的:第一次是在mysql->net.vio == 0的狀況下,第二次是net_write_command失敗且不是由於包太大的狀況。vio相關的代碼看得一頭霧水,實在找不出頭緒,因而決定暴力一點:直接修改這個函數,加入一堆fprintf(stderr, ...)(具體加在哪裏就不說了,反正使勁塞就是了),而後寫了一個C代碼:ide

# include <stdio.h>
#include <stdlib.h>
#include <mysql/mysql.h>

void do_err(MYSQL *mysql) {
    if (mysql_errno(mysql)) {
        fprintf(stderr, "%d:%s\n", mysql_errno(mysql), mysql_error(mysql));
        exit(mysql_errno(mysql));
    }
}

int main()
{
    MYSQL * mysql = mysql_init(NULL);
    do_err(mysql);

    mysql_real_connect(mysql, "127.0.0.1", "root", "123456", "test", 3306, NULL, 0);
    do_err(mysql);

    char value = 1;
    mysql_options(mysql, MYSQL_OPT_RECONNECT, &value);
    
    char cmd[1024] = "SELECT * FROM t";
    while (1) {
        mysql_query(mysql, cmd);
        do_err(mysql);

        MYSQL_RES *result = mysql_store_result(mysql);

        MYSQL_ROW  row;
        while ((row = mysql_fetch_row(result)) != NULL) {
            int i, num_fields = mysql_num_fields(result);
            for (i = 0; i < num_fields; i++) 
                printf("%s\t", row[i] ? row[i] : "NULL"); 
            //注意上一句是否是二進制安全的,由於row裏頭可能包含\0,也可能末尾沒有\0
            printf("\n");
        }

        mysql_free_result(result);
        printf("press enter..."); getchar();
    }
    mysql_close(mysql);
    return 0;
}



運行輸出:函數

引用
inside mysql_real_query
mysql->net.vio = 0x90e760
mysql->status = 0
net write_command
after send_query
---
1
2
press enter...//按回車以前先重啓一下mysql server,下面這幾句按照函數調用層次進行手動縮進了……
inside mysql_real_query
    mysql->net.vio = 0x90e760 //進入cli_advanced_command
    mysql->status = 0
    net_write_command
    end_server //說明net_write_command失敗了
        inside mysql_reconnect //它會調用mysql_real_query
            inside mysql_real_query
                mysql->net.vio = 0x919990 //因而又回到了cli_advanced_command
                mysql->status = 0
                net_write_command //此次成功了
            after send_query  //這句我是寫在mysql_real_query裏面的
        reconnect succeded
    after reconnect: mysql->status = 0
after send_query //因此又來一次。。



根據fprintf的輸出,發如今正常狀況下,mysql->net.vio這個指針並不等於0,因此第一個mysql_reconnect不會被調用。而net_write_command也是正確執行,第二個reconnect也沒被調用。

而在執行完一個query,而後重啓mysql server再執行query (mysql_query => mysql_real_query => mysql_send_query => cli_advanced_command),就會發現,mysql->net.vio仍然不等於0,可是net_write_command失敗了,因而先調用了end_server()(這裏面會將mysql->net.vio設置爲0,不過不影響後面的流程...),而後調用了第二個reconnect,這個reconnect會調用mysql_init()以及mysql_real_query()執行一些初始化的命令,因而又回到cli_advanced_command,再一步一步回溯。。。

綜上可知,若是設置了MYSQL_OPT_RECONNECT(),那麼mysql_query()是能夠完成自動重連的。實際上,因爲cli_advanced_command會在必要狀況下調用mysql_reconnect(實際上這個函數也只在這裏被調用),所以,全部用到了cli_read_query_result的地方(或者simple_command),也均可以完成自動重連。

完結。

//混蛋,這篇純粹是爲了湊一月至少一篇這個目標啊!

--


轉載請註明出自 https://www.felix021.com/blog/read.php?2102 ,如是轉載文則註明原出處,謝謝:)
RSS訂閱地址: http://www.felix021.com/blog/feed.php 。

相關文章
相關標籤/搜索