精华 一个处理二维表数据的库:tableman,并聊聊数据中间层
发布于 5 年前 作者 alsotang 4403 次浏览 最后一次编辑是 3 年前 来自 分享

这几天开发了一个库,叫 tablemanhttps://github.com/alsotang/tableman。名字取的比较挫。

这个库的作用是处理多个二维表之间的 join 或者 group 关系。

这里所说的二维表,是指这样的一种结构:

var table = [
  {id: 1, num: 2},
  {id: 2, num: 10},
  {id: 3, num: 20},
];

node-mysql 从数据库中取出的数据就长上面这个样。

我现在面对的业务,数据会存储在不同的数据源上,有 mysql,也有 hbase,还有一些阿里的内部存储服务。而且这些数据经常需要按特定键进行 join

这个场景之前的解决方案叫做 itier(虽然阿里管数据安全比较严格,但既然有开源并且与业务无关,我就提一下)。

https://github.com/xianbei/itier https://github.com/aleafs/itier-client

当然,上面两个 repo 很久没有更新了。

这个 itier 的解决方案如下:

untitled1.png

itier 以 sql 为 interface,它接受的输入类似这样:

select aaa.user_name, aaa.user_age, bbb.user_trade_money
from mysql_table aaa
join hbase bbb on aaa.user_id === bbb.uid
where aaa.age > 20 and bbb.gender = 0 and aaa.user_name = 'alsotang';

itier 作为一个数据中间层,代理所有的客户端发来的后端数据源的查询。在 itier 中,会有 msyql-driver、hbase-driver 等等适配层。这样适配层可以接受 sql 语句,并将相应的 sql 语句转换成对应数据库的查询,并以二维表的形式返回。

比如上面的 sql 会被拆成:

select user_name, user_age
from mysql_table
where aaa.age > 20 and aaa.user_name = 'alsotang'

select user_trade_money from hbase where bbb.gender = 0 and bbb.uid in (:aaa___user_id)

类似的形式。

先查询 mysql,得到 user_id 之后,去查询 hbase 获得更多的详细数据。

itier 在拿到不同数据库传来的二维表后,通过解析 sql 语句中的 group, where 或者 join 关系。将多张二维表合成一张,并返回给客户端。

在使用中,踩的坑如下:

一、 灵活性

sql 过于灵活。sql 如果是被逻辑完全的实现,各种组合使用会非常灵活。

在解析 sql 的时候,如果 group by 和 join 的条件稍微复杂一点,甚至 select 的条件稍微变化,就可能出现数据中间层解析不到的边角情况。

我们必须限定用户使用一个 sql 的子集,用户需要去记忆规则。为了不踩坑,我已经开始慢慢在使用那些我实践过的子集,而不是依照 sql 的规则来写语句。

这种感觉就像是在套模板,我脑中记下了多表连接查询时可以使用的模板、记下了可以使用的 select 场景的模板。这时我还不如抽出并固定下这些模板,以函数和参数的形式精准提供,不支持的特性就直接抛错。

而对于新手来说,由于对子集的生疏,很有可能写出数据中间层不支持的语句来。这时 sql 的便利性并不比它带来的混乱更多。

二、 缺少数据源的元信息

如下面两句 sql

select mysql.user_name, sum(hbase.login_time)
from mysql aaa
join hbase bbb on aaa.id = bbb.id
where mysql.user_name = 'alstoang' and bbb.valid = 1
select mysql.user_name, sum(mysql.login_time)
from mysql aaa
join hbase bbb on aaa.id = bbb.id
where mysql.user_name = 'alstoang' and bbb.valid = 1

两者所代表的处理过程就不相同,他们只是在 select 字段有所差别。

而 select 中的这个 sum,到底应该在数据全部取回之后再进行,还是交由数据库来进行,数据中间层很难判断。

如果最终要扫一万行记录,而只返回一条记录的话。在数据库支持 sum 的情况下,数据中间层只需要接收一条数据;如果像上面的情况,hbase 不支持 sum,那数据中间层需要取回这一万条,然后在自己的内存中做 sum。

那为了保险起见,每次都全量取回吗?

这个例子举得不是太精妙,当然,我们可以在 itier 中标注一下:hbase-driver 不支持 sql 特性,mysql-driver 支持 sql 特性。这样,当查询遇到 mysql 时,可以透传;遇到 hbase 时,只好全量取回。

这样还是不能解决的问题是:某些数据源(比如 G 数据源)声称支持 sql,但只支持 sql 的一部分特性。那这时,数据中间层还需要在 driver 的层面对 G 数据源的支持的子集专门又做个适配?或者由程序员想办法 workaround 地让数据中间层产生 G 数据源能够理解的 sql?

再比如三张表 join,按某字段排个序取 limit 100。到底在取这三张表数据的那个阶段去 limit 100?没有元数据难以判断。数据中间层现在的策略是适当多取一部分。

再比如三张表 join,先后顺序怎么判断?哪张是大表,哪张是小表?先扫后扫哪个会导致总扫描行数最少。程序员虽然知道,但数据中间层难以判断。

鉴于以上的考虑,我打算再遇到多数据源查询时,去掉数据中间层这个概念。将查询的步骤留给程序员,要求查询回来的结果都是二维表,并提供一个库让他们方便地处理取回的二维表数据。

这样虽然在代码量上,会比写 sql 语句多出不少字数。但算上踩坑和调试和尝试 workaround 的时间来说,是有收益的。

于是有了 tableman 这样一个库。

在开发之前,我很多时间都花在了设计上,就是希望 tableman 的接口既能满足通用的需求,又不会导致调用时写的字数太多。

在设计之初,我本来想采用链式调用 api 的,比如

tableman(someTable).join(otherTable).group(some_field).select('sum(money)')

后来我发现,当实现链式调用时,我又进了上述所说的【灵活性】的坑。于是改成传 options 的形式,让每个支持的特性都有对应的名字。不能去随意组合处理方式,而 tableman 要尽可能枚举出常用的形式来。

后来我想了下,为什么 request 和 superagent 之间,后者的链式调用这么优美。

原因是,superagent 并不需要处理负责的组合条件,每个 superagent 暴露出来的方法都只是让那个 superagent 实例的内部多存储一个条件。当 .end 被调用时,把这些条件分别塞进对应的 http 协议中的位置,发出查询即可。

而如果 tableman 要实现链式调用,感觉是要写个 sql 引擎的意思。

在这个场景中,less is really more。

6 回复

哈哈,我想起了node-lession里的“反正我是不想再碰mongodb了”,辛苦了,昨晚辣么晚提交的,先马,节后再看 自豪地采用 CNodeJS ionic

我也是自挖灵活坑,REDIS+MYSQL双绑。 opt={} opt.method=‘select’; opt.where=“1=1”; opt.table=‘account’; opt.data={}; sql(opt,cb); 以及更复杂的事件写法

最近也一直在考虑数据中间层这个东西,感觉这个东西做活了工程非常大,做简单了又觉得没功能,而且这玩意跟业务关联还比较大,如果要把一些共用的提取出来还不大好弄。有几个疑问: 1、你这样拆语句跨数据库查询还好,万一是写的操作,怎么保证一致性是一个问题,mysql插入成功,HBASE插入失败了 2、如果你的后面数据库是集群,用户查询条件没带分片片键,效率是没法保证 3、如果用sql子集,会不会随着后端的数据库越接越多,这个子集越来越小,最后只剩下CRUD

既然已经查询出来的数据都是类似 mongodb的BSON文档模式了,为什么不借鉴 mongodb 的查询方式,处理 你的 各类Table

mongodb 本来就是存储JSON数据的数据库,你们跨 数据库查询出来的数据,还是JSON 形式的对吧?那就干脆移植 并改进一下 mongodb的查询呗!

@DoubleSpout

我说说我的理解吧。

  1. 写的话,我们这边的中间层只提供了【透过】的特性,但还没做多数据源写的特性。多数据源写那应该是学术难题吧我觉得。。
  2. 没有分区键就直接报错
  3. 能实现 crud 都很好了啊。。。要知道,光实现 R 都很难了
回到顶部