1.1 数据库拆分过程及挑战

2017-06-08 23:12:39 9,861 6

互联网当下的数据库拆分过程基本遵循的顺序是:垂直拆分、读写分离、分库分表(水平拆分)。每个拆分过程都能解决业务上的一些问题,但同时也面临了一些挑战。

1 垂直拆分

对于一个刚上线的互联网项目来说,由于前期活跃用户数量并不多,并发量也相对较小,所以此时企业一般都会选择将所有数据存放在一个数据库 中进行访问操作。

举例来说,对于一个电商系统,其用户模块和产品模块的表刚开始都是位于一个db_eshop库中。

Image.png

其中:user表和user_account表属于用户模块,product_category表和product表属于产品模块

刚开始,可能公司的技术团队规模比较小,因此整个技术团队共同维护db_eshop库。随着公司业务的发展,技术团队人员也得到了扩张,划分为不同的技术小组,不同的小组负责不同的业务模块。例如A小组负责用户模块,B小组负责产品模块。此时数据库也迎来了第一次拆分:垂直拆分。

这里的垂直拆分,指的是将一个包含了很多表的数据库,根据表的功能的不同,拆分为多个小的数据库,每个库包含部分表。下图演示将上面提到的db_eshop库,拆分为db_user库和db_product库。

Image.png

关于垂直拆分,还有另一种说法,将一个包含了很多字段的大表拆分为多个小表,每个表包含部分字段。而笔者认为,根据表功能的不同的对数据库进行拆分,这种情况更加常见。

2 读写分离

随着后续的市场推广力度不断加强,用户数量和并发量不断上升。这时如果仅靠一个数据库来支撑所有访问压力,几乎是在 自寻死路 。以产品库为例,可能库中包含了几万种商品,并且每天新增几十种,而产品库每天的访问了可能有几亿甚至几十亿次。数据库读的压力太大,单台mysql实例扛不住,此时大部分 Mysql DBA 就会将数据库设置成 读写分离状态 ,也就是一个 Master 节点(主库)对应多个 Salve 节点(从库)。可以将slave节点的数据理解为master节点数据的全量备份。

Image.png

master节点只有一个且可读可写,slave节点有多个且只可以读。新增产品时,应用将数据写入master主库,主库将数据同步给多个slave从库。当查询产品时,应用选择某个salve节点读取数据。

读写分离的优点:

    这样通过配置多个slave节点,可以有效的避免过大的访问量对单个库造成的压力。

读写分离的挑战:

1、对于DBA而言,需要配置数据库主从同步

     关于如何配置数据库的主从同步,这个目前方案已经很成熟。以mysql为例:

     可以参考官方文档:https://dev.mysql.com/doc/refman/5.7/en/replication.html,

     笔者也写了一篇文章介绍如何通过mysql_multi的方式配置主从同步:http://www.tianshouzhi.com/api/tutorials/mysql

2、对于开发人员而言,必须要对sql类型进行判断,如果是select等读请求,就走从库,如果是insert、update、delete等写请求,就走主库。此外还有一些其他的问题要考虑:

  • 主从数据同步延迟问题:因为数据是从master节点通过网络同步给多个slave节点,因此必然存在延迟。因此有可能出现我们在master节点中已经插入了数据,但是从slave节点却读取不到的问题。对于一些强一致性的业务场景,要求插入后必须能读取到,因此对于这种情况,我们需要提供一种方式,让读请求也可以走主库,而主库上的数据必然是最新的。

  • 事务问题:如果一个事务中同时包含了读请求(如select)和写请求(如insert),如果读请求走从库,写请求走主库,由于跨了多个库,那么jdbc本地事务已经无法控制,属于分布式事务的范畴。而分布式事务非常复杂且效率较低。因此对于读写分离,目前主流的做法是,事务中的所有sql统一都走主库,由于只涉及到一个库,jdbc本地事务就可以搞定。

  • 高可用的考虑:例如master配置了多个slave节点,如果其中某个slave节点挂了,那么之后的读请求,我们应用将其转发到正常工作的slave节点上。另外,如果新增了slave节点,应用也应该感知到,可以将读请求转发到新的slave节点上。

3 分库分表

        经过垂直分区后的 Master/Salve 模式完全可以承受住难以想象的高并发访问操作,但是否可以永远 高枕无忧 了?答案是否定的,一旦业务表中的数据量大了,从维护和性能角度来看,无论是任何的 CRUD 操作,对于数据库而言都是一件极其耗费资源的事情。即便设置了索引, 仍然无法掩盖因为数据量过大从而导致的数据库性能下降的事实 ,因此这个时候 Mysql DBA 或许就该对数据库进行 水平分区 (sharding,即分库分表 )。经过水平分区设置后的业务表,必然能够将原本一张表维护的海量数据分配给 N 个子表进行存储和维护。

水平分表从具体实现上又可以分为3种:只分表、只分库、分库分表,下图展示了这三种情况:

Image.png


只分表:

        将db库中的user表拆分为2个分表,user_0和user_1,这两个表还位于同一个库中。  适用场景:如果库中的多个表中只有某张表或者少数表数据量过大,那么只需要针对这些表进行拆分,其他表保持不变。

只分库:

        将db库拆分为db_0和db_1两个库,同时在db_0和db_1库中各自新建一个user表,db_0.user表和db_1.user表中各自只存原来的db.user表中的部分数据。

分库分表:

        将db库拆分为db_0和db_1两个库,db_0中包含user_0、user_1两个分表,db_1中包含user_2、user_3两个分表。下图演示了在分库分表的情况下,数据是如何拆分的:假设db库的user表中原来有4000W条数据,现在将db库拆分为2个分库db_0和db_1,user表拆分为user_0、user_1、user_2、user_3四个分表,每个分表存储1000W条数据。

Image.png

分库的好处:

    降低单台机器的负载压力

分表的好处:

    提高数据操作的效率。举个例子说明,比如user表中现在有4000w条数据,此时我们需要在这个表中增加(insert)一条新的数据,insert完毕后,数据库会针对这张表重新建立索引,4000w行数据建立索引的系统开销还是不容忽视的。但是反过来,假如我们将这个表分成4 个table呢,从user_0一直到user_3,4000w行数据平均下来,每个子表里边就只有1000W行数据,这时候我们向一张 只有1000W行数据的table中insert数据后建立索引的时间就会下降,从而提高DB的运行时效率,提高了DB的并发量。当然分表的好处还不知这些,还有诸如写操作的锁操作等,都会带来很多显然的好处。


分库分表的挑战主要体现在4个方面:基本的数据库增删改功能,分布式id,分布式事务,动态扩容,下面逐一进行讲述。 

挑战1:基本的数据库增删改功能   

对于开发人员而言,虽然分库分表的,但是其还是希望能和单库单表那样的去操作数据库。例如我们要批量插入四条用户记录,并且希望根据用户的id字段,确定这条记录插入哪个库的哪张表。例如1号记录插入user_1表,2号记录插入user_2表,3号记录插入user_3表,4号记录插入user_0表,以此类推。sql如下所示:

insert into user(id,name) values (1,”tianshouzhi”),(2,”huhuamin”), (3,”wanghanao”),(4,”luyang”)

 这样的sql明显是无法执行的,因为我们已经对库和表进行了拆分,这种sql语法只能操作mysql的单个库和单个表。所以必须将sql改成4条如下所示,然后分别到每个库上去执行。

insert into user_1(id,name) values (1,”tianshouzhi”)
insert into user_2(id,name) values (2,”huhuamin”)
insert into user_3(id,name) values (3,”wanghanao”)
insert into user_0(id,name) values  (4,”luyang”)

具体流程可以用下图进行描述:

Image.png

解释如下:

    sql解析:首先对sql进行解析,得到需要插入的四条记录的id字段的值分别为1,2,3,4

    sql路由:sql路由包括库路由和表路由。库路由用于确定这条记录应该插入哪个库,表路由用于确定这条记录应该插入哪个表。

    sql改写:因为一条记录只能插入到一个库中,而上述批量插入的语法将会在 每个库中都插入四条记录,明显是不合适的,因此需要对sql进行改写,每个库只插入一条记录。

    sql执行:一条sql经过改写后变成了多条sql,为了提升效率应该并发的到不同的库上去执行,而不是按照顺序逐一执行

    结果集合并:每个sql执行之后,都会有一个执行结果,我们需要对分库分表的结果集进行合并,从而得到一个完整的结果。

挑战2:分布式id

    在分库分表后,我们不能再使用mysql的自增主键。因为在插入记录的时候,不同的库生成的记录的自增id可能会出现冲突。因此需要有一个全局的id生成器。目前分布式id有很多中方案,其中一个比较轻量级的方案是twitter的snowflake算法。

挑战3:分布式事务

    分布式事务是分库分表绕不过去的一个坎,因此涉及到了同时更新多个数据库。例如上面的批量插入记录到四个不同的库,如何保证要么同时成功,要么同时失败。关于分布式事务,mysql支持XA事务,但是效率较低。柔性事务是目前比较主流的方案,柔性事务包括:最大努力通知型、可靠消息最终一致性方案以及TCC两阶段提交。但是无论XA事务还是柔性事务,实现起来都是非常复杂的。

挑战4:动态扩容

    动态扩容指的是增加分库分表的数量。例如原来的user表拆分到2个库的四张表上。现在我们希望将分库的数量变为4个,分表的数量变为8个。这种情况下一般要伴随着数据迁移。例如在4张表的情况下,id为7的记录,7%4=3,因此这条记录位于user_3这张表上。但是现在分表的数量变为了8个,而7%8=0,而user_0这张表上根本就没有id=7的这条记录,因此如果不进行数据迁移的话,就会出现记录找不到的情况。本教程后面将会介绍一种在动态扩容时不需要进行数据迁移的方案。

4、总结

    在上面我们已经看到了,读写分离和分库分表带来的好处,但是也面临了极大的挑战。如果由业务开发人员来完成这些工作,难度比较大。因此就有一些公司专门来做一些数据库中间件,对业务开发人员屏蔽底层的繁琐细节,开发人员使用了这些中间件后,不论是读写分离还是分库分表,都可以像操作单库单表那样去操作。

    在下一节中,我们将介绍 主流的数据库中间件设计方案和实现。