在《
modb 開發之需求和整體設計
》中,第三個要實現的功能點就是
「 支持對 sql 語句的相關日誌記錄」。下面就講解下設計這個功能的。
【需求分析】
終於到了處理 sql 日誌的階段了,萬里長征重點的關鍵一步。
須要考慮解決的問題點以下:
- 在哪一個模塊上作 sql 日誌記錄
- 都要記錄哪些信息才能作到跨機房數據同步時,具備可查詢、可分析、可監控的目的
- sql 日誌記錄的模式或者說頻率
針對 MoDB 要作跨機房數據的同步這個功能,那麼能夠對 sql 語句進行記錄的「地方」有:
其中 Atlas 目前已支持 sql 日誌的記錄,格式以下:
[11/25/2013 14:58:54] C:172.16.80.111 S:127.0.0.1 OK 0.155 "SET NAMES utf8"
其中所涵蓋的內容包括:時間戳、源和目的 ip 地址、查詢對應的應答狀態信息、查詢耗時,以及查詢語句自己。
Atlas 會對全部經由 Atlas 發往 MySQL 服務器的類型爲 COM_QUERY 的查詢按照上述形式進行記錄。
而 modb 中對 sql 的日誌記錄須要本身實現。
從整體設計上講,訪問 Atlas (訪問 MySQL 數據庫)的入口有兩處:一個是經過 modb 進行訪問;另外一個是各類業務應用程序直接訪問。
而只有經由 modb 訪問 Atlas 的數據庫查詢動做,纔是跨機房同步所須要處理的內容,同時 Atlas 自己記錄的 sql 查詢操做比較全面,很大一部分咱們實際上是不須要關心的。綜上所述,必須在 modb 上實現 sql 日誌的記錄。
至於須要記錄的日誌內容,應該包括但不限於下面幾點:
- 日誌記錄的時間戳
- 日誌的「流向」(從哪裏來,到哪裏去)
- sql 語句自己
- sql 語句的執行狀況(分紅:直接在 MySQL 上執行成功後在 modb 上記錄;經過 modb 向 MySQL 發送執行命令後記錄)
承載 sql 語句的載體:
以 JSON 數據結構保存相關信息,最終做爲 rabbitmq 的消息發送接收。
日誌記錄的模式:
- 每條日誌都執行打開文件,寫日誌,關閉文件的動做
- 僅在應用初始化時打開文件,在須要記錄日誌時寫,在應用退出時關閉文件。經過 fflush 控制刷盤頻率
【JSON 庫選擇】
下面,能夠談談 JSON 解析的問題了。
JSON 格式自己不復雜,經過官網上的描述至多 10 分鐘就能夠基本瞭解清楚。一個值得思考的問題是,是否須要支持相似於 SAX(Simple API for XML)的流式解析方式。對於 modb 應用來說,是不須要支持的。另一個問題是,JSON 官網上提供的了那麼多開源的庫,選擇什麼樣的纔是適合個人?這個就須要親身實踐了。因此我實踐了以下幾個開源庫:
====
-- rui_maciel/mjson --
該庫能夠很方便的集成到其餘項目中,支持跨平臺;
該庫支持 SAX-like 解析;支持從文本文件中按行獲取數據進行解析;
支持 UTF-8;
支持 pretty 格式和 raw 格式的 json 數據相互轉換;
一句話總結:
針對 json 數據中特定節點數據的搜索功能基本不可用(這個比較噁心)
-- william/libjson --
一句話總結:
庫自己支持的功能絕對有亮點,但因爲原做者對 C99 標準貫徹的很是堅定,因此將上述代碼移植到不支持 C99 標準的 VS 上有必定困難。
--vincenthz/libjson --
可中斷的解析器:按字節處理 或者 按 string 塊處理。
沒有對象模型的限定:可經過簡單回調方式方便地集成到任何模型中。
代碼量很小。
速度快。
JSON全特定支持。
無本地語言轉換:字符編碼處理由用戶進行。
支持對json數據解析深度的進行控制。
支持對待處理數據大小的限制。
(可選)支持YAML/python註釋和C註釋。
一句話總結:
沒有搜索接口,故意把字符串內容留給用戶本身處理。
-- json-parser --
一句話總結:
沒有搜索接口,沒有 UTF-8 處理。
-- Jansson --
提供簡單直觀的 API 以及數據模型
全面的文檔
無第三方庫依賴
對 Unicode 的徹底支持(UTF-8 等)
完整的測試集
以 MIT 許可證發佈
一句話總結:
跨平臺支持良好,提供了完整的測試集,各類搜索方式都支持,總之,該有的都有了,不錯。
====
選定了使用 jansson 庫,接下來就該定義待處理的 json 數據結構了。本來我覺得這個應該很容易定,其實仍是有點搞頭的,請看下面:
【JSON 數據結構定義】
可供選擇的數據結構以下:
1. sql 的 value 以 string 的形式包含單條待執行語句。
這種形式的的問題是任何 sql 動做都對應產生一條 rabbitmq 消息,因此總的消息量會增長,好處是不須要 modb 去作複雜業務處理,即不用考慮當前 sql 是做用於哪一個庫,由於切換庫的動做也會以 sql 的形式經過 json 數據結構以 rabbitmq 消息進行發送。
缺點:rabbitmq 消息量變大;業務側須要將 sql 逐條發送;
優勢:modb 邏輯處理簡單(如日誌記錄等)。
形式一:
{
"src" : "172.16.80.111",
"key" : "172.16.80.123",
"app" : "Movision",
"state" : "transfer",
"sql" : "set names utf8"
}
形式二:針對這種形式須要在鏈接時設置好 CLIENT_MULTI_STATEMENTS ,而且須要客戶端實現多結果集處理。
{
"src" : "172.16.80.111",
"key" : "172.16.80.123",
"app" : "Movision",
"state" : "transfer",
"sql" : "set names utf8;show databases"
}
2. sql 的 value 以 array 的形式包含多條待執行語句。
這種形式其實和上面形式大致相同(尤爲和形式二)。
缺點:同上
優勢:同上
形式:
{
"src" : "172.16.80.111",
"key" : "172.16.80.123",
"app" : "Movision",
"state" : "notify",
"sql" : [
"set names utf8",
"show databases",
"use mysql"
]
}
3. sql 的 value 以 object 的形式包含多條待執行語句。
這種形式爲上層業務提供了靈活的操做方式,即容許在一條 rabbitmq 消息中同時對多個數據庫中的數據進行操做。缺點是增長了 modb 的邏輯處理複雜度(須要作額外的字符集設置、數據庫切換等動做,而且日誌記錄也更復雜)。另外也對 json 解析庫提供了更好的要求(好比相同的 key 與不一樣的 value 的映射)。
缺點:讓 modb 須要處理各類複雜的狀況。
優勢:爲上層業務提供了靈活性。
形式一:
{
"src" : "172.16.80.111",
"key" : "172.16.80.123",
"app" : "Movison",
"state" : "notify",
"sql" : {
"default" : "show databases",
"default" : "use test",
"test" : "show tables",
}
}
形式二:
{
"src" : "172.16.80.111",
"key" : "172.16.80.123",
"app" : "moooofly",
"state" : "notify",
"sql" : [
{
"dbname" : "",
"sqlstr" : "show databases"
},
{
"dbname" : "",
"sqlstr" : "use test"
},
{
"dbname" : "test",
"sqlstr" : "show tables"
},
]
}
綜上,考慮到 modb 須要同步 sql 語句是比較單一的數據 insert、update 和 delete ,應該不會有多數據庫同時操做的必要。因此,只須要支持「sql 的 value 以 string 的形式包含單條待執行語句」這類就能夠了。
【json 消息中字段的含義】
- src 字段表示當前消息的來源地址;
- key 字段表示 routing_key 和 binding_key ,根據具體業務場景進行區別對待;
- app 字段表示當前消息來源於何種應用;
- state 字段用於標識消息該如何被處理,該字段具備兩種值:"transfer" 和 "notify" 。業務模塊老是使用 "transfer" 狀態告之 modb 進行跨機房同步,但收到 rabbitmq 消息時不需關心該值;
- sql 字段用於標識當前傳輸的 sql 語句。
【遇到的問題】
最初在 modb 上實現 MySQL 數據庫訪問時,僅支持簡單 sql 的處理,後續開發過程當中,有 java 業務開發人員說基於其使用的
sdk 作業務實現時,最經常使用的方式是使用 prepared statement ,而且其使用的 bind 參數的類型大多數狀況都
是自適應的,不指定具體類型。但 C api 中卻沒有相應的接口實現自適應功能,因此在 C api 中必須按照下面的方式進行設置。
memset(ps_params, 0, sizeof (ps_params));
/* - v0 -- INT */
ps_params[0].buffer_type= MYSQL_TYPE_LONG;
ps_params[0].buffer= (char *) &int_data[0];
ps_params[0].length= 0;
ps_params[0].is_null= 0;
/* - v_str_1 -- CHAR(32) */
ps_params[1].buffer_type= MYSQL_TYPE_STRING;
ps_params[1].buffer= (char *) str_data[0];
ps_params[1].buffer_length= WL4435_STRING_SIZE;
ps_params[1].length= &str_length;
ps_params[1].is_null= 0;
/* - v_dbl_1 -- DOUBLE */
ps_params[2].buffer_type= MYSQL_TYPE_DOUBLE;
ps_params[2].buffer= (char *) &dbl_data[0];
ps_params[2].length= 0;
ps_params[2].is_null= 0;
/* - v_dec_1 -- DECIMAL */
ps_params[3].buffer_type= MYSQL_TYPE_NEWDECIMAL;
ps_params[3].buffer= (char *) dec_data[0];
ps_params[3].buffer_length= WL4435_STRING_SIZE;
ps_params[3].length= 0;
ps_params[3].is_null= 0;
/* - v_dec_2 -- DECIMAL */
ps_params[8].buffer_type= MYSQL_TYPE_DECIMAL;
ps_params[8].buffer= (char *) dec_data[0];
ps_params[8].buffer_length= WL4435_STRING_SIZE;
ps_params[8].length= 0;
ps_params[8].is_null= 0;
這樣就存在了一個問題,當 java 客戶端經過本身的 sdk 採用 prepared statement 方式更新數據庫後,再將相應的 sql 語句和參數以 rabbitmq 消息的形式發送給 modb 後,以後 modb 再更新本地數據庫,此時沒法知道應該設置爲什麼種參數類型,只能根據值進行猜想。這就有可能致使錯誤發生。
一種可選的補救措施:
{
"src" : "172.16.80.111",
"key" : "pc_1",
"app" : "Ejabberd",
"state" : "transfer",
"sql" : "insert into users values(?,?,?)",
"sql-args" : [1, 2, "abc"]
}
=== 未完待續 ===