第二十天:浪迹天涯网上商城(1.0版本)--浪迹天涯商城关于分库分表的思路---取模实现
背景
“分库分表”是谈论数据库架构和优化时经常听到的关键词。那么对于这些业务量正在高速增长的公司,它有那么容易实践吗?
在谈论数据库架构和数据库优化的时候,我们经常会听到“分库分表”、这样的关键词。让人感到高兴的是,这些朋友所服务的公司业务量正在(或者即将面临)高速增长,技术方面也面临着一些挑战。让人感到担忧的是,他们系统真的就需要“分库分表”了吗?“分库分表”有那么容易实践吗?为此,笔者整理了分库分表中可能遇到的一些问题,并结合以往经验介绍了对应的解决思路和建议。
一、按业务垂直分库
垂直分库在“微服务”盛行的今天已经非常普及了。基本的思路就是按照业务模块来划分出不同的数据库,而不是像早期一样将所有的数据表都放到同一个数据库中。如下图:
系统层面的“服务化”拆分操作,能够解决业务系统层面的耦合和性能瓶颈,有利于系统的扩展维护。而数据库层面的拆分,道理也是相通的。与服务的“治理”和“降级”机制类似,我们也能对不同业务类型的数据进行“分级”管理、维护、监控、扩展等。
众所周知,数据库往往最容易成为应用系统的瓶颈,而数据库本身属于“有状态”的,相对于Web和应用服务器来讲,是比较难实现“横向扩展”的。数据库的连接资源比较宝贵且单机处理能力也有限,在高并发场景下,垂直分库一定程度上能够突破IO、连接数及单机硬件资源的瓶颈,是大型分布式系统中优化数据库架构的重要手段。
然而,很多人并没有从根本上搞清楚为什么要拆分,也没有掌握拆分的原则和技巧,只是一味的模仿大厂的做法。导致拆分后遇到很多问题,例如跨库join,分布式事务等。
二、按业务垂直分库,水平分表
我们已订单库为例,进行垂直分库,水平分表的设计。 水平分表容易理解,就是将表中不同的数据行按照一定规律分布到不同的表中,这些表保存在同一个数据库中,这样可以降低单表数据量,优化查询性能。最常见的方式就是通过主键取模或者其他字段进行Hash再取模后进行拆分。如下图所示:
分库规则:库ID=订单ID%库的数量(此时为1)=[0]
分表规则:表ID=订单ID/库的数量(此时为1)%表的数量(此时为2)=[0,1]
三、水平分库,水平分表
实现扩容的步骤:
第一步:先让 “机器B的订单库_1” 成为 “机器A的订单库_0” 的从库。
第二步:实现数据库的主从复制。
第三步:在主从复制完成之后,停用线上的订单库的所有写操作一段时间。
第四步:确认 “机器B的订单库_1” 已经完全同步 “机器A的订单库_0” 的数据。
第五步:更新分库分表规则。
库ID=订单ID%库的数量(此时为2)=[0,1]
表ID=订单ID/库的数量(此时为2)%表的数量(此时为2)=[0,1]
第六步:打开线上的写操作开关。
第七步:最后删除各个库的冗余数据即可。
四、水平分库,水平分表,继续成倍扩容
实现扩容的步骤:
第一步:让 “机器C的订单库_2” 成为 “机器A的订单库_0” 的从库,让 “机器D的订单库_3” 成为 “机器B的订单库_1” 的从库。
第二步:实现数据库的主从复制。
第三步:在主从复制完成之后,停用线上的订单库的所有写操作一段时间。
第四步:确认 “机器C的订单库_2” 已经完全同步 “机器A的订单库_0” 的数据。
第五步:确认 “机器D的订单库_3” 已经完全同步 “机器B的订单库_1” 的数据。
第五步:更新分库分表规则。
库ID=订单ID%库的数量(此时为4)=[0,3]
表ID=订单ID/库的数量(此时为4)%表的数量(此时为2)=[0,1]
第六步:打开线上的写操作开关。
第七步:最后删除各个库的冗余数据即可。
五、分库分表的难点
水平分库分表,能够降低单表的数据量,一定程度上可以缓解查询性能瓶颈。在高并发和海量数据的场景下,分库分表能够有效缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源的瓶颈。当然,投入的硬件成本也会更高。同时,这也会带来一些复杂的技术问题和挑战,例如:跨分片的复杂查询,跨分片事务等。
分库分表带来的问题和解决思路:
第一:跨库join的问题的产生
在拆分之前,系统中很多列表和详情页所需的数据是可以通过sql join来完成的。而拆分后,数据库可能是分布式在不同实例和不同的主机上,join将变得非常麻烦。而且基于架构规范,性能,安全性等方面考虑,一般是禁止跨库join的。下面笔者将结合以往的实际经验,总结几种常见的解决思路,并分析其适用场景。
第二:跨库join的问题的的解决方案
1、字段冗余
这是一种典型的反范式设计,在互联网行业中比较常见,通常是为了性能来避免join查询。
举个电商业务中很简单的场景:“订单表”中保存“商家Id”的同时,将商家的“name”字段也冗余,这样查询订单详情的时候就不需要再去查询“商家用户表”。字段冗余能带来便利,是一种“空间换时间”的体现。但其适用场景也比较有限,比较适合依赖字段较少的情况。最复杂的还是数据一致性问题,这点很难保证,可以借助可靠消息和最终一致性去保证。
当然,也需要结合实际业务场景来看一致性的要求。就像上面例子,如果商家修改了name之后,是否需要在订单信息中同步更新呢?
2、系统层组装
在系统层面,通过调用不同模块的组件或者服务,获取到数据并进行字段拼装。说起来很容易,但实践起来可真没有这么简单,尤其是数据库设计上存在问题但又无法轻易调整的时候。具体情况通常会比较复杂。下面笔者结合以往实际经验,并通过伪代码方式来描述。
伪代码很容易理解,先获取“我的提问列表”数据,然后再根据列表中的UserId去循环调用依赖的用户服务获取到用户的RealName,拼装结果并返回。有经验的读者一眼就能看出上诉伪代码存在效率问题。循环调用服务,可能会有循环RPC,循环查询数据库。不推荐使用。再看看改进后的:
这种实现方式,看起来要优雅一点,其实就是把循环调用改成一次调用。简单字段组装的情况下,我们只需要先获取“主表”数据,然后再根据关联关系,调用其他模块的组件或服务来获取依赖的其他字段,如例中依赖的用户信息,最后将数据进行组装。
通常,我们都会通过缓存来避免频繁RPC通信和数据库查询的开销。
第三:分布式事务的问题
按业务拆分数据库之后,不可避免的就是分布式事务的问题。以往在代码中通过spring注解简单配置就能实现事务的,现在则需要花很大的成本去保证一致性。业界主流的解决方案就是可靠消息+最终一致性。
六、分库分表策略–取模实现的优缺点
1、优点:数据热点分散。
2、缺点:对非主键字段维度查询困难,扩容需要进行数据迁移。