【数据库】分库分表

本质

  当业务的增长导致数据库瓶颈的时候,一种解决瓶颈的手段。分库分表的核心是创建一个对业务透明的逻辑大表,隔离存储的负载度,进而实现在业务应用眼里的无限存储!

  分表

  分表解决的是:过大的数据表影响计算速度的问题,比如单表上亿,那么拆成十个表,必然会比一个表更快,用了一部分计算落表时间,来换取单表计算的时间。

  分库

  分库更多解决的是机器局限的问题,单机容量有限,单机容量实际上是由机器硬件决定,比如网卡打满、CPU打满、机器磁盘写满等case,必须将计算分布到其他机器上解决。先拆分进程,随后将进程拆分到不同的机器上

索引

  对于数据库来说,强行提升了储存,而索引就是需要付出的代价。

  当存储分布之后,单机的索引是无法满足对索引的要求,而SQL是对索引更上一层的封装,会有一定程度的限制,比如:分库分表最多能满足两个维度的拆分,Join表这种操作就变得异常困难。如何解决呢?方法就是,简单索引逻辑靠中间件构建虚拟索引,复杂索引靠其他方式构建外置索引。

  所有的分库分表的中间件的工作,无非是在数据源和业务应用之间封装一层虚拟的满足基本需要的索引。业务应用发送请求到中间件,中间件起到一部分索引的作用,判定需要到哪个库,哪个表来执行,这其实就是单机数据库,选择表过程的外置。中间件还有一个作用是,对涉及逻辑表的部分,进行一部分SQL逻辑改写,来最终判定到某些部分执行。

  对于更复杂的查询要求,应用方需要单独构建另外的索引,即把索引单独拆出来做成一个系统,来满足检索需要。作为代价来说,外置的索引会比内置的索引相对慢些,这是在系统架构上需要注意的地方。但是作为大原则,尽量把计算逻辑放置在索引外进行。

一致性

  分库分表还好的地方在于,数据最终存储是一份的;

  因为分库分表把需要操作的数据源分散了,操作的原子性在保障起来是有损耗了核心问题就在于,如何保障跨数据库、跨进程、跨机器的操作原子性。方法就是:外置保证一致性的方式

  通过柔性分布式事务的方式来实现最终一致性,方式就是外置检查,某一动作失败时,往前回滚,不一定完全回滚数据状态,回滚到不影响发生其它业务逻辑的业务状态。

实践

  一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值最好保持在每秒 1000 左右,不要太大。那么可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

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

  水平拆分

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

【数据库】分库分表

  2、另外一种水平拆分是表层面拆分,和上面不同的是一个库有多个同样结构的表,但是表的名字不一样,一般会按照命名排序,如table_1,table_2,.....这样,因为单表索引等问题,也是导致SQL慢的原因。 

  垂直拆分

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

【数据库】分库分表

  拓展方式

  还有两种分库分表的方式:  

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

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

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

平滑升级

  分表分库还有一个问题,一般都是演进过来的系统,都需要迁移,如何做到平滑升级,基本的方案都是数据库双写,综合一点可以利用binlog的方式先做同步,然后在流量最低的情况下,断10分钟服务,切换双写。

架构类型

  因为分表分库后,每一个库都是一个单独的服务器,具有自己的CUP和内存,所以是符合MMP架构。但一般性的会认为,数仓类型的才适合用MMP架构。

【数据库】分库分表

  但分库分表是属于OLTP类型的,用NUMA架构实际上会比较好,但这个不一定,看具体的软件方面的优化和实现方式,如TDDL本身就不支持JOIN,所以用MMP其实也是最简单的实现了。

主键

  分库分表后,一个问题就是如何生成自增id,推荐雪花算法(snowflake)

  snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 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。
0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000

常见中间件

  Cobar

  阿里 b2b 团队开发和开源的,属于 proxy 层方案,就是介于应用服务器和数据库服务器之间。应用程序通过 JDBC 驱动访问 Cobar 集群,Cobar 根据 SQL 和分库规则对 SQL 做分解,然后分发到 MySQL 集群不同的数据库实例上执行。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。

  TDDL

  淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。 Atlas 360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。

  Sharding-jdbc

  当当开源的,属于 client 层方案,是 ShardingSphere 的 client 层方案, ShardingSphere 还提供 proxy 层的方案 Sharding-Proxy。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且截至 2019.4,已经推出到了 4.0.0-RC1 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案。

  Mycat

  基于 Cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 Sharding jdbc 来说,年轻一些,经历的锤炼少一些。