前言
接触过后端开发的都知道MySQL是很脆弱的,随着业务的增长,当单表的数据量达到千万时,数据库的性能将大幅下降,即使通过添加从库、优化索引等方式也无法完全解决问题。在这种情况下,我们需要考虑对数据库进行切分。切分的主要目的是减轻数据库的负担,降低sql响应时间,从而提高系统的整体性能。
1.什么时候需要分库分表
在进行数据库表设计的时候,不是一上来就进行拆分的,我们要时刻谨记避免过度设计,只在万不得已的时候(性能瓶颈)进行分库分表。
- 表比较“大”,随着业务的迭代,当表列数较多的时候,我们可以进行一个垂直的拆分;随着业务的增长,当表行数较多的时候(行数过亿),我们可以进行一个水平的拆分。
- 资源不够,当数据量较多的时候,传统的单库数据存储上会有压力,当并发较高时,数据库连接池会被打满,这个时候就往往就要进行分库分表了。
2.垂直拆分
垂直拆分有垂直分库和垂直分表两种
2.1垂直分库
- 从业务上来说,可以根据业务的不同,将相关性高的表放在一个库里,类似于微服务的拆分;
- 从性能上说当我们一个业务库的数据库连接池不够用了,可以拆分成多个库,分散连接压力;
- 从数据可用性上来说,也可以拆分成多个库,这样即使某个数据库挂掉,不会导致整个服务不可用;
2.2垂直分表
垂直分表一般多见于单表字段数过多的情形下,当表字段较多时,聚簇索引存放的数据量就比较大,导致单表在存储数据时B+树的高度较高,导致性能急剧下降。
大多数情况下,一行纪录只是某几个字段修改比较频繁,垂直分表可以进行数据的冷热分离。
3.水平拆分
当经过前面的垂直拆分后,数据量仍巨大,查询修改增删性能无法满足业务需求的时候,我们就得进行水平拆分了。
3.1范围切分
我们可以对拆分后的表主键进行范围拆分,这种拆分处理起来比较简单,并且单表大小可控,水平扩展非常方便,同时还支持范围查找,但会造成数据热点问题,当某些时刻流量突增,请求都会打到某一张表上,造成数据倾斜,影响系统稳定性。
3.2唯一id
使用单表的时候,自增主键是一个非常不错的选择,但当进行水平拆分后,自增主键无法满足唯一性,同时为了避免数据大量插入时页分裂导致的性能问题,我们的主键最好是递增的(UUID不可以),那我们该如何生成这个唯一id呢?
- 雪花算法:雪花ID算法是个不错的选择,它是递增且唯一的,并且生成速度很快。
- 自定义生成算法:在一些复杂场景中,简单的雪花算法会无法满足业务需求,可以根据业务规则自定义生成算法。
- 基因法:在就拿商品订单来说的话,根据订单表主键可以定位到对应数据库表,但根据用户id查询订单的时候,就无法直接定位了,这个时候可能就需要再根据用户id切分出另一张同步表,造成成本的增加,有一种巧妙的做法是,订单id的生成规则中加入用户id,这样根据订单id或者用户id都可以直接路由到对应的数据库表。
3.3如何切分
- ID取模:直接对id%分表数,简单粗暴,但存在不均衡问题,并且后期扩容的时候,需要迁移大量数据。
- hash取模:对ID先进行哈希,再进行取模,可以解决均衡问题,但扩容仍然需要迁移大量数据。
- 一致性hash:一致性哈希算法可以做到分布均匀,并且扩容的时候只需要迁移少量数据,是一种比较常用的切分方法。
4.分库分表带来的问题
4.1查询问题
前面提到的基因法虽然能解决一些关键的查询问题,但是当遇到复杂查询、join、分页或是不带分片键的查询时就束手无策了。
- 字段冗余:冗余一些重要字段,可以避免join查询操作;
- 大表:当对查询性能要求不高时,可以同步一张完整的大表,这张表包含所有的数据;
- 数仓/ES:当查询复杂的时候,前面这些方法都无法满足,可以将数据通过binlog同步到大数据数仓和ES,当然这种情况下对数据会有一定的延迟。
4.2事务问题
当进行分库后,一个事务操作多个数据库的时候,典型的分布式事务问题,如果处理不好的话就会造成数据不一致。
当对数据一致性要求很高的时候,我们可以采用分布式事务框架,2PC、3PC等处理方式,当然这种情况下势必会造成性能的损失。
当业务对性能要求较高时,可以采用最终一致性的解决方案,对比事务回滚,补偿是一种比较轻量的处理方式,可以引入对账、人工告警等措施。