为何要分库分表
| # | 分库分表前 | 分库分表后 |
|---|---|---|
| 并发支撑情况 MySQL | 单机部署,扛不住高并发 | MySQL从单机到多机,能承受的并发增加了多倍 |
| 磁盘使用情况 MySQL | 单机磁盘容量几乎撑满 | 拆分为多个库,数据库服务器磁盘使用率大大降低 |
| SQL 执行性能 | 单表数据量太大,SQL 越跑越慢 | 单表数据量减少,SQL 执行效率明显提升 |
拆分方案
-
垂直拆分 垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表,或者是多个库上去。 每个库表的结构都不一样,每个库表都包含部分字段。 一般来说,会将较少的访问频率很高的字段放到一个表里去, 然后将较多的访问频率很低的字段放到另外一个表里去。 因为数据库是有缓存的,你访问频率高的行字段越少, 就可以在缓存里缓存更多的行,性能就越好。 这个一般在表层面做的较多一些。
-
水平拆分 把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样, 只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。 水平拆分的意义,就是将数据均匀放更多的库里, 然后用多个库来扛更高的并发,还有就是用多个库的存储容量来进行扩容。
分库分表中间件
这里划分为proxy和client两种来介绍。
- client
client 层方案的优点在于不用部署,
运维成本低,不需要代理层的二次转发请求,性能很高,
但是需要耦合中间件的代码,遇到中间件版本升级会比较麻烦,
常见的client层分库分表中间件有如下:
- sharding-jdbc:
- TDDL
- 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直接定位到库