在《 modb 开发之需求和总体设计 》中,第三个要实现的功能点就是 “ 支持对 sql 语句的相关日志记录”。下面就讲解下设计这个功能的。
【需求分析】
终于到了处理 sql 日志的阶段了,万里长征重点的关键一步。 需要考虑解决的问题点如下:
- 在哪个模块上做 sql 日志记录
- 都要记录哪些信息才能做到跨机房数据同步时,具有可查询、可分析、可监控的目的
- sql 日志记录的模式或者说频率
- modb 应用中
- Atlas 应用中
[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 发送执行命令后记录)
日志记录的模式:
- 每条日志都执行打开文件,写日志,关闭文件的动作
- 仅在应用初始化时打开文件,在需要记录日志时写,在应用退出时关闭文件。通过 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"]
}
=== 未完待续 ===
来源:oschina
链接:https://my.oschina.net/u/617889/blog/189197