OneProxy :: 支持分库分表下的结果集合并,让应用开发更轻松,代码更干净!

数据库分库分表功能可以提升系统的事务处理能力,让业务规模不再受制于技术,但也会给应用开发带来一些麻烦。 如果你有一个查询,需要涉及分库分表后的多个分片,比如“select count(*) from table where …”这样的语句,就必须要连接到所有的分片所在的库进行分别查询,得到每一个分片里符合条件的记录数,然后在应用程序里进行累计汇总。如果在应用代码层实现分库分表的逻辑,应用就必须自行处理跨库跨表的查询,如果没有通用的程序库来支持各分片的结果集合并,相当于对任何一个涉及多个分片的SQL都需要进行代码定制了。 事实上,很多系统是由多种不同的语言,比如PHP、Java、Python、Go等,研发的不同的应用程序构成的,统一的程序库的工作量不少。如果在中间件上支持跨分片结果集的合并,那就是一件非常美好的事情,平民软件的OneProxy刚好就是支持跨分片结果集合并功能的数据访问层。

oneproxy_result_sets_merge

当然,现在并不是所有的复杂SQL语句都支持,但每一天都在改进和进步。下面所述的跨分片操作都可以被很好地支持,稍后也会讲一下哪些复杂的SQL是不支持的。

  1. “select * from table where ……”,跨越多个分片的简单查询,无论分区键有无出现在Where条件中。
  2. “select * from table where … order by …”,跨分片结果集的排序。
  3. “select * from table where … order by … limit …”,跨分片查询的有序分页操作。
  4. “select distinct … from table where …”,取得唯一值列表,过滤掉相同的记录。
  5. “select … from table1 join table2 on …”,对于关联查询,本身不做语法限制,也不做语义检查。如果“table1”和“table2”以相同的规则来分片,并且每个分片位于同一个MySQL实例上, 象按订单号拆分的“orders”和“orders_detail”两个表,并且是按分表字段来做关联的话,可以直接查询。建议在数据库建模时考虑到这一点,对于有Join操作的相关表,使用相同的维度进行拆分。
  6. “select min(…),max(…), count(…), sum(…) from table where …”,支持分库分表下的汇总操作。
  7. “select colname, count(…), sum(…) from table where … group by colname”,支持分库分表下的分组汇总操作。

在默认配置下,从各个分片查询出来的临时结果会保存在OneProxy的内存中,当所有的分片都返回结果后,OneProxy会进行二次处理,比如排序、汇总、分页等等, 最后给客户端返回准确的结果。下面是当查询涉及多个分片的SQL时需要注意的一些地方(只涉及单个分片的无此限制):

  1. 临时结果集有大小限制,目前是100万行,以避免用完内存,可以通过“proxy-cache-rows”选项进行调整。
  2. 对某个字段统计维一值个数(“count(distinct …)”)的操作。
  3. 对字段求平均值(”avg(…)”)不被直接支持,请转换成计数(“count(…)”)和求和(“sum(…)”)两个操作,再作除法。
  4. 不支持分组汇总下的“having xxx”条件过滤。
  5. 不支持跨实例的表进行关联操作(”… join … ”)。
  6. 对多表关联的SQL语句不做语法和语义检查,需要自行保证这两个表按相同的维护拆分,并且按分区键去关联。

由于中间件内存大小有限,OneProxy也可以引入一个专门的MySQL节点来做中间库,使用“proxy-memory-db”选项来指定一个MySQL的连接停牌即可,这时各个分片的临时结果会被保存到中间数据库里,而不是在OneProxy的内存中,以避免内存用尽。使用中间数据库,也可以通过改写SQL语句来解决“count(distinct …)”、“avg(…)”、“having …”等操作的限制,但多表关联的限制依旧存在。

export ONEPROXY_HOME=/data/oneproxy

# valgrind --leak-check=full --show-reachable=yes \
${ONEPROXY_HOME}/oneproxy --keepalive --proxy-address=:3307 \
  --proxy-master-addresses=172.30.12.4:3306@trade1 \
  --proxy-slave-addresses=172.30.12.5:3306@trade1 \
  --proxy-master-addresses=172.30.12.6:3306@trade2 \
  --proxy-slave-addresses=172.30.12.7:3306@trade2 \
  --proxy-user-list=test/1378F6CC3A8E8A43CA388193FBED5405982FBBD3@test \
  --proxy-part-tables=${ONEPROXY_HOME}/part.txt \
  --proxy-memory-db=user/pass@host:port:dbname \
  --proxy-group-policy=trade1:master_only \
  --proxy-group-policy=trade2:master_only \
  --log-file=${ONEPROXY_HOME}/oneproxy.log \
  --pid-file=${ONEPROXY_HOME}/oneproxy.pid

要使用中间库的能力,目前还必须改写SQL才能支持。如下所示,子查询中的SQL语句会到各个分片去执行,然后用一个临时表名保存到中间数据库,再在中间数据库上进行二次查询,返回客户端查询的结果。

select /* local */ ... from 
   (select ... from ... where ...) as temp
   where ... having ...

最外层查询(必须带“local”注释)会在中间结果上运行,而子查询会在各个分片上运行,OneProxy会给客户端返回临时数据库上的查询结果,相当于是做了一次MapReduce操作。

  1. 由于子查询的结果会被保存到临时表里,因此子查询的每一个列,必须有一个合法的别名。
  2. 子查询会被替换成临时表的表名(OneProxy自动生成),替换后的SQL语句的查询结果会返回给客户端应用。

如果你有大量的涉及多分片的SQL语句,则需要你精心设计数据模型,涉及多个分片的SQL语句执行的成本会偏高(需要创建临时表,进行数据装载,最后删除临时表等多个步骤),需要尽量避免,或控制其调用量。