分库分表及分布式问题
当数据库中的数据量越来越多的时候,我们不得不考虑用户量,用户活跃度,相关数据集的大小会不会成为我们应用系统的瓶颈。而且数据库量大的时候,基本上单表的数据也会增大,行锁和表锁等锁机制会很大程度地限制查询速度。我们不得不考虑采用分库分表等一系列操作来为数据库进行优化。
分区
分区是很多数据库都有的一个特性,在MySQL中被分区的表又叫分区表。对用户来说,分区表是一个独立的逻辑表,但是底层却是由很多个物理子表来组成的。它的分区主要是两种形式:水平分区和垂直分区。
水平分区
水平分区是对根据表的行进行分区,相当于每个分组里面都存放不同物理列的分割数据。所有在表中定义的列在每个数据集中都能找到,所以表的特性依然得以保持。而水平分区一定要通过某个属性列来分割,比较常见的比如年份,日期等。
垂直分区
这种分区方式一般来说是通过对表的垂直划分来减少目标表的宽度,使某些特定的列被划分到特定的分区,每个分区都包含了其中的列所对应的行。比如不常用的text或者blob(二进制大对象)之类的字段,这样既可以保持数据的相关性又能提高读取速度。
为什么很少使用呢?
不过,我在查询相关资料的时候,发现MySQL的分区表很少被使用,很多公司更愿意直接进行分表而不是使用这种逻辑表。这也是因为它虽然帮我们实现了物理上的分表,又带给了只需要管理一个表的方便,但是与此同时它也有很多的缺点。
- 比如查询访问分区表的时候,它会打开并锁住所有的底层表,这个开销成本太高。
- 比如维护分区的成本很高,像新增或者删除分区可能很快。但是像重组分区类似alter这种操作,其原理和alter一样,创建一个临时分区再复制数据过去,成本太高。
- 比如分区表不支持query cache,在查询的时候会自动避开query cache。
- ... ...
关于分区表因为真实开发中使用的也少,不过多赘述。
分表
分表是将一个大表按照一定规则分解成多张具有独立存储空间的实体表,这些表可以分布在同一块磁盘上,也可以在不同的机器上。
垂直分表
垂直分表在日常中比较常见,就是“大表拆小表”。比如我们的的一个订单表可能设计了好几十个字段,如果这个时候存储数据下来吗,表的数据量就会特别达到性能瓶颈。所以我们一般把一些字段拿出来,然后新建一张“扩展表”。当然,这些字段最后是那些大字段并且是不经常使用的,不然就很容易产生两张表进行关联查询。基本的垂直分表大概思路如下图:
水平分表
也称为横向分表,这个比较容量理解。就是将表中不同的数据行按照一定规律分布到不同的数据表中(这些表保存在同一个数据库中),这样来降低单表数据量,优化查询性能。最常见的方式就是通过主键或者时间等字段进行Hash和取模后拆分。
扩展
这里,我要去查了相关资料,单表的数据到底达到多少需要分表呢?
好像最开始网上传的是单表数据量大于2000万行的时候,性能会明显下降。这是百度DBA测试MySQL性能时候发现的,然后结论就传开了。不过后来阿里《Java开发手册》提出了单表数据超过500万行或者单表容量超过2GB,就进行分表。(原话参考如下)
【推荐】单表行数超过 500万行或者单表容量超过 2GB,才推荐进行分库分表。 说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
不过呢,这个数值其实跟实际记录的条数关系不算特别大。500行其实也是一个折中的考虑,因为这个性能其实还是跟MySQL的配置以及机器的硬件有关。
一般来说,如果内存足够大的话,我们的InnoDB buffer size也可以设置的比较大,然后加载进内存。这样通过索引缓存和数据缓存是可以提高查询性能的,查询不会有什么问题。但是单表数据一旦实在太大的话,达到机器的性能瓶颈,那么内存无法存储其索引,只能导致进行IO操作的话,就会开始导致性能下降!
反正,最后基本上就是内存的限制了,要么加配置,要么就进行分库分表了。😁
分库
分库是将一个数据库中大量的表,按照业务划分多个模块放到多个数据库。例如订单有自己的数据库,商品有自己的数据库。因为当数据库中数据量变大的时候,并发程度高到一定程度就会达到数据库的I/O瓶颈。
数据库分库呢基本可以分为垂直分库和水平分库。
垂直分库
垂直分库算是比较常见了,基本上规模大一点的公司就会进行垂直分库。基本的思路就是像我们上面介绍那样,按照业务模块来划分出不同的数据库。然后该模块的数据库都会存放着自己相关的数据表。
通过对数据库层面的划分,我们可以像对系统那样对不同业务类型的数据进行"分级"管理,维护,监控,扩展。而且这也很大程度的解决了数据库连接瓶颈的问题,释放了硬件资源限制。
水平分库
水平分库呢主要是通过对一个关键字取模(分库策略),然后达到对数据访问进行路由。思想基本上和分表差不多,不过不同的是,这些拆分出来的表是保存在不同的数据库中。
分片
这里刚开始呢我一直把分片和水平分库的概念混淆, 查了网上相关资料解释的也不是很清楚。
不过在看了相关博客之后,也是自己总结了一下,如有错误,不吝指出。
所谓分片便是将我们一个完整的原本数据库数据打碎成碎片(也就是每一个分片包含数据库的一部分)。这一个碎片可以是多个表的数据也可以是多个数据库实例的数据。我们的一个数据库服务器可以处理多个分片,也可以只处理一个分片。我们的分片需要系统中通过服务器进行路由查询然后进行转发,将我们操作转发到包含该数据的分片或者分片集合上去执行。
这样看来,我们的分片确实和水平分库很是类似。但是我们的分片最大的不同点便是这里的分片可以是任意的,不局限与传统的水平和垂直。
需要解决的问题
虽然通过分库分表提升了查询性能,但是与此同时也带来了分布式相关的一系列问题。
跨库Join
首先如果在我们单库单表的情况下,表与表之间的join查询还是比较容易的,不用考虑太多的问题。但是在分库和分表之后,就会遇到跨库和跨表相关的联合查询。要知道这个时候数据库可是分布式在不同的实例或者主机上,要进行join等操作的话,是比较麻烦的。而且一般基于架构规范、性能、安全等方面来考虑,是禁止跨库join的。
解决思路
-
全局表
可以通过建立全局表,存放一些所有模块都有可能依赖到的一些字段。为了避免跨库join,这种表可以在每个库里面都放一张。但是需要注意的是,这个表一般只用来存放那些不怎么进行修改的字段,不然解决了跨库join,又会带来这个表数据一致性的问题了。
-
字段冗余
字段冗余是一种典型的反范式设计,就是为了来解决避免join查询。比如我们在订单表保存了卖家id同时,如果需要用到卖家姓名的话,可以将该字段也冗余。然后在需要卖家姓名的时候,就不需要再去查询卖家用户表相关存储的表。
这是一种空间换时间的实现。但是该方法也不是说非常适用,只能说用来保存一些不经常修改的问题,不然带来的也还是数据一致性的问题。
-
(... ...)
排序分页
分库分表之后,还有一个经常使用而且需要解决的问题就是排序分页。随着分库和分表之后,原本的单表排序也变成了跨库排序和跨表排序了。而且各个节点的数据可能是随机的,为了排序的准确性,必须把所有分片节点的前N页数据都排好序然后做合并,最后再进行整体的排序,非常的耗资源。
解决思路
-
只提供前N页的功能
一般来说,用户越往后翻页性能越差。因为上面也提到了,各个节点数据是随机性的,为了准确性,要第N页内容就必须把前N页内容都拿出来进行总的排序。所以越往后翻页,性能越差。那么直接只提供给用户前N页查询的能力。
-
提供下一页,禁止跳页
或者也可以只提供给用户下一页查询,禁止了跳页查询。我们可以每一次查询之后获取汇总排序后的最大值,用来当作每一个表下一次查询的起始值。
-
增大pageSize
如果提供给用户使用,而是后台需要批处理任务要求分批获取数据。那么我们可以适当考虑增加pageSize,虽然总体的获取数据的体量不会变,但是可以减少获取的次数,减少排序。
-
(... ...)
分布式ID
所谓分布式id便是我们的数据库进行分库分表之后都需要一个唯一ID来标识一条数据,这个全局唯一id便是分布式id。原本我们在我们单表的情况下,id可以通过数据库自增来获取。但是在分库分表之后,只是单纯的数据库自增id已经无法满足我们的业务需求了,这时候就需要通过办法来实现我们的全局唯一ID。
关于分布式id的解决方案,基本上可以分为四种。
- UUID
- 数据库自增
- 号段模式
- 类雪花算法(SnowFlake)实现
基本上各大公司的实现也是基于其中,比如美团的Leaf提供了号段模式和SnowFlake两种实现,分别对两种算法进行了优化,解决了一部分各自的问题;比如滴滴的TinyID也是基于号段模式,实现原理和美团基本类似,不过提供了客户端;比如百度的UidGenerator是基于SnowFlake算法的唯一ID生成器。
美团Leaf:https://github.com/Meituan-Dianping/Leaf
滴滴TinyID:https://github.com/didi/tinyid
百度UidGenerator:https://github.com/baidu/uid-generator