策略以及中间件

Posted by Clear Blog on December 12, 2018

为何要分库分表

# 分库分表前 分库分表后
并发支撑情况 MySQL 单机部署,扛不住高并发 MySQL从单机到多机,能承受的并发增加了多倍
磁盘使用情况 MySQL 单机磁盘容量几乎撑满 拆分为多个库,数据库服务器磁盘使用率大大降低
SQL 执行性能 单表数据量太大,SQL 越跑越慢 单表数据量减少,SQL 执行效率明显提升

拆分方案

  1. 垂直拆分 垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表,或者是多个库上去。 每个库表的结构都不一样,每个库表都包含部分字段。 一般来说,会将较少的访问频率很高的字段放到一个表里去, 然后将较多的访问频率很低的字段放到另外一个表里去。 因为数据库是有缓存的,你访问频率高的行字段越少, 就可以在缓存里缓存更多的行,性能就越好。 这个一般在表层面做的较多一些。

  2. 水平拆分 把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样, 只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。 水平拆分的意义,就是将数据均匀放更多的库里, 然后用多个库来扛更高的并发,还有就是用多个库的存储容量来进行扩容。

分库分表中间件

这里划分为proxy和client两种来介绍。

  1. client client 层方案的优点在于不用部署, 运维成本低,不需要代理层的二次转发请求,性能很高, 但是需要耦合中间件的代码,遇到中间件版本升级会比较麻烦, 常见的client层分库分表中间件有如下:
    • sharding-jdbc:
    • TDDL
  2. proxy proxy 层方案的缺点在于需要部署,自己运维一套中间件,运维成本高, 但是好处在于对于各个项目是透明的, 如果遇到升级之类的都是自己中间件那里搞就行了。 常见的proxy层分库分表中间件有如下:
    • mycat

分库分表的方式

  • 按照 range 来分,就是每个库一段连续的数据, 这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。
  • 按照某个字段hash一下均匀分散,这个较为常用。

range来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了, 到了一个新的月份的时候,自然而然,就会写新的库了; 缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。

hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力; 坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表。

分库分表,我想到的第一个问题是,用uid分库,uname上的怎么快速查询。 在网上搜罗了一圈,找到以下一些策略,mark一下。

ElasticSearch

海量数据你还想啥呢,直接搜索引擎吧。

索引表法

思路:

uid能直接定位到库,uname不能直接定位到库,如果通过uname能查询到uid,问题解决

解决方案:

建立一个索引表记录uname->uid的映射关系 用uname来访问时,先通过索引表查询到uid,再定位相应的库。 这里可以用缓存也可以直接存储db,看数据量。 因为索引表属性较少,可以容纳非常多数据,一般不需要分库

潜在不足:

多查询db或者cache,性能下降

uname生成uid

思路:

不进行远程查询,由uname直接得到uid

解决方案:

在用户注册时,设计函数uname生成uid,uid=f(uname),按uid分库插入数据 用uname来访问时,先通过函数计算出uid,即uid=f(uname)再来一遍,由uid路由到对应库

潜在不足:

该函数设计需要非常讲究技巧,有uid生成冲突风险

基因法(uname基因融入uid)

思路:

不能用uname生成uid,可以从uname抽取“基因”,融入uid中 假设分8库,采用uid%8路由,潜台词是,uid的最后3个bit决定这条数据落在哪个库上,这3个bit就是所谓的“基因”

解决方案:

在用户注册时,设计函数uname生成3bit基因,uname_gene=f(uname),如上图粉色部分 同时,生成61bit的全局唯一id,作为用户的标识,如上图绿色部分 接着把3bit的uname_gene也作为uid的一部分,如上图屎黄色部分 生成64bit的uid,由id和uname_gene拼装而成,并按照uid分库插入数据 用uname来访问时,先通过函数由uname再次复原3bit基因,uname_gene=f(uname),通过uname_gene%8直接定位到库