7.杨明翰的MySQL教学系列之分布式数据库

MySQL教学系列 专栏收录该内容
5 篇文章 1 订阅


前言

本文主要介绍以关系型数据库mysql作为业务数据的存储背景下,
分库分表,主从分离,多主多从的玩法。

下面的文章中会提到一些术语,为了避免歧义,先解释如下:
数据库实例&节点:一个物理数据库或docker容器数据库,
一般具有独立的硬件资源(IOPS,CPU,内存,硬盘,连接数等)。

数据库:一个数据库实例上可以创建n个数据库,一个数据库上可以创建n个表,
一个表中可以创建n个行和列(数据和字段)。


1. 单库单表

在一些小微系统中,数据量和并发量往往很少,一般我们会使用一个数据库实例,
使用一个数据库,以及一个表来存放业务数据,将所有的业务数据全部放到一个数据库中,
这就是所谓的原始人时代&单机时代&单库单表时代。

但在数据量大,并发量高的互联网场景下,单库单表的玩法就显得捉襟见肘了,
我们只能去花更多的钱来升级我们的单机硬件性能,但这样真的合理合法吗?

单库单表的缺点:

  1. 单实例性能瓶颈
    单机(单个数据库实例)的性能是有瓶颈的,
    数据库往往会成为系统性能瓶颈的急先锋,包括IOPS,硬盘,连接数,CPU,内存等性能指标。

  2. 查询效率低
    单表的数据量过大(一般超过500万条数据),会对索引非常不优化,索引的B+树高度提升,
    不利于查询性能。

  3. 没法做高可用
    数据库一旦down掉或hung住后,整个系统蒙受损失,牵一发而动全身。

因此我们需要一些机制,在面对即将高速增长或已经高速增长的业务,
来对单库单表结构进行优化,让数据库也变得更分布式一些。
当索引、缓存、从库都帮不了你的时候,你就需要进行分库分表了。

把单库单表的数据切分到多库多表中,再让每个库分散出一系列从库搞读写分离,
让更多的库表协同作战来共同承担压力,以此来减少每个单库单表的压力,提升查询性能。
秉承着鸡蛋不要放在一个篮子里的原则,即使一个库出事故挂掉了,
影响的也仅仅只是一个业务点。

产品初期业务量小就用单库单表+索引,
有读瓶颈就用一主多从+缓存,有写瓶颈就用分库分表。

单库单表->一主多从&读写分离->分库分表&多主多从。

所谓的优化结构、架构,其实就是各种拆,类似于当前比较流行的微服务。
我们需要把单库单表拆成多库多表。

做分布式数据库的前提是考虑业务场景,不建议过度设计与提前优化,
不结合业务的设计都是耍流氓,劳民伤财。

快速增长到单表500万条数据以上并保持持续快速增长,
或者有预见性的增长,需要考虑分库分表。


2. 一主多从&读写分离

大部分互联网业务场景都是读多写少,读往往成为性能瓶颈。

在访问量增加的前提下,会导致单库单表的数据库节点面临着硬件资源的性能瓶颈,
我们可以通过读写分离的方式把压力分摊,把读与写这2个操作分开,写操作走主库,
读操作走从库,主从搭配的方式构成一个小集群,共同对外提供服务。

主库与从库通过binlog进行数据同步,相当于把主库的数据复制到从库上,
主库与从库存储的数据结构与数据完全相同。

一个主库可以对应多个从库,读库具备高可用,减少锁冲突,
提升读操作的性能。

一主多从的缺点是,主库依然是单点。


3. 分库分表

所谓的分库分表就是按照某种规则将原来单库单表的数据散落在不同的库表上(包括不同的节点上),
共同组成一个数据库集群共同对外提供服务。

就像58同城可以将100亿帖子数据分成256个库一样,玩多主多从,

理论上,我们可以把所有的业务数据无限扩容成n个主库,
n个主库保持高可用,同时再n个主库下还分出n个从库进行读写分离。

分库分表主要有两大类,:竖着拆(垂直切分),横着拆(水平切分)。
有的业务场景要结合这两种拆分方式,既竖着拆,也横着拆,先竖着拆,后横着拆。
有些地方也称为Sharding、分片。

分库分表后,每个库表中的数据结构一样但数据都不一样,
这一点与一主多从不一样。
所有的库表组成一个数据库集群,共同对外提供服务。

一主多从可以线性提升读性能,但不能提升写性能。
分库分表可以线性提升写性能。

3.1 垂直切分

3.1.1 垂直分库

垂直分库多见于微服务架构(每个微服务一个库),按业务解耦(使得业务清晰),
将不同业务模块的表(单个或多个表)放到不同的数据库中,
每个数据库又处于不同的数据库实例&节点中,这样有利于单机硬件性能的提升。

3.1.2 垂直分表

假设一张表中有许多列,可以新建一张表作为扩展表,
将使用频率不高或特别长(存储数据特多)的列挪到新表中,
类似于冷热列分离。

拆分后的两张表或多张表可以做主键1对1关联关系(就是2张表的主键相同)。

mysql底层存储使用“页”来存储数据,每个页是有容量限制的,对于一些大型列,
例如文章内容等,这些列的数据要占用很多的页,跨页查询会造成额外的性能开销。

读取数据时,数据以行为单位被读到内存中,由于内存容量有限,对于长列来说会增加磁盘IO。

3.2 水平切分

3.2.1 水平分表

将一张表中的数据按照一定的规则&算法划分到n张表中,
将切分完的表存储在同一个数据库中。
这样,单表的数据量就被有效的降低了,查询效率提升了,
对索引友好了。

这种只分表不分库的玩法有一点好处:就是在一个库中的事务是不用考虑分布式的。

虽然做了表切分,但这些表还是存在于一个数据库一个数据库实例中,
单机还是有性能瓶颈,因此我们需要更深一步,水平做分库分表。

3.2.1.1 按范围分片

我们可以按照时间作为切分的维度来对表中的数据做切分。
例如:我们可以把2018年的数据放在一张表,2019年的数据放在一张表,
有点类似于冷热数据分离的感觉。

在范围查询中,往往不需要进行跨库跨表查询,效率较高。
在需要做扩容时,只需要新增节点即可,不需要对其他节点的数据进行迁移。

如果主键是整型id的话,那么我们可以类似于把id为1-100000的数据分到a表上,
id为100001-200000的数据分到b表上。

优点:

  1. 路由规则简单。
  2. 在扩容时,只需要新增节点即可,不需要对其他节点的数据进行迁移。

缺点:

  1. 主键必须是自增整型。
  2. 会出现热点数据频繁查询,热的非常热,冷的非常冷,不利于资源利用,
    热表很可能快速成为系统性能瓶颈。

3.2.1.2 按某列取模或哈希分片

一般是按主键或其他业务列做取模或哈希运算后,得知数据要放到哪个库的哪个表中。
在做水平切分前,我们需要定义按照哪个或哪几个字段来做切分?
我们可以把需要做切分的列称为分片列。

如果查询的条件中没有涉及到分片列,那就需要进行全局轮训查询,这样的效率会很低。
因此我们需要结合业务来选择使用最频繁以及最重要的列作为分片列。

分片列的制定需要知道业务场景,类似于99%的查询场景都会使用某个列,
那这个列比较适合做分片列,其他1%的场景使用的列就不用做成分片列。

优点:

  1. 路由规则简单。
  2. 数据相对均匀,不容易出现热点数据瓶颈问题。

缺点:

  1. 扩容时需要为老数据做数据迁移(使用一致性哈希算法可最大程度降低这个问题)。

在使用非分片列查询时候,没办法直接定位数据究竟存储在哪个库表中,需要做轮巡性能会很低。

3.2.1.2.1 映射关系法

使用非分片列进行查询,效率较低,需要全局轮巡,
此时我们可以创建一个映射关系表(或叫索引表),把非分片列与分片列做映射。
前提是非分片字段是唯一的,不能有重复值。

当使用非分片列查询时,先去映射表查询到对应的分片列的值,再用分片列的值进行路由。
索引表只有两列,可以承载很多数据,kv结构,数据量大的话可以继续做水平切分,
也可以把映射关系存储在分布式缓存中,提升性能。

缺点:多一次数据库或缓存的查询。
需要注意,采用映射关系法的非分片列的值变化后需要同步更新到映射表或缓存中。

3.2.1.2.2 基因法

将非分片列的值使用一个函数f随机生成一个3位的随机码,这个码相当于非分片列的值的基因。
函数f类似于:128bit = md5(非分片列的值),再截取最后3位。

再将基因放到分片列的后3位中,例如:分片列是64位整型,前61位跟原来一样,后3位放基因。
需要保证这3位基因可以起到路由库表的作用。

分片列的值 = 分片列的值(前61位) + f(非分片列的值)。

再将有基因的分片列插入数据库中,落到不同的库表中。
等回头用非分片列进行查询时,先用函数f得出3位基因,然后通过基因路由到对于的库表中。
(基因的长度是根据分库数量制定的,如果分库数量多则需要增加基因长度,不能再是3位)

当使用非分片列进行查询时,通过函数f进行计算得到分片列的值,再通过分片列的值进行路由。
需要注意,采用基因法的非分片列的值是不能更改的。


需要提前做好容量评估,预留基因长度:
%8的本质 -> 最后3个bit决定这行数据落在哪个库上。
uid%16的本质 -> 最后4个bit决定这行数据落在哪个库上。
uid%32的本质 -> 最后5个bit决定这行数据落在哪个库上。

3.2.1.3 主流水平切分场景

结合业务场景,一般有四种主流场景。

3.2.1.3.1 单key业务-用户表

user(uid,uname,pwd,age,create_time)

1%的使用场景-登录或按其他列查询:
WHERE uname = ? AND pwd = ?
WHERE age = ?

99%的使用场景-按主键查询详情:
WHERE uid = ?

结论:
使用uid作为分片字段,非分片字段查询则使用映射关系法或基因法。

3.2.1.3.2 一对多业务-评论表

comment(cid,uid,title,content,create_time)

10%的使用场景-查询用户的评论
WHERE uid = ?

90%的使用场景-按主键查询详情
WHERE cid = ?

使用一对多里的一方的主键做分片字段,即uid,
之后使用映射关系法来映射uid与cid的关系或基因法来通过uid来生成cid。
cid = 全局唯一id(前61位)+ uid的分库基因(后3位)

3.2.1.3.3 多对多业务-好友表

friend(uid,friend_uid,remark,create_time)

50%的使用场景-查询我的好友
WHERE uid = ?

50%的使用场景-查询加我好友用户
WHERE friend_uid = ?

使用数据冗余方案,多份数据使用多种分库手段。

3.2.1.3.4 多key业务-订单表

order(oid,buyer_id,seller_id,order_info)

80%的使用场景-按主键查询详情
WHERE oid = ?

19%的使用场景-查询用户的订单
WHERE buyer_id = ?

1%的使用场景-查询商户的订单
WHERE seller_id = ?

方案A:使用2+3
方案B:1%的请求采用多库查询

3.2.2 水平分库分表

通俗的说,将一个业务表中的数据,按照某些拆分规则,
拆到不同的数据库实例,不同的数据库,不同的表中。


4. 分布式后带来的问题

4.1 分布式事务

分布式锁,2pc,消息队列,最终一致性,柔性事务。

跨分片事务也分布式事务,想要了解分布式事务,就需要了解“XA接口”和“两阶段提交”。
值得提到的是,MySQL5.5x和5.6x中的xa支持是存在问题的,会导致主从数据不一致。
直到5.7x版本中才得到修复。

如果必须要使用一致性事务,那就会导致同时锁住多个节点的资源,导致并发量下降,
容易造成死锁。

对于数据一致性要求不高的场景可以使用消息队列,采用最终一致性的方式(事务补偿)。
例如:对数据对账,检查log,定期同步数据等方式。

Java应用程序可以采用Atomikos框架来实现XA事务(J2EE中JTA)。
感兴趣的读者可以自行参考《分布式事务一致性解决方案》,链接地址:
http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency

4.2 分布式join

分布式后,原本一个业务表的数据会散落在不同的库、表中,
那原来的join查询就会变得复杂,也会非常消耗性能,理论上应该避免分布式join查询。

注意:后台管理系统中的报表逻辑一般会join多张表,
这种玩法已经过时很久了,这种设计非常不合理。

可以借助离线分析、流式计算等解决方案。
下面列举了一些常见的解决方案。

4.2.1 增加冗余列

我们可以使用反范式增加冗余列来减少或消除join,提升性能,典型用空间换时间的玩法。

适用于读多写少以及冗余列较少的场景,
数据更新时需要同时更新冗余列数据,
这里还牵扯到一个问题,就是分布式场景下的数据一致性问题,
一般可以通过程序的业务代码来控制,细节暂不展开。

4.2.2 全局表

所谓全局表,就是有可能系统中所有模块都可能会依赖到的一些表。
比较类似我们理解的“数据字典”。为了避免跨库join查询,
我们可以将这类表在其他每个数据库中均保存一份。
同时,这类数据通常也很少发生修改(甚至几乎不会),
所以也不用太担心“一致性”问题。

当然,更一般的玩法是我们把数据字段表的数据放到分布式缓存中。

4.2.3 冗余关系表数据

在关系型数据库中,表之间往往存在一些关联的关系。如果我们可以先确定好关联关系,
并将那些存在关联关系的表记录存放在同一个分片上,那么就能很好的避免跨分片join问题。
在一对多关系的情况下,我们通常会选择按照数据较多的那一方进行拆分。

例如:假设a表与b表是一对多关系,a是一方,b是多方(b表中有a表的id)。

这样一来,我们就可以进行单节点的局部join,从而避免跨分片join,
但又引入了一个数据一致性的问题。

4.2.4 中间件或业务代码拼装或内存计算

简单场景下,我们可以借助数据库中间件或自己写业务代码的形式将不同库表的数据做拼装,
需要注意n+1次查询的问题。

如果是业务代码拼装,可以将第一次查询的结果存到一个集合中,
并将这个集合传给后面的查询方法做执行,之后再进行数据拼装。

也可以通过2次或多次查询的方式来消除join。
也可以通过spark的内存计算最终将结果返回。

4.3 分布式分页&排序&函数

一般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,
我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,
情况就会变得比较复杂了。

为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,
并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。

例如:业务逻辑要求查询前10条数据,数据分散在2个节点上。

从节点1取出前10条,从节点2取出前10条,
将这20条数据再进行排序,之后返回给上游最终结果。

但这也会有一个问题,假设要取出第100页的数据,
分别要把2个节点上的前100页数据都取出来,然后再进行排序,页数越大,性能越差。

函数处理也是同理,先在各个节点上执行一次,汇总后再执行一次,最后返回给上游。

4.4 分布式主键

在单库单表时代,我们往往会使用mysql的自增主键来自动生成主键id,
但在分库分表后,自增主键就没办法全局控制主键id的顺序性和唯一性了。

4.4.1 UUID

使用32位的UUID作为主键,理论上全局唯一,但需要主键为字符串类型,性能上没有整型好。

4.4.2 主键生成服务

需要启动类似于Ticket Server这样的服务,
每次生成id则需要调用服务来获取,需要考虑服务的高可用,
如果服务挂掉,则连累整个系统。

4.4.3 雪花算法

雪花算法(snowflake)生成64bit的长整型数据,
雪花算法是由Twitter公布的分布式主键生成算法,
由41位的timestamp+ 10位自定义的机器码+ 13位累加计数器组成,
它能够保证不同进程主键的不重复性,以及相同进程主键的有序性。
理论上可以使用到2086年。

4.4.4 全局主键表

建立一张MYISAM引擎的全局主键表,该表只有2个列,
分别为:主键列和表名列。主键设置为自增,
将表名列设置为唯一索引,可以同时为多张表生成主键。
MYISAM使用表级锁,不用担心并发读取时读取到同一个主键值。


5. 扩容后的数据迁移

在业务高速发展的过程中,往往需要进行软硬件升级或扩容的操作。
那就需要考虑到老数据的数据迁移问题。

这里需要考虑一个点,就是容量设计(根据QPS与数据量),
类似于设计多少个库多少个表能满足当前业务的数据量,细节暂不展开。

  1. 读出老数据,按新的分片规则将老数据写入各个节点的表中。
  2. 一致性哈希。
  3. 将老库的数据备份到新库中,在新库中删除不符合标准的数据。
    4.先使用范围落地,然后在每个范围内再进行哈希&取模运算拆分多个表。
    这样再扩容的时候就不用担心数据迁移问题,也不用担心热点数据问题。

6. mysql数据库中间件

如果要把上述的所有问题都自己解决,那真的是太慢太复杂了。
不要重复发明车轮子,网络上有很多现成的数据库中间件来为我们排忧解难。

中间件一般实现了特定数据库的网络通信协议,模拟一个真实的数据库服务,
屏蔽了后端真实的Server,应用程序通常直接连接中间件即可。

而在执行SQL操作时,中间件会按照预先定义分片规则,对SQL语句进行解析、路由,
并对结果集做二次计算再最终返回。

引入数据库中间件的技术成本更低,对应用程序来讲侵入性几乎没有,
可以满足大部分的业务。增加了额外的硬件投入和运维成本,
同时,中间件自身也存在性能瓶颈和单点故障问题,需要能够保证中间件自身的高可用、可扩展。

使用中间件会或多或少的影响系统性能,所以不要过度设计。

常见的数据库中间件:
mysql_proxy(mysql官方)
TDDL(阿里)
sharding-jdbc(当当,已捐给阿帕奇基金会)
mycat(阿里的cobar上改进)
Atlas(360)


7. sharding-jdbc

sharding-jdbc定位为轻量级数据库中间件Java框架,
在Java的JDBC层提供的额外服务,0代码入侵,只需要配置文件即可。

它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,
可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。

适用于任何基于Java的ORM框架,
如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。

基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL

sharding-jdbc版本:
1.x com.dangdang
2.x io.shardingjdbc
3.x io.shardingsphere
4.x org.apache.shardingsphere


8.总结

在数据量大、访问量大的前提下,
我们可以使用一主多从+分库分表结合的方式来构建成多库多表的分布式数据库结构,
来统一对外提供数据服务。

将mysql进行分库分表拆分后,一般会尽量不使用或减少:
1.关联查询和子查询
2.自定义函数+存储过程+触发器
3.事务
4.外键

  • 0
    点赞
  • 0
    评论
  • 3
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值