【原创】modb 功能设计之“支持对sql语句的相关日志记录”

原创
2013/12/31 13:06
阅读数 321

      在《   modb 开发之需求和总体设计   》中,第三个要实现的功能点就是    支持对 sql 语句的相关日志记录”。下面就讲解下设计这个功能的。

【需求分析】

终于到了处理 sql 日志的阶段了,万里长征重点的关键一步。 需要考虑解决的问题点如下:
  • 在哪个模块上做 sql 日志记录
  • 都要记录哪些信息才能做到跨机房数据同步时,具有可查询、可分析、可监控的目的
  • sql 日志记录的模式或者说频率
针对 MoDB 要做跨机房数据的同步这个功能,那么可以对 sql 语句进行记录的“地方”有:
  • modb 应用中
  • Atlas 应用中
其中 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"]
}


=== 未完待续 ===


展开阅读全文
打赏
0
2 收藏
分享
加载中
更多评论
打赏
0 评论
2 收藏
0
分享
返回顶部
顶部