理论结合实践总结分库、分表

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/phantom_111/article/details/83448665

垂直拆分

垂直分表

把一张列比较多的表拆分成多张表,又称为「大表拆小表」,拆分是基于关系型数据库中的「列」(字段)进行的。通常情况,某个表中的字段比较多,可以新建立一种「扩展表」,可以按照如下原则进行垂直拆分:

  • 把不经常使用的字段单独放在一张表
  • 把 text, blob 等大字段拆分出来放在附表中
  • 经常组合查询的列放在一张表中

数据库设计的一般建议遵循三大范式,垂直拆分更多的时候应该在数据表设计之初就执行,但是实际使用的的场景中,等级越高的范式设计出来的表越多,会大大的增加查询的时间。所以当我们业务所涉及的表非常多,可以考虑使用「反范式」即用空间换取时间,把数据冗余在多个表中,避免多表 join 查询

垂直分库

垂直分库在「微服务」盛行的今天已经变的非常普及。基本思路就是按照业务模块来划分出不同的数据库,而不是将所有的数据表都放在同一个数据库中。垂直分库的好处:

  • 系统层面「服务化」 拆分操作,能够解决业务系统层面的耦合和性能瓶颈,有利于系统的扩展维护。
  • 在高并发场景下,垂直分库一定程度上能够突破单机的 IO、连接数以及单机硬件资源的瓶颈。

水平拆分

水平分表

水平拆分是指数据表行的拆分,表中的行数据超过百万级时,查询就会变慢,此时可以通过将一张表的数据拆分成多张表来存放。简单来说,就是将表中的不同数据行按照一定规律分布到不同的数据表中,这样可以降低单表数据量,优化查询性能。常见的方式&好处:

  • 通过主键或者时间等字段进行 Hash 和取模后拆分
  • 水平分表,一定程度上可以缓解查询性能的瓶颈。但本质上这些表还是保存在同一库中,所以库级别还是会有IO 瓶颈。一般很少使用。

水平分库分表

水平分库分表与水平分表的核心思想相同,唯一的不同就是将这些拆分出来的表保存在不同的数据中。很多大型的互联网公司都是采用这种做法。水平分库分表的好处:

  • 可以进行「冷热数据分离」
  • 有效缓解单机和单库的性能瓶颈和压力,突破 IO、连接数、硬件资源的瓶颈。

分库分表的难点

不论是垂直拆分还是水平拆分,在拆分后都会带来一些复杂的技术问题和挑战,例如跨分片的复杂查询,跨分片的事务等。

跨库 join 查询

拆分后,数据库可能是分布式的在不同实例和不同主机上的,基于架构规范、性能、安全性等方面的考虑, 一般是禁止跨库 join 的。首先考虑下垂直分库的设计问题,如果不能调整,可以结合实际场景从如下方案中选择一种:

1.全局表

将系统只能够所有模块都依赖到的一些表,在每个数据库中均保存一份。但这类信息应该是很少发生修改的,所以几乎不用担心「一致性」 的问题。

2.字段冗余

字段冗余通常是为了避免 join 查询。在电商业务中有个很常见的场景:

「订单表」中保存「卖家 ID」的同时,将卖家「name」作为冗余字段,这样查询订单详情的时候就不再需要查询「卖家用户表」

  • 冗余字段是一种「空间换时间」的体现,适用依赖字段比较少的情况下
  • 冗余字段会带来的另一个问题就是数据一致性的问题,卖家修改名称之后订单表中信息是否要同步,当然是可以通过触发器或者业务代码层面来保证,但只要有人为干预就导致问题更复杂

3.数据同步

数据库 A 中表 a 和数据库 B 中表 b 有关联,需要定时指定表同步。但同步会对数据库带来一定的影响,需要在性能影响和数据实效性中取得一个平衡。

4.系统层组装

在系统层面上,通过调用不同的模块的组件或者服务,获取到数据并进行字段拼接。但在实际使用中,通常会遇到查询效率的问题。

  • 字段组装的情况下,需要先获取「主表」数据,然后再根据关联关系,调用其他模块的组件或服务来获取依赖的其他字段,最后将数据进行组装。
  • 关联关系的查询的复杂度即使在系统层进行组装也是能够接受的,但是对于连接查询并且还带有过滤条件的情况下,想在代码层面进行组装数据,代码会变得异常复杂
  • 通常情况下可以通过缓存来避免频繁 RPC 通信和数据库查询的开销

5.跨库事务

将业务拆分数据库之后,不可避免的就是「分布式事务」的问题

实践

以上的内容都是参考网上的文章进行总结的,以下的内容是在上海 gopher meeting 上听许老师分享学来的。

券码设计

假如产品经理提了一个需求是,让我们设计一个满足如下特性的物品券码:不可重复、不可预测、不能太长、数字码,你会这么设计券码?

  • UUID/GUID X
  • MongoDB ObjectId X
  • Twitter Snowflake X

线性同余

在这里插入图片描述

  • 线性同余容易实现,生产速度快,但是弊端也很明显,32 位的数周期最长只能到 2 32 2^{32}
  • 精心挑选的参数,可保证在整个周期 (m) 内随机数不重复

对于不可预测的问题,需要怎么解决?

券码生成的功能需要支持能够被多个进程同时访问,并且实现不可预测的功能,所以可以按照如下步骤执行(seed 指的是 Nj):

  • 从数据库获取 seed ,并持有写锁
  • 生成一批随机数并 shuffle (洗牌,破坏可预测性)
  • 更新 seed

分库分表的场景下,如何选择 sharding key?

在分库分表的情况下, sharding key 只有一个,但需要支持两个独立的查询条件:

  • 根据 UID 查询,比如钱包应用中查看我的券
  • 根据券码查,比如商户核销券码

按照上面的介绍,你可能会想到,维护 A、 B 两张内容一样的表,一张为 UID 索引,一张为 code 核销索引,通过冗余能够提高查询性能。但维护第二份数据的服务器成本,以及两张表中的数据一致性,这些都使得问题更加复杂。

更简单的实践:

  • 引入新的字段作为 sharding key : shardID = HASH(UID) % M
  • 券码再包含 shardID,因为券码不能太长,M 此处选择的是 10000

分布式事务之用户间转增

分布式事务可以通过队列来实现,这张图是直接在现场照的,原谅我实在是不想自己再画一遍。
在这里插入图片描述

这里有两个点需要注意:

  • 消息服务不是事务的:消息存了,数据库更新可能失败
  • 消息服务不是事务的:消息清理可能失败
  • 所以分布式事务在写代码的时候有很多细节需要注意

参考资料

猜你喜欢

转载自blog.csdn.net/phantom_111/article/details/83448665