unixODBC驱动链接的数据库客户端和程序直接链接的数据库客户端库一定要一致

这两天调试了一个诡异的问题,使用unixODBC连接MySQL数据库,最后程序core dumpMySQL client代码的fill_ird_data_lengths函数:

void fill_ird_data_lengths(DESC *ird, ulong *lengths, uint fields)
{
  ......
  assert(fields == ird->count);
}

fileds是一个很离谱的数,一看就像是内存值被破坏了。

经过艰苦的单步调试,最后查出的问题是unixODBC驱动链接的MySQL数据库客户端库和程序直接链接的MySQL数据库客户端库不一样(因为程序还没有完全改完,有一部分code还会直接调用MySQL数据库客户端库)。

这个坑一定要记住!

ODBC中的三种变量类型

无论是使用ODBC的新手或老兵,看到SQLCHARSOL_C_CHARSQL_CHAR,可能都会有些糊涂。它们之间的区别到底是什么呢?

(1)第一种是ODBC定义的数据类型(像SQLCHAR):SQL开头,后面跟着一系列大写字符,但是没有下划线。这种数据类型定义在sqltypes.h中,比如:

typedef signed short int   SQLSMALLINT;
typedef SQLSMALLINT     SQLRETURN;

#if (ODBCVER >= 0x0300)
typedef void *                  SQLHANDLE; 
......
#endif

这种数据类型可以用来定义变量,ODBC API的变量类型都是这种类型:

#if (ODBCVER >= 0x0300)
    SQLRETURN  SQL_API SQLAllocHandle(SQLSMALLINT HandleType,
                                      SQLHANDLE InputHandle, SQLHANDLE *OutputHandle);
#endif

这样做的好处是ODBC提供了一套自己的变量类型,相当于封装了一层,使用者不用关心底层具体的变量类型实现细节。即使底层的变量类型做了修改,使用者的代码也不用做任何改动。
这种数据类型和C语言数据类型的映射如下:

ODBC 类型 C 类型
SQLCHAR char
SQLSCHAR signed char
SQLINTEGER long int

(2)第二种是C语言的数据类型编码(SQL_C_CHAR)。他们并不是真正的数据类型,而是针对上面提到的的ODBC定义的数据类型的编码,所以不能用来定义变量。他们可以用作ODBC API的参数。这种数据类型定义在sqlext.h中,比如:

#define SQL_C_CHAR    SQL_CHAR             /* CHAR, VARCHAR, DECIMAL, NUMERIC */
#define SQL_C_LONG    SQL_INTEGER          /* INTEGER                      */
#define SQL_C_SHORT   SQL_SMALLINT         /* SMALLINT                     */
#define SQL_C_FLOAT   SQL_REAL             /* REAL                         */
#define SQL_C_DOUBLE  SQL_DOUBLE           /* FLOAT, DOUBLE                */

这种数据类型和ODBC数据类型的映射如下:

整数编码 ODBC 类型
SQL_C_CHAR SQLCHAR
SQL_S_STINYINT SQLSCHAR
SQL_C_SLONG SQLINTEGER

(3)第三种是SQL数据类型编码。同第二种一样,他们也不是真正的数据类型,而是提供SQL数据类型和编程语言数据类型的关联,因为也是数字编码,所以也不能用来定义变量。他们可以用作ODBC API的参数。这种数据类型定义在sqlext.h中,比如:

#define SQL_C_CHAR    SQL_CHAR             /* CHAR, VARCHAR, DECIMAL, NUMERIC */
#define SQL_C_LONG    SQL_INTEGER          /* INTEGER                      */
#define SQL_C_SHORT   SQL_SMALLINT         /* SMALLINT                     */
#define SQL_C_FLOAT   SQL_REAL             /* REAL                         */
#define SQL_C_DOUBLE  SQL_DOUBLE           /* FLOAT, DOUBLE                */

这种数据类型和SQL数据类型的映射如下:

整数编码 SQL类型
SQL_CHAR Char(n)
SQL_VARCHAR Varchar(n)
SQL_SMALLINT Smallint

使用MySQL ODBC一次提交多条SQL语句

MySQL ODBC3.51.18版本开始支持一次提交多个语句(请参考:http://dev.mysql.com/doc/connector-odbc/en/connector-odbc-configuration-connection-parameters.html)。方法是要给odbc.ini的数据源配置值为67108864option。如下图所示:

[DB1]
......
option = 67108864

注意这个option一定要配在odbc.ini的数据源上,而不能配在odbcinst.ini的驱动上。因为MySQLODBC代码只从odbc.ini文件查找option

int ds_lookup(DataSource *ds)
{
    ......
    for (used= 0; used < size; used += sqlwcharlen(entries) + 1,
                             entries += sqlwcharlen(entries) + 1)
      {
        int valsize;
        ds_map_param(ds, entries, &dest, &intdest, &booldest);

        if ((valsize= SQLGetPrivateProfileStringW(ds->name, entries, W_EMPTY,
                                                  val, ODBCDATASOURCE_STRLEN,
                                                  W_ODBC_INI)) < 0)
        {
          rc= 1;
          goto end;
        }
        else if (!valsize)
          /* skip blanks */;
        else if (dest && !*dest)
          ds_set_strnattr(dest, val, valsize);
        else if (intdest)
          *intdest= sqlwchartoul(val, NULL);
        else if (booldest)
          *booldest= sqlwchartoul(val, NULL) > 0;
        else if (!sqlwcharcasecmp(W_OPTION, entries))
          ds_set_options(ds, ds_get_options(ds) | sqlwchartoul(val, NULL));

        RESTORE_MODE();
      }

    ......
}

验证这个配置项是否成功,可以通过wireshark查看建立连接时的抓包:

捕获
可以看到“Multiple Statements”值为1,设置成功。

unixODBC的SYSTEM_FILE_PATH

unixODBC中,SYSTEM_FILE_PATH定义了像odbc.iniodbcinst.ini这些系统文件存放的位置。默认情况下,是在“/usr/local/etc”这个文件夹下。我们看一下用到SYSTEM_FILE_PATH的地方。

(1)odbcinst_system_file_path

char *odbcinst_system_file_path( char *buffer )
{
    char *path;
    static char save_path[ 512 ];
    static int saved = 0;

    if ( saved ) {
        return save_path;
    }

    if (( path = getenv( "ODBCSYSINI" ))) {
        strcpy( buffer, path );
    strcpy( save_path, buffer );
    saved = 1;
        return buffer;
    }
#ifdef SYSTEM_FILE_PATH
    else {
    strcpy( save_path, SYSTEM_FILE_PATH );
    saved = 1;
        return SYSTEM_FILE_PATH;
    }
#else
    else {
    strcpy( save_path, "/etc" );
    saved = 1;
        return "/etc";
    }
#endif
}

可以看到在查找系统文件路径时,首先会看ODBCSYSINI这个环境变量有没有赋值,如果有,以这个变量定义的值为准,否则如果SYSTEM_FILE_PATH定义了,则用SYSTEM_FILE_PATH。最后才考虑使用“/etc”。

(2)odbc_configmain函数:

int main( int argc, char **argv )
{
    ......
    else if ( strcmp( argv[ i ], "--odbcini" ) == 0 )
    {
        printf( "%s/odbc.ini\n", SYSTEM_FILE_PATH );
    }
    else if ( strcmp( argv[ i ], "--odbcinstini" ) == 0 )
    {
        printf( "%s/odbcinst.ini\n", SYSTEM_FILE_PATH );
    }
    ......
}

可以看到,在使用odbc_config程序获得odbc.iniodbcinst.ini文件位置时,也要用的SYSTEM_FILE_PATH

unixODBC的代码库

unixODBC的代码目前还托管在sourceforge上,它的项目主页是:http://sourceforge.net/projects/unixodbc/?source=directory。感兴趣的朋友也可以checkout项目的svn代码:

svn checkout svn://svn.code.sf.net/p/unixodbc/code/trunk unixodbc-code

trunk上的代码可以看到,Nick还是会不时更新的。

此外,也可以访问unixODBCftp站点:ftp://ftp.unixodbc.org/pub/unixODBC/。这里包含各个版本的代码包,可以按需下载。

我为unixODBC提过的bug

今天在使用unixODBC 2.3.2时,无意中又发现了一个bug。就是在调用configure程序生成config.h文件时,关于软件包版本字符串是:

/* Define to the full name and version of this package. */
#define PACKAGE_STRING "unixODBC 2.3.2-pre"

/* Define to the version of this package. */
#define PACKAGE_VERSION "2.3.2-pre"

而实际这个已经是正式的release版本了,所以软件包版本字符串应该是:

/* Define to the full name and version of this package. */
#define PACKAGE_STRING "unixODBC 2.3.2"

/* Define to the version of this package. */
#define PACKAGE_VERSION "2.3.2"

Nick发了个邮件,他也承认是个bugA question about config.h )。屈指一算,这已经是我提给unixODBC的第四个bug了。记录一下,做个纪念:-):
Add missing unicode setting when returning a connection to the pool
Wrap lt_dlinit and dlerror in the lib mutex
Change mutex protection around release_env

unixODBC使用经验分享

unixODBC(http://www.unixodbc.org)和iODBC(http://www.iodbc.org)是UNIX系统上两个开源的ODBC实现。关于二者的比较,有兴趣的朋友可以参考stackoverflow上的这篇帖子。我没有用过iODBC,所以不好评论孰优孰劣。但是从stackoverflow上关于两个项目的标签统计来看,应该是unixODBC用的更广泛一些。在距离上一版本发布两年以后,今年10月,unixODBC发布了2.3.2版本,基本上就是修正了一些bugs(其中有3个bug是我发现并提出修改意见的),没有新的feature引入。我从2011年开始在项目中使用unixODBC,2012年中旬项目正式上线,到现在已经运行了1年半时间。总的来说unixODBC还是很稳定的,基本连个错误都没有。在这篇文章中,我把我使用unixODBC的一点经验分享出来,希望能给需要的朋友一些帮助。

(1)源码,编译和安装
在unixODBC的网站上提供了最新版本的源码下载,如果想下载之前的发布版本,可以访问这个网站:ftp://ftp.unixodbc.org/pub/unixODBC/。个人感觉,unixODBC的代码风格还是很清晰的,前后也比较一致(unixODBC里面也包含了一些其它开源的代码,像libltdl,而这些开源代码的风格和unixODBC的风格是不一致的)。只是有些函数有些太长了(像SQLConnect函数就有600多行),似乎不符合KISS(Keep It Small and Simple)原则。
编译unixODBC很简单:

./configure;
make;
make install

我的很多同事都说在Solaris环境下编译源代码是很痛苦的事情,但是编译unixODBC却总是一气呵成,连个错误都没有。
此外,由于默认编译unixODBC是优化的,所以我在实验室里使用时一般都是禁止优化的(./configure CFLAGS=’-g -O0 ‘),这样debug时也比较方便,不会出现代码和函数对不上的问题。只有到了生产环境,才把优化打开。

(2)使用连接池
从2.0.0版本起,uinixODBC引入了连接池。unixODBC的项目负责人Nick Gorham在这篇文章里详细介绍了连接池。引入连接池,可以使对数据库连接的使用更高效。下面我就对连接池的原理做个分析:
首先,uinixODBC引入两个全局变量:

/*
 * connection pooling stuff
 */

CPOOL *pool_head = NULL;
int pooling_enabled = 0;

其中pool_head指向连接池链表的首节点,而pooling_enabled则表示是否使用连接池。
其次,在SQLDisconnect函数中,可以看到:

/*
 * is it a pooled connection, or can it go back 
 */

if ( connection -> pooled_connection )
{
    __clean_stmt_from_dbc( connection );
    __clean_desc_from_dbc( connection );

    return_to_pool( connection );

    ......

    return function_return( SQL_HANDLE_DBC, connection, SQL_SUCCESS );
}
else if ( pooling_enabled && connection -> pooling_timeout > 0 ) 
{
    __clean_stmt_from_dbc( connection );
    __clean_desc_from_dbc( connection );

    return_to_pool( connection );

    ......

    return function_return( SQL_HANDLE_DBC, connection, SQL_SUCCESS );
}

如果使用了连接池的话,当断开连接时,连接是回到了连接池里,也就是return_to_pool这个函数。
最后,在SQLConnect函数中,可以看到:

if ( pooling_enabled && search_for_pool( connection, 
                                            server_name, name_length1,
                                            user_name, name_length2,
                                            authentication, name_length3,
                                            NULL, 0 ))
{
    ret_from_connect = SQL_SUCCESS;

    .....

    connection -> state = STATE_C4;

    return function_return( SQL_HANDLE_DBC, connection, ret_from_connect );
}

如果连接数据库时可以在连接池中找到可用的连接(search_for_pool),函数就直接返回,不用再做连接的操作了。

(3)在日志中打印线程
unixODBC的日志输出是通过dm_log_write函数:

if ( !log_info.program_name )
{
    uo_fprintf( fp, "[ODBC][%s]%s[%s][%d]%s\n", __get_pid((SQLCHAR*) tmp ), 
            tstamp_str,
            function_name, line, message );
}
else
{
    uo_fprintf( fp, "[%s][%s]%s[%s][%d]%s\n", log_info.program_name,
        __get_pid((SQLCHAR*) tmp ), 
        tstamp_str,
        function_name, line, message );
}

可以看到,日志输出包含进程名,时间戳,函数名,行号,和消息。但是现在的程序基本是多线程的,所以当有多个线程同时访问数据库时,从日志就很难区分开哪个线程到底做了什么,因此通常我会在日志中增加对线程的打印。因为我们的程序运行在Solaris系统,而在Solaris上,pthread_t类型是个整数,标示当前的线程号,所以下面的改动就可以增加对线程号的输出:

if ( !log_info.program_name )
{
    uo_fprintf( fp, "[ODBC][%s][%d]%s[%s][%d]%s\n", __get_pid((SQLCHAR*) tmp ), pthread_self(),
            tstamp_str,
            function_name, line, message );
}
else
{
    uo_fprintf( fp, "[%s][%s][%d]%s[%s][%d]%s\n", log_info.program_name,
        __get_pid((SQLCHAR*) tmp ), 
        pthread_self(),
        tstamp_str,
        function_name, line, message );
}

由于pthread_t类型在不同的系统定义不一样,所以如果同一份unixODBC代码需要运行在不同系统上,就需要实现定制化:

#ifdef __linux__
......
#endif

#ifdef __sun
......
#endif

关于pthread_t的打印,可以参考stackoverflow上的这篇帖子
以上就是我使用unixODBC的一点经验分享,希望能给需要的朋友一点帮助。如果大家有其它好的经验,也希望能分享出来。