引言
關於Mysql,大家都已經非常熟悉了。但是這種C/S的數據服務不適合於一些輕量級的應用,很多情況下,我們希望能使用一個進程級的存儲引擎,供我們查詢一些關係數據,而非使用一套臃腫的服務。當然這時我們可以選擇SqlLite,Berkeley DB,Access等輕量級的存儲件,但是都有學習成本,我們能否採用我們已經非常熟悉的Mysql介面來實現呢?答案是肯定的,很多人可能不知道:Mysql其實可以將服務以library的形式編譯,供程序單獨使用,這樣我們的程序相當於內聯了一套Mysql服務。
編譯
我們以linux下的編譯為例介紹編譯Embedded Mysql的步驟。
Mysql 5.0版本的編譯
在Mysql 5.0及之前版本,都是採用autobuild工具編譯,config時的參數可以如下配置:
./configure --prefix=/home/work/mysql --with-embedded-server--with-mysqld-ldflags--with-mysqld-ldflags=-all-static--enable-thread-safe-client --enable-local-infile
--with-extra-charsets=complex--disable-shared
上面的參數中:
--prefix=/home/work/mysql 指定安裝位置,可以根據自己的需要設置到其他目錄。
--with-embedded-server是編譯embedded mysql的關鍵,這個參數默認是不開啟的,因此一般只會編譯出libmysqlclient.a(.so)這個客戶端API庫,而開啟此參數後,會多生成一個libmysqld.a(.so)的庫,這個庫同時集成了mysql服務與客戶端API。
--with-mysqld-ldflags=-all-static與--disable-shared兩個參數表示只編譯靜態庫,如果需要使用動態庫,也可以去掉這兩個參數。
-enable-thread-safe-client表示客戶端API應該保持線程安全(但貌似Mysql 5.0有bug,即使開啟此參數,有時也非線程安全)。
--with-extra-charsets=complex 表示支持大字符集。
configure完成之後:
$ make
$ make install
即可以完成編譯。
Mysql 5.5之後的版本編譯
mysql 5.5之後的版本都遷移到採用cmake進行編譯,因此機器需要先安裝cmake,關於cmake的安裝,在此不做介紹,可以參考:
安裝cmake之後,可以用下面的命令編譯
cmake . -DWITH_EMBEDDED_SERVER=1 -DCMAKE_INSTALL_PREFIX=/home/work/mysql -DCMAKE_BUILD_TYPE=Release
上面的參數中:
-DCMAKE_INSTALL_PREFIX指定安裝位置,與5.0中的--prefix對應。
-DDWITH_EMBEDDED_SERVER表示將編譯Embedded Mysql庫,與5.0中的--with-embedded-server對應。
-DCMAKE_BUILD_TYPE表示生成庫的方式,它有以下幾種選項:
1. RelWithDebInfo 帶調試信息,開啟編譯優化的庫,編譯參數為-O2 -g
2. Debug 帶調試信息,不做任何編譯優化的庫,編譯參數-g
3. Release 不帶調試信息,編譯優化的庫,編譯參數-O2
4. MinSizeRel 不帶調試信息,最小size的庫,編譯參數-Os
Mysql5.5之後的版本編出的庫都是線程安全的,所以不需要 類似-enable-thread-safe-client的參數
是否帶調試信息對生成出庫的大小影響很大,例如Debug庫編譯出來有70多兆,而Release編譯出來只有不到20MB,如果不需要調試,建議已Release編譯。
Embedded Mysql使用方法
我們編寫如下代碼(test.cpp)進行測試:
#include
#include
#include
#include
int main()
{
MYSQL* sql;
MYSQL_STMT* stmt;
MYSQL_BIND bind;
unsigned* indice;
int ret;
int result;
unsigned long length;
char query1[] = "create database if not exists mydb";
char query2[] = "use mydb";
char query3[] = "create table if not exists mytable (id int not null, col int not
null)";
char query4[] = "insert into mytable (id, col) values (1,100)";
char query5[] = "select col from mytable where id = ?";
char *server_options[] = { "","--defaults-file=my.cnf","--basedir=./"};
int num_elements = sizeof(server_options)/ sizeof(char *);
if(mysql_server_init(num_elements, server_options, NULL))
{
puts("failed to initialize server");
return 1;
}
sql = mysql_init(NULL);
if(!mysql_real_connect(sql, NULL , NULL , NULL , NULL, 3306, NULL, 0))
printf("failed to connect\n");
if(mysql_query(sql, query1))
printf("failed to create database:%s\n", mysql_error(sql));
ret = mysql_query(sql, query2);
if(ret)
printf("failed to use database:%s\n", mysql_error(sql));
ret = mysql_query(sql, query3);
if(ret)
printf("failed to create table:%s\n", mysql_error(sql));
ret = mysql_query(sql, query4);
if(ret)
printf("failed to insert record:%s\n", mysql_error(sql));
stmt = mysql_stmt_init(sql);
if(!stmt)
printf("failed to init stmt:%s\n", mysql_error(sql));
ret = mysql_stmt_prepare(stmt, query5, strlen(query5));
if(ret)
printf("failed to prepare:%s\n", mysql_stmt_error(stmt));
result = 1;
memset(&bind, 0 , sizeof(bind));
bind.buffer_type = MYSQL_TYPE_LONG;
bind.is_unsigned = (my_bool)0;
bind.is_null = 0;
bind.buffer = (char*)&result;
bind.buffer_length = 4;
bind.length = &length;
ret = mysql_stmt_bind_param(stmt, &bind);
if(ret)
printf("failed to bind param:%s\n", mysql_stmt_error(stmt));
ret = mysql_stmt_execute(stmt);
if(ret)
printf("failed to execute stmt:%s\n", mysql_stmt_error(stmt));
memset(&bind, 0 , sizeof(bind));
bind.buffer_type = MYSQL_TYPE_LONG;
bind.is_unsigned = (my_bool)0;
bind.is_null = 0;
bind.buffer = (char*)&result;
bind.buffer_length = 4;
bind.length = &length;
ret = mysql_stmt_bind_result(stmt, &bind);
if(ret)
printf("failed to bind result:%s\n", mysql_stmt_error(stmt));
ret = mysql_stmt_store_result(stmt);
if(ret)
printf("failed to store result:%s\n", mysql_stmt_error(stmt));
ret = mysql_stmt_fetch(stmt);
if(ret)
printf("failed to fetch result:%s\n", mysql_stmt_error(stmt));
mysql_stmt_close(stmt);
mysql_close(sql);
mysql_server_end();
return 0;
}
從代碼我們可以看出,使用embedded mysql的方法與使用一般的mysql API基本一致,但是需要在使用前後初始化庫與清理環境,分別調用mysql_server_init()與mysql_server_end()這兩個函數。其中,mysql_server_init中可以指定mysql服務的配置文件my.cnf以及數據生成的位置(也可以不在init中配置數據目錄,在my.cnf文件中配置數據目錄)。
相比正式的mysql服務,embedded mysql的my.cnf只支持[server]與[embedded]這兩節配置,我們可以在當前目錄下創建一個類似下面的my.cnf:
[server]
language=/home/work/mysql/share/english
datadir==./data
[embedded]
language==/home/work/mysql/share/english
注意:上面的配置文件中,datadir指定mysql數據保存的位置(注意:該目錄必須在程序啟動前已存在,如果不存在需要提前手工創建,否則會啟動失敗)。language這個選項相當重要,它指定的是服務端與客戶端各種錯誤信息的資源文件。我們可以在mysql安裝目錄的share目錄下找到多種語言的資源文件,一般採用share/english/errmsg.sys即可(既可以直接將language指向這個目錄,也可以將這個文件拷貝至當前目錄下,然後將language指向目錄設為./)。
配置好my.cnf之後,我們如下編譯:
$ g++ test.cpp -o a.out -I/home/work/mysql/include -L/home/work/mysql/lib -lmysqld
如果編譯了動態鏈接庫,生成的執行文件可能不能正常運行,需要在LD_LIBRARY_PATH中導出libmysqld.so的位置:
$ export LD_LIBRARY_PATH=/home/work/mysql/lib:$LD_LIBRARY_PATH
編譯生成a.out,執行即可查詢。
注意事項
embedded mysql目前只能使用inno-db引擎,它會在datadir指定的目錄下生成資料庫文件,每個database用一個目錄保存,我們也可以將這些資料庫目錄拷貝到真正的mysql服務的數據目錄中,使用mysql工具進行查詢(注意:如果這麼操作,mysql服務必須重啟)。
關於Mysql 5.5的bug
本節是我在使用embedded mysql過程中發現的一些bug,這些bug至少在Mysql 5.5.15版本中存在,我已向Mysql社區反映,在官方修復之前,建議在編譯之前修改下面一些文件:
bug1:會導致多線程下的內存泄漏
在libmysqld/lib_sql.cc源代碼的256行:
stmt->fields= mysql->fields;
之後添加:
free_root(&stmt->mem_root, MYF(0));
bug2:會導致多線程下的內存泄漏
在libmysqld/lib_sql.cc的356行(stmt->result= *data;語句之前)加:
if(stmt->result.alloc.free != (*data).alloc.free ||
stmt->result.alloc.pre_alloc != (*data).alloc.pre_alloc)
{
free_root(&stmt->result.alloc, MYF(0));
}
bug3:會導致某些查詢失敗
在libmysqld/lib_sql.cc的346行:
if (res)
{
NET *net= &stmt->mysql->net;
set_stmt_errmsg(stmt, net);
DBUG_RETURN(1);
}
之後,添加下面兩行代碼:
else if (stmt->mysql->status==MYSQL_STATUS_GET_RESULT)
stmt->mysql->status= MYSQL_STATUS_STATEMENT_GET_RESULT;
結論
使用Embedded Mysql方案可以儘可能少的改動我們的代碼,兼容Mysql服務,實現輕量級的關係資料庫存儲。
其他