蛋疼的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 。