在《
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"]
}
=== 未完待续 ===