分库分表
分库分表的原因
场景:由于业务关系,现在注册用户就 20 万,每天活跃用户就 1 万,每天单表数据量就 1000,然后高峰期每秒钟并发请求最多就 10 个。但是业务扩大后,注册用户数达到了 2000 万!每天活跃用户数 100 万!每天单表数据量 10 万条。
这个时候开始就需要分库分表
分库分表的原则
- 能不分就不分,任何分库分表都会在某种程度上提升业务逻辑的复杂度
- 数据量太大,正常的运维影响正常的业务访问
- 某些数据出现了无穷增长的情况
- 安全性与可用性的考虑
- 业务耦合性考虑
分表
单表几千万的数据,会极大印象sql的执行性能,到了后面可能sql就跑的非常慢。一般来说,单表到几百万的时候,性能就会相对差了一些,这时候就开始分表了。
分表即,把一个表的数据放到多个表中,然后查询的时候就查一个表。比如按照用户id来份表,将一个用户的数据就放到一个表中。然后操作的时候你对一个用户就操作那个表就好了,可以控制每个表的数据量在可控的范围内,比如每个表就固定在200W以内。
分库
一般一个库而言,最多支撑到并发2000,就一定要扩容了,而且一个健康的单库并发值最好保持在每秒1000左右。可以将一个库的数据拆分到多个库中,访问的时候方位一个库就好了。
总结
场景 | 分库分表前 | 分库分表后 |
---|---|---|
并发支撑情况 | MySQL 单机部署,扛不住高并发 | MySQL 从单机到多机,能承受的并发增加了多倍 |
磁盘使用情况 | MySQL 单机磁盘容量几乎撑满 | 拆分为多个库,数据库服务器磁盘使用率大大降低 |
SQL执行性能 | 单表数据量太大,SQL 越跑越慢 | 单表数据量减少,SQL 执行效率明显提升 |
分库分表的中间件?不同分库分表中间件优缺点?
-
Sharding-jdbc 优点:在于不用部署,运维成本低,不需要代理层的二次转发请求,性能高 缺点:如果遇到升级需要各个系统重新升级发版,各个系统都需要耦合Sharding-jdbc依赖
-
Mycat 优点:对于各个项目是透明的,如果遇到升级的直接中间件执行就可以 缺点:需要部署,运维成本比较高
如何对数据进行垂直拆分或水平拆分
- 水平拆分 把一个表的数据弄到多个表,但是每个库的表结构都一样,只是每个库表的数据不同,所有库表数据加起来的数据就是全部数据。水平拆分就是将数据均匀放到更多的库里,然后用多个库来扛更高的并发,还有用多个库的存储容量进行扩容。
- 水平分表 根据标中某些数据行,定义某种映射规则,将不同的数据行分布到不同的表中(表的基本结构不变)
- 水平分表分库 将水平分表拆分出来的分表,放到不同的库中
)
)
- 垂直拆分 就是把一个有很多字段的表拆分成多个表,或者是多个库。每个库的表结构不一样,每个库都包含部分字段。一般来说 ,会将较少访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另一个表里去,因为数据是有缓存的,访问频率高的字段越少,就可以在缓存里缓存更多的行,性能就越好。
- 垂直分表 将大表拆分成小表,将表中不常用的信息拆分出去,避免跨页查询
- 垂直分库 根据不同业务进行划分,每个业务有自己的独立库
)
比如:订单拆分成:订单表、订单支付表、订单商品表
无论是分库分表,上面说的中间件都是可以支持的。中间件可以根据制定的某个字段,比如userid自动路由到对应的库上去,然后再自动路由到对应的表。
不过还需要考虑项目如何分库?一般来说,垂直拆分可以在表层面做,对一些字段特别多的表做拆分;水平拆分,可以是并发承载不了,或者是数据量太大,容量承载不了;
有两种分库分表的方式: 分库分表:做到永不迁移数据和避免热点
- 一种是按照range来分,就是每个库一段连续的数据,比如按照时间范围来,但是一般比较少用,容易产生热点问题,大流量容易打在最新数据上。
range来分,好处在于,扩容比较简单,只需要预备好,每个月都准备一个库就行了;但是大部分情况都是访问最新的数据,容易造成流量打到最新的数据。
- 或者按照某个字段hash以下均匀分布,比较常用
hash好处在于,可以平均分配每个库的数据量和请求压力;坏处在于扩容比较麻烦,会有数据迁移的过程,之前的数据需要重新计算hash值并重新分配到不同的库。
分库分表实战
方案一:停机迁移
0点停机,没有流量写入,提前写一个导数的工具,将单表单库的数据全部读出来,写到分库分表里面。
导数完之后就ok了,修改系统的数据库连接配置,包括可能的代码,然后直接启动连接到新的分库分表中。
)
方案二:双写迁移
不用停机,简单来说,就是在线上系统,所有写库的地方,增加老库修改操作,除了对旧库的修改,都加上对新库的修改,这就是重点的双写,同时写两个库,新库和老库。
用提前写好的导数工具,跑读老库的数据写入到新库,写得时候要注意,可以读出来的数据是新库没有的,或者是新库里面新写的,拒绝用老数据覆盖新的数据。
导完之后,有可能数据还是存在,对比新老数据,如果有不一样的,针对不一样,从老库读,新库再次写入。循环直到两个库数据一致。
)
扩容方案
分表分库方案实现后,万一库又无法支撑,需要继续扩容
方案一:停机扩容(不推荐)
数据量大的情况下,光是导入2亿的数据,就要导入几个小时。
方案二:直接一开始多库多表
在设计的时候直接就32个库,每个库32张表,一共是1024张表
每个库正常写入量是1000,然后32 * 1000=32000,如果每个库写入量是1500,则就是32 * 1500=48000,将近5W的写入量,前面还有一个MQ,削峰写入MQ8W条,每条消费数据5W条
1024张表,假如每张放500W数据,在MySQL可以放入50亿的数据。
15W的并发写,10亿条数据。
根据某个id先根据32取模找库,然后再32取模找到库对应的表
订单号 | id%32(库) | id/32%32(库) |
---|---|---|
259 | 3 | 8 |
1189 | 5 | 5 |
352 | 0 | 11 |
4593 | 17 | 15 |
刚开始,这个库可能就是逻辑库,建在一个数据库上,就是一个MySQL服务器可能建立n个库
总结
- 设置好几台数据库服务器,每台服务器上几个库,每个库就多少表,推荐是32库*32表
- 路由规则,id模32=库,id/32模32=表
- 扩容的时候,申请增加更多的数据库服务器,装好MySQL,成倍数扩容,4台服务器就扩容到8台,8台就扩容到16台
- 由DBA负责将原来的数据库服务器的库,迁移到新的数据库服务器的库
- 修改配置,调整迁移库的数据库所在地址
- 发布系统,原先的路由规整没有了,可以基于n倍数据库服务器的资源,继续进行线上系统的提供服务
分库分表之后,id主键如何处理?
数据库自增id
系统每次得到一个id,都是往库里插入一个id,难道这个id再往对应的分库写入
优点是方便,谁都可以用;缺点就是单库生成的自增id,高并发的情况下,就会有瓶颈;改进的方式可以专门开一个服务,这个服务每次拿到当前id最大值,然后自己递增几个id,一次性返回一批id,然后再把当前最大id值修改成递增几个id之后的值,但是都是基于单个数据库。
适合场景:并发不高,数据量大太可以使用这个方案
设置数据库sequence(步长)或则和表自增字段步长
比如现在有8个服务节点,每个服务点使用一个sequence产生id,每个sequence其实id不同,并且依次递增,步长都是8
)
适合场景:用户防止产生的ID重复,这种方案实现比较简单,但是服务节点固定,步长固定,如果还需要增加服务节点,就比较麻烦
UUID
好处就是本地生产,不需要基于数据库;但是UUID太长,占用空间大,而且没有有序性,导致B+树索引写的时候有过多的操作,性能下降明显
适合场景:文件名或者随机编号之类可以用UUID
获取系统当前时间
这个就是获取当前的时间即可,但是如果并发很高,一秒并发几千,会有重复的可能,所以不适合。
适合场景:一般是把当前时间跟其他业务字段拼接起来作为一个id。
snowflake算法(雪花算法)
是twitter开源的分布式id生成算法,是64位的long型id。1 个 bit 是不用的,用其中的 41 bits 作为毫秒数,用 10 bits 作为工作机器 id,12 bits 作为序列号。
- 1 bit:不用,为啥呢?因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
- 41 bits:表示的是时间戳,单位是毫秒。41 bits 可以表示的数字多达 2^41 - 1 ,也就是可以标识 2^41 - 1 个毫秒值,换算成年就是表示 69 年的时间。
- 10 bits:记录工作机器 id,代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。但是 10 bits 里 5 个 bits 代表机房 id,5 个 bits 代表机器 id。意思就是最多代表 2^5 个机房(32 个机房),每个机房里可以代表 2^5 个机器(32 台机器)。
- 12 bits:这个是用来记录同一个毫秒内产生的不同 id,12 bits 可以代表的最大正整数是 2^12 - 1 = 4096 ,也就是说可以用这个 12 bits 代表的数字来区分同一个毫秒内的 4096 个不同的 id。
41个bit是当前毫秒单位的一个时间戳;5bit是机房的id(最大只能是32以内),另外5bit是机器id(最大只能是32以内),剩下12bit序号,如果跟上次生成id的时间在一个毫秒内,就会把顺序累加,最多在4096个序号内。
雪花算法存在的问题
- 时间回拨问题:由于机器的时间是动态的调整的,有可能会出现时间跑到之前几毫秒,如果这个时候获取到了这种时间,则会出现数据重复
解决方案:
- 方案一:保存过去一段时间内每一台机器在当前这一毫秒产生的ID的最大值,比如使用Map形式,就是<machine_id,max_id>,这样如果某台机器发生了时钟回拨,直接在这台机器对应的max_id的基础上继续自增生成ID即可。
- 方案二:发现时钟回拨增加指定步长,加上重试机制3次左右
总结
上面主要从:垂直和水平,两个方向介绍了我们的系统为什么要分库分表。
说实话垂直方向(即业务方向)更简单。
在水平方向(即数据方向)上,分库和分表的作用,其实是有区别的,不能混为一谈。
- 分库:是为了解决数据库连接资源不足问题,和磁盘IO的性能瓶颈问题。
- 分表:是为了解决单表数据量太大,sql语句查询数据时,即使走了索引也非常耗时问题。此外还可以解决消耗cpu资源问题。
- 分库分表:可以解决 数据库连接资源不足、磁盘IO的性能瓶颈、检索数据耗时 和 消耗cpu资源等问题。 如果在有些业务场景中,用户并发量很大,但是需要保存的数据量很少,这时可以只分库,不分表。
结论
如果在有些业务场景中,用户并发量不大,但是需要保存的数量很多,这时可以只分表,不分库。 如果在有些业务场景中,用户并发量大,并且需要保存的数量也很多时,可以分库分表。
Enjoy Reading This Article?
Here are some more articles you might like to read next: