本文你将学到什么?
本文是手把手项目实战系列的第二篇文章上一篇手把手 0 基础教你搭建一套可自动化构建的微服务框架 (SpringBoot+Dubbo+Docker+Jenkins) 受到巨大好评, 在这里也深表感谢应大家要求继续完成后续章节的撰写上一篇的实战过程介绍的高喜商城项目其实是一个真实项目, 它是一个标准的在线商城(为了避嫌, 高喜商城是我随意起的一个假名字), 这个项目的很多技术具有一定的普适性因此我计划将它实现的方方面面以项目实战的形式介绍给大家, 让大家体验一个真实线上项目的开发运维升级过程
相信很多同学对分库分表这一概念一知半解, 不用着急, 本文的后续章节将会分成知识点扫盲篇和实战动手篇两部分知识点扫盲篇将会从零开始, 介绍分库分表的基本知识, 然后再带领大家开始动手实践希望能够给你带来完美的阅读体验接下来我将用尽量通俗易懂的语言介绍分库分表的相关知识, 不装逼, 做一个低调的程序猿
预告一下, 整个系列会介绍如下内容:
手把手 0 基础项目实战 (一) 教你搭建一套可自动化构建的微服务框架(SpringBoot+Dubbo+Docker+Jenkins)
手把手 0 基础项目实战 (三) 教你开发一套权限管理系统
手把手 0 基础项目实战 (四) 电商订单系统架构设计与实战(分布式事务一致性保证)
手把手 0 基础项目实战 (五) 电商系统的缓存策略
手把手 0 基础项目实战 (六) 基于配置中心实现集群配置的集中管理和熔断机制
手把手 0 基础项目实战 (七) 电商系统的日志监控方案
手把手 0 基础项目实战 (八) 基于 JMeter 的系统性能测试
知识点扫盲篇
1. 什么是分库分表?
随着大数据时代的到来, 业务系统的数据量日益增大, 数据存储能力逐渐成为影响系统性能的瓶颈目前主流的关系型数据库单表存储上限为 1000 万条记录, 而这一存储能力显然已经无法满足大数据背景下的业务系统存储要求了随着微服务架构分布式存储等概念的出现, 数据存储问题也渐渐迎来了转机而数据分片是目前解决海量数据持久化存储与高效查询的一种重要手段数据分库分表的过程在系统设计阶段完成, 要求系统设计人员根据系统预期的业务量, 将未来可能出现瓶颈的数据库数据表按照一定规则拆分成多个库多张表这些数据库和数据表需要部署在不同的服务器上, 从而将数据读写压力分摊至集群中的各个节点, 提升数据库整体处理能力, 避免出现读写瓶颈的现象
目前数据分片的方式一共有两种: 离散分片和连续分片
离散分片是按照数据的某一字段哈希取模后进行分片存储只要哈希算法选择得当, 数据就会均匀地分布在不同的分片中, 从而将读写压力平均分配给所有分片, 整体上提升数据的读写能力然而, 离散存储要求数据之间有较强的独立性, 但实际业务系统并非如此, 不同分片之间的数据往往存在一定的关联性, 因此在某些场景下需要跨分片连接查询由于目前所有的关系型数据库出于安全性考虑, 均不支持跨库连接因此, 跨库操作需要由数据分库分表中间件来完成, 这极大影响数据的查询效率此外, 当数据存储能力出现瓶颈需要扩容时, 离散分片规则需要将所有数据重新进行哈希取模运算, 这无疑成为限制系统可扩展性的一个重要因素虽然, 一致性哈希能在一定程度上减少系统扩容时的数据迁移, 但数据迁移问题仍然不可避免对于一个已经上线运行的系统而言, 系统停止对外服务进行数据迁移的代价太大
第二种数据分片的方式即为连续分片, 它能解决系统扩容时产生的数据迁移问题这种方式要求数据按照时间或连续自增主键连续存储从而一段时间内的数据或相邻主键的数据会被存储在同一个分片中当需要增加分片时, 不会影响现有的分片因此, 连续分片能解决扩容所带来的数据迁移问题但是, 数据的存储时间和读写频率往往呈正比, 也就是大量的读写往往都集中在最新存储的那一部分数据, 这就会导致热点问题, 并不能起到分摊读写压力的初衷
2. 数据库扩展的几种方式
数据库扩展一共有四种分配方式, 分别是: 垂直分库垂直分表水平分表水平数据分片每一种策略都有各自的适用场景
垂直分库
垂直分库即是将一个完整的数据库根据业务功能拆分成多个独立的数据库, 这些数据库可以运行在不同的服务器上, 从而提升数据库整体的数据读写性能这种方式在微服务架构中非常常用微服务架构的核心思想是将一个完整的应用按照业务功能拆分成多个可独立运行的子系统, 这些子系统称为微服务, 各个服务之间通过 RPC 接口通信, 这样的结构使得系统耦合度更低更易于扩展垂直分库的理念与微服务的理念不谋而合, 可以将原本完整的数据按照微服务拆分系统的方式, 拆分成多个独立的数据库, 使得每个微服务系统都有各自独立的数据库, 从而可以避免单个数据库节点压力过大, 影响系统的整体性能, 如下图所示
垂直分表
垂直分表如果一张表的字段非常多, 那么很有可能会引起数据的跨页存储, 这会造成数据库额外的性能开销, 而垂直分表可以解决这个问题垂直分表就是将一张表中不常用的字段拆分到另一张表中, 从而保证第一章表中的字段较少, 避免出现数据库跨页存储的问题, 从而提升查询效率而另一张表中的数据通过外键与第一张表进行关联, 如下图所示
水平分表
如果一张表中的记录数过多(超过 1000 万条记录), 那么会对数据库的读写性能产生较大的影响, 虽然此时仍然能够正确地读写, 但读写的速度已经到了业务无法忍受的地步, 此时就需要使用水平分表来解决这个问题水平分表是将一张含有很多记录数的表水平切分, 拆分成几张结构相同的表举个例子, 假设一张订单表目前存储了 2000 万条订单的数据, 导致数据读写效率极低此时可以采用水平分表的方式, 将订单表拆分成 100 张结构相同的订单表, 分别叫做 order_1order_2order_100 然后可以根据订单所属用户的 id 进行哈希取模后均匀地存储在这 100 张表中, 从而每张表中只存储了 20 万条订单记录, 极大提升了订单的读写效率, 如下图所示 当然, 如果拆分出来的表都存储在同一个数据库节点上, 那么当请求量过大的时候, 毕竟单台服务器的处理能力是有限的, 数据库仍然会成为系统的瓶颈, 所以为了解决这个问题, 就出现了水平数据分片的解决方案
水平分库分表
水平数据分片与数据分片区别在于: 水平数据分片首先将数据表进行水平拆分, 然后按照某一分片规则存储在多台数据库服务器上从而将单库的压力分摊到了多库上, 从而避免因为数据库硬件资源有限导致的数据库性能瓶颈, 如下图所示
3. 分库分表的几种方式
目前常用的数据分片策略有两种, 分别是连续分片和离散分片
离散分片
离散分片是指将数据打散之后均匀地存储在逻辑表的各个分片中, 从而使的对同一张逻辑表的数据读取操作均匀地落在不同库的不同表上, 从而提高读写速度离散分片一般以哈希取模的方式实现比如: 一张逻辑表有 4 个分片, 那么在读写数据的时候, 中间件首先会取得分片字段的哈希值, 然后再模以 4, 从而计算出该条记录所在的分片在这种方法中, 只要哈希算法选的好, 那么数据分片将会比较均匀, 从而数据读写就会比较均匀地落在各个分片上, 从而就有较高的读写效率但是, 这种方式也存在一个最大的缺陷数据库扩容成本较高采用这种方式, 如果需要再增加分片, 原先的分片算法将失效, 并且所有记录都需要重新计算所在分片的位置对于一个已经上线的系统来说, 行级别的数据迁移成本相当高, 而且由于数据迁移期间系统仍在运行, 仍有新数据产生, 从而无法保证迁移过程数据的一致性如果为了避免这个问题而停机迁移, 那必然会对业务造成巨大影响当然, 如果为了避免数据迁移, 在一开始的时候就分片较多的分片, 那需要承担较高的费用, 这对于中小公司来说是无法承受的
连续分片
连续分片指的是按照某一种分片规则, 将某一个区间内的数据存储在同一个分片上比如按照时间分片, 每个月生成一张物理表那么在读写数据时, 直接根据当前时间就可以找到数据所在的分片再比如可以按照记录 ID 分片, 这种分片方式要求 ID 需要连续递增由于 Mysql 数据库单表支持最大的记录数约为 1000 万, 因此我们可以根据记录的 ID, 使得每个分片存储 1000 万条记录, 当目前的记录数即将到达存储上限时, 我们只需增加分片即可, 原有的数据无需迁移连续分片的一个最大好处就是方便扩容, 因为它不需要任何的数据迁移但是, 连续分片有个最大的缺点就是热点问题连续分片使得新插入的数据集中在同一个分片上, 而往往新插入的数据读写频率较高, 因此, 读写操作都会集中在最新的分片上, 从而无法体现数据分片的优势
4. 引入分库分表中间件后面临的问题
跨库操作
在关系型数据库中, 多张表之间往往存在关联, 我们在开发过程中需要使用 JOIN 操作进行多表连接但是当我们使用了分库分表模式后, 由于数据库厂商处于安全考虑, 不允许跨库 JOIN 操作, 从而如果需要连接的两张表被分到不同的库中后, 就无法使用 SQL 提供的 JOIN 关键字来实现表连接, 我们可能需要在业务系统层面, 通过多次 SQL 查询, 完成数据的组装和拼接这一方面会增加业务系统的复杂度, 另一方面会增加业务系统的负载 因此, 当我们使用分库分表模式时, 需要根据具体的业务场景, 合理地设置分片策略设置分片字段, 这将会在本文的后续章节中介绍
分布式事务
我们知道, 数据库提供了事务的功能, 以保证数据一致性然而, 这种事务只是针对单数据库而言的, 数据库厂商并未提供跨库事务因此, 当我们使用了分库分表之后, 就需要我们在业务系统层面实现分布式事务关于分布式事务的详细内容, 可以参考笔者的另一篇文章常用的分布式事务解决方案
5. 现有分库分表中间件的横向对比
Cobar 实现数据库的透明分库, 让开发人员能够在无感知的情况下操纵数据库集群, 从而简化数据库的编程模型然而 Cobar 仅实现了分库功能, 并未实现分表功能分库可以解决单库 IOCPU 内存的瓶颈, 但无法解决单表数据量过大的问题此外, Cobar 是一个独立运行的系统, 它处在应用系统与数据库系统之间, 因此增加了额外的部署复杂度, 增加了运维成本
为了解决上述问题, Cobar 还推出了一个 Cobar-Client 项目, 它只是一个安装在应用程序的 Jar 包, 并不是一个独立运行的系统, 一定程度上降低了系统的复杂度但和 Cobar 一样, 仍然只支持分库, 并不支持分表, 也不支持读写分离
MyCat 是基于 Cobar 二次开发的数据库中间件, 和 Cobar 相比, 它增加了读写分离的功能, 并修复了 Cobar 的一些 bug 但是, MyCat 和 Cobar 一样, 都是一套需要独立部署的系统, 因此会增加部署的复杂度, 提高了后期系统运维的成本
实战篇
1. 为何要进行分库分表?
高喜商城已经上线了一段时间, 用户量超预期增长, 业务层采用基于 Dubbo 的微服务架构, 并结合了 Docker+Jenkins 实现了自动化部署, 具备灵活的扩展能力, 能够轻松支撑目前的业务量然而, 数据库层面却出现了瓶颈由于 1.0 版本采用单库单表设计, 虽然使用 Mysql 读写分离实现了一主多备架构, 一定程度上分摊了数据库的读写压力但按照目前的业务发展速度, 很多业务表将会面临单表过长的问题目前 Mysql 数据库在保证读写性能的前提下, 单表最大支持 1000W 条数据当单表超过 1000W 条数据后, 虽然仍然可以存储数据, 但读写性能大幅下降因此, 为了满足极速增长的业务需求, 需要使用数据库中间件实现数据分库分表存储分库能将读写压力分摊至不同节点, 从而缓解读写压力; 而分表能够避免单表过长的问题此外, 大多数分库分表中间件都会提供读写分离的功能, 从而进一步缓解数据库的读写压力, 提升读写性能
综上所述, 对数据库进行分库分表迫在眉睫!
2. 高喜商城 1.0 数据库架构介绍
高喜商城 1.0 的架构如下图所示:
该架构的业务层采用微服务架构, 所有将整个应用分成四个业务系统: 用户系统产品系统订单系统和数据分析系统关于微服务架构这里不做过多介绍, 详细内容请阅读手把手 0 基础项目实战 (一) 教你搭建一套可自动化构建的微服务框架(SpringBoot+Dubbo+Docker+Jenkins), 这里主要介绍数据库架构
在高喜商城 1.0 版本中, 虽然业务层采用微服务架构, 业务层被拆分成多个相互独立的子系统, 但在数据库层, 整个系统的所有表均在同一个数据库中存储此外, 采用数据库的主从复制实现了读写分离, 数据库有一个主库和两个从库组成了一个数据库集群它是一个对等集群, 每个库中存储的数据是一致的
在加入了读写分离后, 一方面提升了数据库的读写性能; 另一方面, 实现了数据库的高可用当某一个节点发生故障时, 仍然有其他两个节点提供服务
这种架构存在如下几个缺点:
没有垂直分库: 所有业务系统的表均存储在同一个库中, 相互之间没有任何隔离, 从而导致一个业务系统可以直接读写其他业务系统的数据, 这违背了微服务的理念
存在单表过长的问题: 系统经过一段时间运营后, 有些表的数据量较大, 单表数据量可能会超过 1000W 这将会极大影响该表的读写性能
针对上述问题, 对数据库进行分库分表迫在眉睫
3. 高喜商城 2.0 数据库架构的演进
高喜商城 2.0 数据库架构如下图所示:
在 2.0 架构中, 首先对数据库进行了垂直拆分, 每个子系统均拥有自己独立的数据库, 不同系统的数据库相互隔离, 无法互相访问这样保证了各个业务系统的纯粹性, 不同业务系统之间如果需要数据交互, 那么就通过业务系统提供了 RPC 接口访问, 而非通过数据库访问, 从而符合微服务的设计理念
上图对用户系统的数据库架构做了详细介绍, 其他系统的数据库架构和用户系统类似, 都采用了分库分表 + 读写分离的架构
在用户系统中, 数据库一共被分成 N 个主库和 N 个从库, 每个库中的表又被拆分成多张以上图为例, 用户系统的数据库一共被分成两个物理库, 分别是 db_0 和 db_1 此外, 为了实现读写分离, 每个物理库均拥有一个从库, 主从数据库的数据保持一致从而, 用户系统的物理库一共被分成四个, 分别是: db_0_masterdb_1_masterdb_0_slavedb_1_slave
每个库中的表 table 被水平拆分成两张, 分别是 table_0table_1 从而, 原本一张 table 表被水平拆分成了四张, 分别是: db_0_master_table_0db_0_master_table_1db_1_master_table_0db_1_master_table_1 与此同时, 从库中也有四张这样的 table 表, 并且和主库的数据保持一致, 因此, 经过水平拆分后, 一共有 8 张 table 表
上述 table 表只是举一个例子, 实际每个系统均包含有多张表, 每张表的拆分规则和拆分数量要根据该表具体的业务量来决定具体的拆分过程将在下面介绍
4. 高喜商城 1.0 数据库表结构设计
下面将会详细介绍高喜商城数据表的设计这些设计在在线商城系统中是通用的, 具备一定的借鉴意义, 因此下面将会详细介绍
4.1 用户系统数据表
用户系统的数据表一共由如上六张表构成, 下面对这六张表的作用以及相互之间的关系作简单介绍
sys_user: 用户表
存储用户的基本信息
sys_role: 角色表
存储本系统中所有的角色, 如: 超级管理员普通用户企业用户等等
用户和角色之间是多对一的聚合关系, 即一个用户只能拥有一种角色, 而一种角色却可以属于多个用户由于角色可以脱离用户单独存在, 因此他们之间是一种弱依赖关系聚合关系
sys_permission: 权限表
存储本系统的权限信息, 如: 创建角色删除角色创建菜单删除菜单修改用户信息等等
角色和权限是多对多的聚合关系, 即一种角色可以拥有多种权限, 而一种权限也可以属于多种角色并且由于权限可以脱离角色单独存在, 因此他们之间是弱依赖关系聚合关系
更多关于本系统权限管理功能的设计, 请关注后面即将推出的手把手 0 基础教你实现一套权限管理系统
sys_menu: 菜单表
存储本系统的菜单信息
由于需要实现角色看到不同的菜单, 因此需要建立这张菜单表, 存储本系统所有的菜单信息
角色和菜单是多对多的聚合关系, 即一种角色可以拥有多个菜单, 而一个菜单也可以属于多种角色并且由于菜单可以脱离于角色单独存在, 因此他们之间是弱依赖关系聚合关系
location: 用户地址信息表
存储用户的地址信息
用户下单之后需要填写收货地址, 因此需要这张表存储用户的地址信息
用户和地址之间是一对多的组合关系, 即一个用户可以拥有多个收货地址, 并且一个收货地址只能属于一个用户此外, 由于收获地址不能脱离于用户单独存在, 因此他们之间是强依赖关系组合关系
receipt: 发票表
用户在下单时可以填写发票信息, 因此需要这张表来存储这些发票信息
用户和发票之间是一对多的组合关系, 即一个用户可以拥有多个发票信息, 而一个具体的发票信息只能属于一个用户此外, 由于发票不能脱离于用户单独存在, 因此他们之间是强依赖关系组合关系
到此为止, 用户系统的每一张表及表于表之间的关系都已详细介绍完毕通过这些表以及表之间的关系我们就能看出用户系统的业务需求
每一个用户都有且仅有一种确定的角色, 该角色对应了若干个菜单和若干种权限当用户登录系统的时候, 用户系统就可以根据数据库中存储的这些用户信息, 知道该用户能够看到哪些菜单, 然后将这些菜单显示在用户的界面上此外, 当用户操作这个系统时, 前端就会调用相应的后台接口, 每次调用任何接口时, 用户系统都会根据用户的权限信息检测该用户是否具有操作该接口的权限, 如果没有权限则拒绝执行, 从而保证系统的安全性
一个用户在下单的时候可以要求开具发票, 那么这些发票信息将会被存储在 receipt 表中, 当用户再次下单的时候, 我们就会查询 receipt 表, 将该用户所有的发票信息展示给他, 供用户选择
一个用户在下单的时候需要填写收货地址, 那么这些收获地址就会被存储在 location 表中, 当用户再次下单时, 无需再次输入收获地址, 我们的系统会查询 location 表, 让用户直接选择
4.2 产品系统数据表
产品系统的数据表一共由如上四张表构成, 下面对这四张表的作用以及相互之间的关系作简单介绍
product: 产品表
存储本系统所有的产品信息
prod_image: 产品图片表
存储本系统所有的产品图片 URL
产品和图片之间是一对多组合关系, 即一个产品能够拥有多张图片, 而一张图片只能属于某一个产品, 并且图片不能脱离于产品单独存在, 因此他们之间是强依赖关系组合关系
brand: 品牌表
存储本系统中所有的品牌信息
产品和品牌是多对一的聚合关系, 即一个产品只属于一种品牌, 而一种品牌可以包含多个产品并且品牌可以独立于产品单独存在, 因此他们之间是弱依赖关系聚合关系
category: 类别表
每件产品都必须属于一个类别, 因此通过类别表来存储所有的类别信息
产品和类别之间是多对一的聚合关系, 即一件产品只能属于一种类别, 而一种类别却可以包含多件产品并且类别可以独立于产品存在, 因此他们之弱依赖关系聚合关系
4.3 订单系统数据表
订单系统的数据表一共由如上三张表构成, 下面对这三张表的作用以及相互之间的关系作简单介绍
orders: 订单表
存储本系统所有用户的订单信息
orders_product: 订单中的产品表
每条订单中一般都包含多件产品, 这种映射关系就存储在这张表中
这张表是订单和产品之间的关联表
订单和产品之间是多对多的聚合关系, 即一条订单中可以包含多件产品, 并且一件产品也可以属于多条订单此外, 由于产品可以独立于订单而存在, 因此他们之间是弱依赖关系聚合关系
订单和订单产品表是一对多组合关系因为, 一条订单中往往包含多个产品, 而一条订单产品映射只能属于某一条具体的订单并且订单产品不能独立于订单而存在, 因此他们之间是强依赖关系组合关系
order_state_time: 订单中各种状态发生时间表
一条订单有多种状态, 如: 已下单未支付已支付发货中已收获等等为了能够详细记录订单每个状态的发生时间, 因此需要这张 order_state_time 表
订单和订单状态之间是一对多的组合关系, 即一条订单可以包含多种订单状态时间, 而一种订单状态时间只能属于某一条具体的订单并且订单状态时间不能独立于订单而存在, 因此他们之间是强依赖关系组合关系
到此为止, 一个在线商城中最核心的三大系统的数据表关系已经梳理清楚了, 下面将会根据具体的业务指标, 对这些数据库和数据表进行合理的分库分表
5. 高喜商城 2.0 分库分表方案
在对高喜商城开始分库分表之前, 我们先要搞清楚, 究竟为何要分库? 为何要分表? 为何要读写分离?
分库的目的: 将对同一个库的读写压力分摊到多个库上, 不同库分布在不同的服务器上, 从而缓解每个库上的读写压力, 避免因服务器硬件资源 (如 IO 内存 CPU) 导致的瓶颈
分表的目的: 将原本一张表中的数据水平拆分至多张表中, 从而避免单表过长, 提升读写性能
读写分离的目的: 将一个物理库复制多份, 主库负责写操作, 从库负责读操作从而避免少量的写操作的表锁或行锁阻塞了大量的读操作, 通过降低数据的一致性来提升读操作的性能
5.1 用户系统的分库分表方案
系统的分库分表策略一定是基于具体的业务指标和实际的业务需求, 在正式进行分库分表策略的设计之前, 一定要做好这两部分数据的采集现在高喜商城的业务指标和业务需求如下面两张表格所示:
高喜商城未来五年的业务指标:
表名 | 未来五年数据量 | 关键字段 |
---|---|---|
sys_user | 1000W | uid, username, password, email, phone, role_id |
location | 5000W | uid, location |
receipt | 5000W | uid, 发票相关字段 |
sys_role | 100 | role_id, role_name |
sys_permission | 1000 | pms_id, permission |
sys_role_permission | 100*1000 | role_id, pms_id |
sys_menu | 200 | menu_id, menu |
sys_role_menu | 200*1000 | role_id, menu_id |
高喜商城的业务需求:
表名 | 业务需求 | 涉及字段 |
---|---|---|
sys_user | 1. 用户登录(用户名登录) | username, password |
2. 用户登录(邮箱登录) | email, password | |
3. 用户登录(短信验证码登录) | phone | |
4. 根据 uid 查询用户信息 | uid | |
5. 管理员按照某些条件分页查询用户 | 任何字段都有可能使用 |
表名 | 业务需求 | 涉及字段 |
---|---|---|
location | 根据 uid 查询收货地址 | uid |
表名 | 业务需求 | 涉及字段 |
---|---|---|
receipt | 根据 uid 查询发票信息 | uid |
高喜商城未来五年预计将会拥有 1000 万用户, 从而用户表将会有 1000 万条数据由于目前 Mysql 单表支持最大长度为 1000 万, 因此为了保险起见, 我们需要将用户表水平拆分成两张此外, 为了防止用户表读写压力过大, 我们干脆将这两张用户表放入两个物理库中并且为了保证用户表的高可用, 我们对这两个数据库采用主从复制技术, 一主一丛, 其结构如下图所示:
从高喜商城未来五年的业务量表中可知, 系统的角色权限菜单数量较少, 没有必要分库分表在用户查询的过程中需要连接用户表角色表权限表和菜单表, 如果将这些无需拆分的表存储在某一个数据库中, 那么用户表将无法和他们进行跨库连接, 从而需要在完成用户信息查询后, 在业务层再次根据 uid 分别查询角色信息权限信息菜单信息, 这无意增加了业务层的实现复杂度为了解决这个问题, 我们可以对角色表权限表和菜单表进行冗余, 即将这些表冗余地存储在 sys_user 的所有物理库中, 从而任何一个物理库的用户查询操作都可以直接通过表连接的方式完成角色信息权限信息和菜单信息的查询其结构如下图所示:
此外, 用户和收获地址用户和发票信息之间都是一对多的组合关系, 如果每个用户平均拥有 5 个收货地址和 5 种发票信息, 那么对于 1000 万用户而言, 一共会创建 5000 万条收获信息和 5000 万条发票信息因此, 收获地址和发票信息各需 6 张表来存储并且, 由于这两种信息都是通过 uid 来查询, 并且查询条件只有 uid 这一项, 因此 uid 毫无争议地成为分片字段, 并且这 6 张表只能分布在 6 个物理库中此外, 为了实现数据库的高可用性, 需要对这 6 个库提供主从复制功能最终, 收货地址表和发票信息表的结构如下图所示:
高喜商城用户系统的数据库分库分表方案就介绍到这, 下面介绍产品系统的分库分表方案
5.2 订单系统的分库分表方案
和用户系统的分库分表方案设计过程一样, 在方案设计之前, 首先要确定系统的业务指标和业务需求
高喜商城未来五年的业务指标:
表名 | 未来五年数据量 | 关键字段 |
---|---|---|
orders | 2000W | order_id, buyer_id, seller_id |
orders_product | 5*2000W | order_id, prodcut_id |
order_state_time | 10*2000W | order_id, state, time |
根据运营同学的估算, 高喜商城未来五年的订单量最多将会达到 2000W 条, 并且平均每条订单中包含 5 件商品, 因此 orders_product 表中的数据量将会达到 10000W; 并且每条订单都有 10 种状态, 因此 order_state_time 表的数据量将会达到 20000W
基于上述数据, orders 需要水平拆分成 4 张物理表, orders_product 需要水平拆分成 20 张物理表, order_state_time 需要水平拆分成 40 张物理表
那么这写物理表究竟该分配给多少个物理库中? 这需要由业务需求来决定
高喜商城的业务需求:
表名 | 业务需求 | 涉及字段 |
---|---|---|
orders | 1. 根据 buyer_id 分页查询某一用户的订单 | buyer_id |
2. 根据 order_id 查询订单 | order_id | |
3. 根据 seller_id 分页查询某一商家的订单 | seller_id |
表名 | 业务需求 | 涉及字段 |
---|---|---|
orders_product | 根据 order_id 查询产品列表 | order_id |
表名 | 业务需求 | 涉及字段 |
---|---|---|
order_state_time | 1. 根据 order_id 和 state 筛选某一状态下的订单 | order_id, state |
2. 修改指定订单的状态 | order_id, state |
订单系统的核心业务需求如上述三张表所示 orders 表和 orders_product 表 order_state_time 表之间都是一对多的组合关系, 在查询过程中需要进行表连接操作因此, 我们必须要指定合理的分库分表方案, 能够使得同一订单的产品信息订单状态信息都落在同一个物理库中, 从而能够直接使用 SQL 语句进行连接操作如果分库分表方案不合理, 那么同一订单的产品信息和订单状态信息会散落在不同的物理库中, 由于 Mysql 并不支持跨库连接, 因此这三张表的连接需要拆分成三次数据库查询, 并在业务层完成数据的连接, 这无意增加了业务层的复杂度下面详细介绍订单系统的分库分表方案
通过分析上述三张业务需求表可知, 订单系统核心操作所涉及到的字段无非就是三个: order_idbuyer_idseller_id 当查询指定订单的时候需要使用 order_id 作为查询条件, 当查询某一买家所有订单的时候需要使用 buyer_id 作为查询条件, 当查询某一卖家所有订单的时候需要使用 seller_id 作为查询条件由此可见, 分片字段需要从这三个字段中选择那么究竟应该如何选择呢? 我们分别来看如下三种方案:
将 order_id 作为分片字段 如果将 order_id 作为分片字段, 那么根据 order_id 查询指定订单的时候可以直接定位到指定的物理表, 然而在根据 buyer_id 和 seller_id 查询订单的时候, 由于无法定位到具体的表, 因此就需要全库表查询, 这显然是低效的
将 buyer_id 作为分片字段 此时, 查询指定买家的订单信息可以直接定位到指定的物理表, 但是当需要根据 order_id 查询具体订单信息查询卖家订单信息时就显得提襟见肘了
将 seller_id 作为分片字段 此时, 查询指定卖家的所有订单信息可以定位到指定的物理表, 但查询买家订单根据订单编号查询订单时就需要全库表查询了
综上所述, 如果只将这三个字段中的某一个作为分片字段, 显然无法满足所有的业务场景, 必定会存在全库表查询, 这就会导致查询效率低下那么, 有没有什么方案能够避免全库表查询呢? 当然有!
首先, 我们来解决跨库连接的问题
解决跨库连接问题的根本方法就是避免跨库连接, 让需要连接的表存储在同一个物理库中在订单系统中, orders 表要分别和 orders_product 表 order_state_time 表产生连接, 并且都是以 order_id 作为连接字段但是, 如果我们以 order_id 作为这三张表的分片字段, 那么当根据 buyer_idseller_id 查询时, 都需要全库表操作所以, 我们需要分别以 buyer_id 和 seller_id 作为分片字段听上去很神奇, 具体怎么实施呢?
在订单关系中, 买家 (buyer_id) 和卖家 (seller_id) 是多对多的聚合关系, 对于多对多关系, 我们可以使用表冗余来实现不同纬度的查询
此时, 我们需要将订单表 (orders) 一分为二, 分别是买家订单表 (orders_buyer) 和卖家订单表 (orders_seller), 这两张表的数据是完全一致的在买家订单表中, 以 buyer_id 作为分片字段; 在卖家订单表中, 以 seller_id 作为分片字段那么当需要查询指定买家的订单时, 根据买家 id(buyer_id) 就可以确定该买家订单数据所在的物理表; 当需要查询指定卖家的订单时, 根据卖家 id(seller_id)就可以确定该卖家订单数据所在的物理表
此时已经避免了上述两个业务场景的全库表查询, 那么还有两种业务场景的全库表查询问题如何解决呢?
根据 order_id 查询订单
orders 表要分别和 orders_product 表 order_state_time 表的连接操作
对于第一个问题, 买家 (buyer_id) 和订单 (order_id) 之间是一对多组合关系对于一对多组合关系, 我们可以建立多>一的映射在这里, 我们需要建立 order_id>buyer_id 的映射关系那么当需要根据 order_id 查询订单的时候, 首先需要查询这个映射关系, 找到 order_id 对应的 buyer_id, 由于 buyer_id 是分片字段, 因此可以直接计算出数据所在的物理表, 从而完成根据 order_id 查询订单的需求
对于第二个问题, 我们可以使用字段冗余的方法来解决在创建买家订单表和卖家订单表的同时, 再分别创建如下四个表:
买家订单产品表(orders_product_buyer)
(order_id, product_id, buyer_id)
加入 buyer_id 字段, 并以 buyer_id 作为分片字段
卖家订单产品表(orders_product_seller)
(order_id, product_id, seller_id)
加入 seller_id 字段, 并以 seller_id 作为分片字段
买家订单状态表(order_state_time_buyer)
(order_id, state, time, buyer_id)
加入 buyer_id 字段, 并以 buyer_id 作为分片字段
卖家订单状态表(order_state_time_seller)
(order_id, state, time, seller_id)
加入 seller_id 字段, 并以 seller_id 作为分片字段
此时, 订单系统的数据库架构如下图所示:
采用了上述方案后, 所有的全库表查询问题都得到了解决, 但不要止步于此, 还可以进一步优化
上述方案中, 我们使用了一张映射表来维护 order_id 和 buyer_id 之间的映射关系, 当需要根据 order_id 查询指定订单的时候, 先要查询映射表, 找到该订单对应的 buyer_id, 然后再根据 buyer_id 计算分片, 找到相应的物理表
这个过程经历了两次地址查询, 还需要额外的策略存储映射表那么, 有没有什么方法能够解决这两个问题呢? 当然是有的, 此时就要介绍我的黑科技了
x%N 的结果其实是由 x 二进制的末尾 logN 位决定的
举个例子, 13534443 % 8, 其实是由 13534443 的二进制表示法的最后 log8 位决定的
13534443 的二进制是: 110011101000010011101011
log8=3
因此, 13534443 % 8 的结果由 011 决定也就是说, 只要末尾三位都是 011 的数字, 对 8 取模的结果都是一样的
基于这个结论, 我们只要保证 buyer_id 和 order_id 的最后 logN 位一致, 就无需再使用额外的映射表来存储这两者的映射关系 order_id 和 buyer_id 的生成方式如下:
在创建订单时, 首先获取买家的 uid
获取 uid 二进制表示法的最后 logN 位, 用 lastN 表示
将 UUID+lastN 作为 order_id
此时同一个买家的 buyer_id%N 的结果和 order_id%N 的结果一致在根据 order_id 查询订单的时候直接通过 order_id%N 计算出订单所在的物理库即可
5.3 产品系统的分库分表方案
高喜商城未来五年的业务指标:
表名 | 未来五年数据量 | 关键字段 |
---|---|---|
product | 100W | product_id |
prod_image | 10*100W | product_id |
brand | 1000 | |
category | 100 |
总体而言, 产品系统的数据量相对较小运营同学规划, 未来五年, 高喜商城的产品数量最多为 100W, 由于每件产品最多允许拥有 10 张图片, 因此 prod_image 表的数量预计为 1000W, 因此需要对 prod_image 表进行拆分; 而产品的品牌产品的类别数量较小, 不需要考虑分库分表
产品系统的数据库分库分表方案如下图所示:
由于 prod_image 表的数据量将会达到 1000W, 因此为了避免单表数据超过 1000W, 将该表根据 prod_id 拆分成两张物理表
此外, 在产品系统中, productbrandcategory 数据量均不会超过 1000W, 因此无需分库分表
来源: https://juejin.im/post/5aa4ded26fb9a028e46e43ca