分库分表-为什么要分库分表

目标:

1、理解分库分表的意义

2、理解数据切分的不同方式,以及带来的问题与解决方案

1、为什么要分库分表

随着业务变得越来越复杂,用户越来越多,集中式的架构性能会出现巨大的问题,比如系统会越来越慢,而且时不时会宕机,所以必须要解决高性能和可用性的问题。

不仅仅是应用,作为所有业务数据的管理者,数据库也会遇到一样的问题,而且很 多时候应用系统出现问题,就是数据库变慢了或者数据库挂掉了导致的。

1.1 数据库瓶颈的出现

一般来说哦会出现哪些问题呢?比如在高并发的情况下连接数不够用;或者数据量太大,查询效率变得越来越低;或者是因为存储的问题,数据库所在的及其性能下降

这些问题归根到底都是收到了硬件的限制,cpu内存磁盘网络等等。在集中式架构里面,我们怎么解决硬件带来的性能瓶颈问题呢?加配置。换cpu,升级内存,扩展磁盘,升级带宽等等

这种优化方式有两个问题

  • 随着业务量变大容易遇到新的瓶颈
  • 收效投入比太低

那我们思考一下,把升级硬件放在一边,如果数据库的性能出现问题了,我们可以从哪些方面进行优化呢?有没有成本更低收效更好的方式?

1.2 数据库优化方案对比

第一招:重启——重启是释放资源最好的方法

1.2.1 SQL与索引

当SQL写的非常复杂,比如关联的表非常多,条件非常多,查询消耗的事件非常长,这样的一个SQL就要慢SQL。以为SQL语句是我们自己写的,可控性最高,所以第一步就是检查SQL。在很多情况下我们优化的目标就是为了用到索引。

1.2.2 表与存储引擎

如果sql语句没什么问题,那就接着检查我们查询的目标,也就是表结构的设计有没有问题。比如对于字段类型和长度的选择,或者表结构是不是需要拆分或者合并,不同的表应该选择什么存储引擎是不是需要分区等等。

1.2.3 架构优化

表结构也没问题了,就要上升到数据库服务的层面,从架构层面进行优化。

因为数据都是在磁盘上存储,如果加了索引还是很慢,干脆可以把数据在内存里面缓存起来,这个时候可以部署缓存中间件。

如果一台数据库服务器承受不了访问压力,可以部署集群做负载均衡。当然这些数据库节点用该有自动同步的机制。有了主从同步之后就可以主从复制实现读写分离。让写的服务都访问master节点,请的请求都访问slaver节点。

有了读写分离以后问题并没有完全解决:

1、只有一个master,写的压力没有得到分摊

2、所有的节点都存储相同的数据,在一个节点出现存储瓶颈的时候,磁盘不够用了,其他节点也一样会遇到这个问题。

所以这个时候无哦们要用到分布式环境中一个非常重要的手段:分片,每个节点都只是存储总体数据的一部分,那这个就是我们今天要讲的分库分表。分片以后为了提升可用性,可以对分片再做冗余。

1.2.4 数据库配置

如果通过架构层面没有解决问题,或者机器虽然配置很高但是性能没有发挥到极致,还可以优化数据库的配置,比如连接数,缓冲池大小等等

1.2.5 操作系统

当然,因为数据库时安装在操作系统上的,所以操作西永的配置也有优化空间。最后才是硬件的优化,在单机的数据库上当然可以做。

我们先对数据库性能优化有了一个全局的认识,这个是非常重要的。我们清楚了分库分表在什么位置,也知道并不是数据库一慢,就要去做分库分表,可以优化的地方多的是。

OK,那么问题来了,这么多的优化方案,到底什么时候才需要分库分表呢?

评判标准是什么?

  • 如果是数据量的话,一张表存储了多少记录的时候,才需要考虑分库分表?
  • 如果是数据增长速度的话,每天产生多少数据,才需要考虑分库分表?
  • 如果是应用访问情况的话,查询超过几秒中,有多少请求无法获取连接,才需要考虑分库分表?

先来看一下一个消费金融核心系统的架构演进过程。这个也是很多公司项目一般的演进过程。

1.3 架构演进与分库分表

消费金融这个名字可能听起来比较陌生,但是跟大 家的生活息息相关。它主要是分成线上和线下两部分。线下的就是跟一些门店合作,提 供贷款服务,所以大家买手机买电脑买摩托车可以分期。线上的就是一些电商的场景, 比如花呗、京东白条,都属于消费金融。

1.3.1 单应用单数据库

当时我们是直接采购了一套消费金融核心系统,这是一个典型的单体架构的应用。

单体架构应用的特点就是所有的代码都在一个工程里面,打成一个 war 包部署到 tomcat,最后运行在一个进程中。

有了这个系统以后,客户办理在门店办理贷款,也就是贷前贷中贷后的所有的流程 都可以通过这一套系统完成。这套消费金融的核心系统,用的是 Oracle 的数据库,初始 化以后有几百张表,比如客户信息表、账户表、商户表、产品表、放款表、还款表等等。

公司门店开到了全国几百个城市,基本上一个城市一个分公司。合作的商户有十几万家,门店数量二十几万家,客户数量到达了千万级别。

为了适应业务的发展,这一套核心系统不停地在修改,代码量越来越大,系统变得 越来越臃肿。

节假日做活动,订单暴增,结果系统宕机了几个小 时,导致全国的销售有几个小时都做不了单,干着急。

对于 IT 部来说,该做的事情全都做了,搭集群,负载均衡,加缓存,优化数据库,优化业务代码系统,但是都应对不了系统的访问压力。

所以这个时候系统拆分就势在必行了。也就是我们说的对计算进行分片。 另外一块其实也是管理上的问题,几百个人做一个项目肯定会有很多的冲突。我们把以前这一套采购的核心系统拆分出来很多的子系统,比如提单系统、商户管理系统、 信审系统、合同系统、代扣系统、催收系统,所有的系统都依旧共用一套 Oracle 数据库。

这个时候我们进入了多应用单数据库的阶段。

1.3.2 多应用单数据库

对代码进行了解耦,职责进行了拆分,我们的生产环境出现问题的时候,可以快速 地排查和解决。这是第一次改造,原来我们一个系统使用一个据库,现在多个子系统共 用一个 Oracle 的数据库。

但是这种多个子系统共用一个 DB 的架构,会出现一些问题,什么问题呢?

第一个就是所有的业务系统都共用一个 DB,无论是从性能还是存储的角度来说,都 是满足不了需求的。随着我们的业务继续膨胀,我们又会增加更多的系统来访问核心数 据库,但是一个物理数据库能够支撑的并发量是有限的,所有的业务系统之间还会产生 竞争,最终会导致应用的性能下降,甚至拖垮业务系统。

1.3.3 多应用独立数据库

所以这个时候,我们必须要对各个子系统的数据库也做一个拆分。这个时候每个业 务系统都有了自己的数据库,不同的业务系统就可以用不同的存储方案。

当然在一段时间之内核心数据库并没有下线,它依然是所有的 IT 系统交换数据的一 个中心,每个系统都会把自己的数据同步到核心系统,也是从核心系统同步其他系统的数据,后面我们也在逐步脱离这个核心系统。

所以,分库其实是我们在解决系统性能问题的过程中,对系统进行拆分的时候带来 的一个必然的结果。现在的微服务架构也是一样的,只拆应用不拆分数据库,不能解决根本的问题。

所以,对数据库进行分库,它一般是应用的服务拆分,模块的分解,或者子系统的 划分带来一个必然的结果。

1.3.4 什么时候分表?

当我们对原来一个数据库的表做了分库以后,其中一些表的数据在快速地膨胀,比 如客户表已经超过 1000 万了,合同表也超过 5000 万了,还款历史表已经上亿了,而且还在以一个非常快的速度在增长,这个时候查询也已经出现了非常明显的效率下降。 所以,在分库之后,还需要进一步进行分表(单应用多数据库)。当然,我们最开 始想到的可能是在一个数据库里面拆分数据,分区或者分表,到后面才是切分到多个数据库中。 所以,分表主要是为了减少单张表的大小,解决单表数据量带来的性能问题。

回到我们开始的问题,大家觉得一张表的存储的数量达到多少的时候,需 要做分表?

那么到底一张表数据量到达多少的时候,我们需要去做分库分表呢?200 万?300 万?500 万?

大家可能在网上看过一些数据库的规范,比如阿里巴巴 Java 开发手册,它推荐的理 论分表界线是 500 万。

【推荐】单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。 说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。

在阿里编程规约里面,它推荐的是超过 500 万才考虑分表。但是一段时间之内到不了这个数量级,不要先做分库分表。
从这句话里面我们读出来两个信息:

1、第一,如果可以不用分库分表,我们尽量不要分库分表。因为它会大大地提升系统的复杂度,带来很多新的问题。如果对于一个很简单的项目,一上线就分库分表,这个就叫过度设计了,也不符合项目迭代和演进的规律。

2、第二,分库分表是一个长期的规划,要解决的不只是现在的问题。它不需要经常 性地去实施,但是一旦做了,在几年之内是不需要变动的。

到底是不是 500 万呢?这个也不一定,还是要以业务的实际的情况为准。如果我们 创建的表结构合理,字段不是太多,并且索引创建正确的情况下,单张表存储几千万的 数据是完全没有问题的,这个还是以应用的实际情况为准。

如果是真的慢了,而且其他的方案都用过了,性能还是跟不上,这个时候才考虑分 库分表。

那如果要做分库分表,分库分表到底有哪些类型?

2. 分库分表的类型和特点

我们把前面的拆分方式归纳了一下,一共就两种,一种叫垂直拆分,一种叫水平拆分。

  • 垂直切分:基于表或字段划分,表结构不同。我们有单库的分表,也有多库的分库。

  • 水平切分:基于数据划分,表结构相同,数据不同,也有同库的水平切分和多库的 切分。

2.1 垂直切分

垂直分表有两种,一种单库的,一种多库的。字段太多就要拆表,表太多就要拆库

2.1.1 单库垂直分表

单库分表,比如:商户信息表,拆分成基本信息表,联系方式表,结算信息表,附 件表等等。

2.1.2 多库垂直分表

多库垂直分表就是把原来存储在一个库的不同的表,拆分到不同的数据库。

比如:消费金融核心系统数据库,有很多客户相关的表,这些客户相关的表,全部 单独存放到客户的数据库里面。合同,放款,风控相关的业务表也是一样的。

当我们对原来的一张表做了分库的处理,如果某些业务系统的数据还是有一个非常 快的增长速度,比如说还款数据库的还款历史表,数据量达到了几个亿,这个时候硬件 限制导致的性能问题还是会出现,所以从这个角度来说垂直切分并没有从根本上解决单 库单表数据量过大的问题。在这个时候,我们还需要对我们的数据做一个水平的切分。 这个时候,一个应用需要多个数据库。

2.2 水平切分

水平切分就是按照数据的维度分布不同的表中,可以是单库的,也可以是多库的。

2.2.1 单库水平分表

两个案例:

银行的交易流水表,所有进出的交易都需要登记这张表,因为绝大部分时候客户都 是查询当天的交易和一个月以内的交易数据,所以我们根据使用频率把这张表拆分成三 张表:

当天表 channel_transaction:只存储当天的数据。

当月表 channel_transaction_month:我们在夜间运行一个定时任务,前一天 的数据,全部迁移到当月表。用的是 insert into select,然后 delete。

历史表 channel_transaction_history:同样是通过定时任务,把登记时间超过 30 天的数据,迁移到 history 历史表(历史表的数据非常大,我们按照月度,每个 月建立分区)。

费用明细表:

消费金融公司跟线下商户合作,给客户办理了贷款以后,消费金融公司要给商户返 费用,或者叫提成,每天都会产生很多的费用的数据。为了方便管理,我们每个月建立 一张费用表,例如 fee_detail_202501……fee_detail_202512。

但是注意,跟分区一样,在一个数据库分表的方式虽然可以一定程度解决单表查询 性能的问题,但是并不能解决单机存储瓶颈的问题,因为所有的表占用的是相同的磁盘 存储空间。

2.2.2 多库水平分表

另一种是多库的水平分表。比如客户表,我们拆分到多个库存储,表结构是完全一 样的。

一般我们说的分库分表都是跨库的分表。

既然分库分表能够帮助我们解决性能的问题,那我们是不是马上动手去做,甚至在 项目设计的时候就先给它分几个库呢?先冷静一下,我们来看一下分库分表会带来哪些 问题,也就是我们前面说的分库分表之后带来的复杂性。

2.3 分库分表带来的问题

2.3.1 跨库关联查询

比如查询在合同信息的时候要关联客户数据,由于是合同数据和客户数据是在不同的数据库,那么我们肯定不能直接使用join这种方式去做关联查询

我们有几种主要的解决方案:

  1. 字段冗余

  2. 数据同步 : 比如商户系统要查询产品系统的产品表,我们干脆在商户系统创建一张产品表,通过 ETL、MQ 或者 canal 定时同步产品数据。

  3. 全局表:有一些基础信息表,比如行名行号表、行政区划表,被很多业务系统用到,如果我们放在核心系统,每个系统都要调接口去查询,这个时候我们可 以在所有的数据库都存储相同的基础数据,各个系统自己维护,保持同步。

    上面的思路都是通过合理的业务设计避免跨库关联查询,实际上在我们的系统中, 尽量不要用跨库关联查询。如果最后无法避免跨库关联的情况,那我们就只能用最后一 种办法。

  4. 系统业务层组装:在不同的数据库节点,各自利用查询条件,把符合条件数据的数据查询出来,然后在内存中重新组装,返回给客户端。

2.3.2 分布式事务

比如在一个贷款的流程里面,合同系统登记了数据,放款系统也必须生成放款记录, 如果两个操作不是同时成功或者同时失败,就会出现数据一致性的问题。

如果实在一个数据库里面,我们可以使用本第十五来控制,但是在不同的数据库里面就不行了。这里必须要出现一个协调者的角色,大家统一行动,而且要分成多个阶段。一般是是先确定都能成功才能保证成功,只要有一个不成功,就要全部失败。

核心思想其实是在预先提交能够恒公的情况下,尽量缩短同时提交的时间差,来提升成功的概率。

分布式事务,在微服务篇有文章详细介绍。

2.3.3 排序、翻页、函数计算问题

跨节点多苦进行查询时没会出现limit分页,order by排序的问题。比如有两个节点: 节点 1 存的是奇数 id=1,3,5,7,9……;节点 2 存的是偶数 id=2,4,6,8,10……

如果查询语句是查出第一页的 10 条数据:
select * from user_info order by id limit 0,10

需要在两个节点上各取出 10 条(为什么都要查 10 条?假设 10 条都在第 2 个节点上?),然后合并数据,重新排序,节点多的话就更麻烦了。

max、min、sum、count 之类的函数在进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回。

2.3.4 全局ID避重

MySQL 的数据库里面字段有一个自增的属性,Oracle 也有 Sequence 序列。如果 是一个数据库,那么可以保证 ID 是不重复的,但是水平分表以后,每个表都按照自己的 规律自增,不同的表之间肯定会出现 ID 重复的问题。

全局ID也有单独文章进行介绍。

当然还有一个非常关键的问题。 原来你的应用系统只需要连接到一个数据库,配置一个数据源,现在要配置多个。

配置就配置吧,问题就来了:
我们在执行一条 SQL 语句的时候,比如插入,它应该是在哪个数据节点上面执行呢?

又比如查询,1 条数据只在其中的一个节点上面,我怎么知道在哪个节点?如果是列表查询,数据分布在多个节点,是不是要在所有的数据库节点里面都查询一遍,才能拿到结 果?

这个问题我们把它叫做:多数据源的问题,或者动态数据源的问题。

我们可以从查询的整个流程来分析一下,哪些环节是我们可以下手的。

2.4 多数据源/动态数据源的解决方案

在 SSM 的项目里面,查询一般要经过这些流程: DAO——Mapper(ORM)——JDBC——代理——数据库服务

2.4.1 DAO层

第一个就是在我们的客户端的代码,比如 DAO 层,在我们连接到某一个数据源之前, 我们先根据配置的分片规则,判断需要连接到哪些节点,再建立连接。

Spring 中提供了一个抽象类 AbstractRoutingDataSource,可以实现数据源的动态切换。

详见代码

在 DAO 层实现的优势:不需要依赖 ORM 框架,即使替换了 ORM 框架也不受影响。实现简单(不需要解析 SQL 和路由规则),可以灵活地定制。

在 DAO 层实现的缺点:不能复用,不能跨语言。

2.4.2 ORM 框架层

第二个是在框架层,比如我们用 MyBatis 连接数据库,也可以指定数据源。我们可以基于 MyBatis 插件的拦截机制(拦截 query 和 update 方法),实现数据源的选择。

例如:

Mybatis 分片:可以插件通过设置 Statement 的 Connection,或者使用不同的 SqlSessionFactory 实现。

2.4.3 驱动层

不管是 MyBatis 还是 Hibernate,还是 Spring 的 JdbcTemplate,本质上都是对 JDBC 的封装,所以第三层就是驱动层。比如 Sharding-JDBC,就是对 JDBC 的对象进行了封 装。JDBC 里面有两个核心对象,一个是 Connection,是一个连接的封装,一个是 DataSource,是对一个数据库的封装。

我们可以自己实现一个 DataSource,在项目中配置多个数据源,这样就可以随心所 欲地切换 datasource 了(Sharding-JDBC 就是这样实现的)。

2.4.4 代理层

前面三种都是在客户端实现的。也就是说,如果你有 10 个项目,那就要对 10 个项 目进行改造,当然这种情况下我们会把逻辑抽取出来打成 jar 包,直接依赖使用。但如果不是 Java 的项目呢?那就麻烦了,同样的逻辑还是要实现一遍。

所以我们干脆把它做成一个服务,这样不同语言的项目都可以直接连接,使用这个逻辑了。它应该提供跟数据库一样的协议,减少客户端的变动,比如我原来用的 Spring、MyBatis、Druid,都不要让我去改,只要改数据库的 IP 和连接规则就行了。

到了这个时候,因为提供了一个代理服务,帮我们去查数据,再把数据返回,我们就把它叫代理层。Mycat 和 Sharding-Proxy,都是属于这一层。

2.4.5 数据库服务

最后一层就是在数据库服务上实现。以非关系型数据库 Redis 为例,Redis Cluster 分片,产品多个连接以后,能够自动路由。MySQL 可以吗?不可以,那我就去改一下 MySQL 的源码,让它变成一个分布式的数据库。在数据库服务前面加一个路由层,后面 支持多个数据源。

腾讯云现在主推的 TDSQL,就是这样一种实现。不过,这样的数据库在部署的时候, 多了很多的节点需要部署。