阿里架构师讲面试:分布式DB

959 阅读15分钟

前面介绍了分布式系统下的两大主题:分布式服务管理和分布式流量管理。从本节开始介绍分布式数据管理部分的系统化知识体系。

为什么要引入分布式数据管理?我们可以从业务场景来看:超高的业务并发;越来越挑剔的客户(系统快速响应,因此数据访问也必须要快);业务量越来越大带来的海量数据积累以及数据稳定性要求越来越高...... 这些都是当前应用面临的实际业务问题。如何解这些问题,就引入了我们本节要讲的分布式数据管理。

分布式数据管理,本质上就是针对高并发,海量数据场景下的数据存储和访问进行管理,核心管理需求其实就是四个:数据的高并发访问,数据的高性能访问,数据服务高可用,以及如何保障分布式环境下的数据一致性。因此,本节将从分布式DB,分布式缓存,分布式数据一致性(包括分布式事务和分布式锁)三个模块进行系统介绍。

分布式DB

数据库分布式核心内容无非就是数据切分(Sharding),以及切分后对数据的定位、及返回结果的数据整合。

读写分离

一主挂多从,写入操作路由到主库,数据写入后,该数据由主库同步到从库。读操作路由到从库。

为什么引入读写分离

单库场景下,会因为写入时数据加写锁操作降低读并发。适用场景:

1.高并发,且读远高于写场景。

2.业务不要求强数据一致性。

主从同步原理

1、主库将对数据库实例的变更记录到binlog中。

2、主库会有线程实时监测binlog的变更并将这些新的events推给从库<广播>

3、从库io线程接收这些events,并将其记录入relaylog。

4、从库sql线程读取relaylog的events,并将这些events重放到从库实例。

这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。

而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。

所以 MySQL 实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题;一个是并行复制,用来解决主从同步延时问题。

这个所谓半同步复制,也叫 semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。

所谓并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

主从同步引入问题

数据丢失问题

mysql同步复制策略。即数据写入主库后,主库向从库同步binlog,从库收到binlog后记录,并ACK主库。当主库至少收到一个ACK消息时,才确认写入成功,返回成功状态给客户。

MySQL 主从同步延时问题

以前线上确实处理过因为主从同步延时问题而导致的线上的 bug,属于小型的生产事故。

是这个么场景。有个同学是这样写代码逻辑的。先插入一条数据,再把它查出来,然后更新这条数据。在生产环境高峰期,写并发达到了 2000/s,这个时候,主从复制延时大概是在小几十毫秒。线上会发现,每天总有那么一些数据,我们期望更新一些重要的数据状态,但在高峰期时候却没更新。用户跟客服反馈,而客服就会反馈给我们。

我们通过 MySQL 命令:

show status

查看 Seconds_Behind_Master,可以看到从库复制主库的数据落后了几 ms。

一般来说,如果主从延迟较为严重,有以下解决方案:

  • 分库,将一个主库拆分为多个主库,每个主库的写并发就减少了几倍,此时主从延迟可以忽略不计。
  • 打开 MySQL 支持的并行复制,多个库并行复制。如果说某个库的写入并发就是特别高,单库写并发达到了 2000/s,并行复制还是没意义。
  • 重写代码,写代码的同学,要慎重,插入数据时立马查询可能查不到。
  • 确实是存在必须先插入,立马要求就查询到的场景,推荐主库写入后新数据刷tair,设置缓存失效时间,客户查询数据先查缓存,缓存存在直接返回,不存在查询从库。(空间换时间)
  • 如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询设置直连主库。不推荐这种方法,你要是这么搞,读写分离的意义就丧失了。

数据分片

为什么要数据切分

先尽量做索引、缓存、读写分离的优化,后期业务发展迅速,数据量到单表瓶颈时,再考虑分库分表。阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。

  • 单表数据量过大,mysql引擎效率降低。
  • 正常运维(比如ddl执行,数据迁移的场景,会锁表),如果单表存储海量数据,会导致锁表时间过长,影响业务访问。
  • 降低锁等待概率。
  • 降低数据库宕机对业务影响面

此外,数据库拆分后,物理资源也随之拆分,单台数据库服务器面临的数据库连接,网络带宽和硬件资源竞争也会降低。

数据切分方案

垂直切分

  • 垂直分库

按照业务内聚性划分子域。外汇交易域划分为敞口域和交易域。

  • 垂直分表

表字段过多,不常用的字段划分到suppmental表中。

部分字段被高频update(拆出来,有助于降低update操作io)

垂直切分的优点:

  • 解决业务系统层面的耦合,业务清晰
  • 与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等
  • 高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈

缺点:

  • 部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度
  • 分布式事务处理复杂
  • 依然存在单表数据量过大的问题(需要水平切分)

水平切分

十库百表,按照hash算法做负载均衡。常见的分片规则:

  • 顺序分片。比如日期时间,用户名。这样的优点在于:

  • 单表大小可控

  • 天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移

  • 使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题。

缺点:

  • 热点数据成为性能瓶颈。连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询

  • 哈希分片。

优点:

  • 数据分片相对比较均匀,不容易出现热点和并发访问的瓶颈

缺点:

  • 后期分片集群扩容时,需要迁移旧的数据(使用一致性hash算法能较好的避免这个问题)
  • 容易面临跨分片查询的复杂问题。比如上例中,如果频繁用到的查询条件中不带cusno时,将会导致无法定位数据库,从而需要同时向4个库发起查询,再在内存中合并数据,取最小集返回给应用,分库反而成为拖累。

数据迁移扩容问题

顺序分片技术方案下数据扩容很简单,直接加分片就行。

hash算法也会面临容错性和扩展性的问题。容错性是指当系统中的某个服务出现问题时,不能影响其他系统。扩展性是指当加入新的服务器后,整个系统能正确高效运行。容错或扩容场景下,则需要对历史数据根据哈希算法进行重新定位迁移。难度很高,一般不建议。可以想象一下,海量数据背景下,对所有历史数据进行重新定位是一件极其有挑战的技术工作!

我们公司新建应用向中间件同学申请DB资源时,总是建议根据业务应用未来发展多申请空间,否则业务发展起来后再进行扩容又将一件耗时耗力且很危险的工作。

当然,采用一致性哈希算法可以在很大程度上缓解上述问题。扩容,下线机器场景下,只影响一小部分数据,不需要对所有历史数据进行重新hash定位。一致性哈希算法也适用分布式缓存场景下的多分片扩容缩容问题。

一致性哈希算法

随着业务的扩展,流量的剧增,单体项目逐渐划分为分布式系统。对于经常使用的数据,我们可以使用Redis作为缓存机制,减少数据层的压力。因此,重构后的系统架构如下图所示:

优化最简单的策略就是,把常用的数据保存到Redis中,为了实现高可用使用了3台Redis(没有设置集群,集群至少要6台)。每次Redis请求会随机发送到其中一台,但是这种策略会引发如下两个问题:

  • 同一份数据可能在多个Redis数据库,造成数据冗余
  • 某一份数据在其中一台Redis数据库已存在,但是再次访问Redis数据库,并没有命中数据已存在的库。无法保证对相同的key的所有访问都发送到相同的Redis中

要解决上述的问题,我们需要稍稍改变一些key存入Redis的规则:使用hash算法

例如,有三台Redis,对于每次的访问都可以通过计算hash来求得hash值。

如公式 h=hash(key)%3,我们把Redis编号设置成0,1,2来保存对应hash计算出来的值,h的值等于Redis对应的编号。

但是hash算法也会面临容错性和扩展性的问题。容错性是指当系统中的某个服务出现问题时,不能影响其他系统。扩展性是指当加入新的服务器后,整个系统能正确高效运行。

现假设有一台Redis服务器宕机了,那么为了填补空缺,要将宕机的服务器从编号列表中移除,后面的服务器按顺序前移一位并将其编号值减一,此时每个key就要按h = Hash(key) % 2重新计算。

同样,如果新增一台服务器,规则也同样需要重新计算,h = Hash(key) % 4。因此,系统中如果有服务器更变,会直接影响到Hash值,大量的key会重定向到其他服务器中,造成缓存命中率降低,而这种情况在分布式系统中是十分糟糕的。

一个设计良好的分布式哈希方案应该具有良好的单调性,即服务节点的变更不会造成大量的哈希重定位。一致性哈希算法由此而生。

“一致哈希”是一种特殊的哈希算法。在使用一致哈希算法后,哈希表槽位数(大小)的改变平均只需要对 K/n 个关键字重新映射,其中K是关键字的数量, n是槽位数量。然而在传统的哈希表中,添加或删除一个槽位的几乎需要对所有关键字进行重新映射。

简单的说,一致性哈希是将整个哈希值空间组织成一个虚拟的圆环,如假设哈希函数H的值空间为0-2^32-1(哈希值是32位无符号整形),整个哈希空间环如下:

整个空间按顺时针方向组织,0和2^32-1在零点中方向重合。接下来,把服务器按照IP或主机名作为关键字进行哈希,这样就能确定其在哈希环的位置。

然后,我们就可以使用哈希函数H计算值为key的数据在哈希环的具体位置h,根据h确定在环中的具体位置,从此位置沿顺时针滚动,遇到的第一台服务器就是其应该定位到的服务器。

例如我们有A、B、C、D四个数据对象,经过哈希计算后,在环空间上的位置如下:

根据一致性哈希算法,数据A会被定为到Server 1上,数据B被定为到Server 2上,而C、D被定为到Server 3上。

容错性和扩展性

那么使用一致性哈希算法的容错性和扩展性如何呢?

  • 容错性

假如RedisService2宕机了,那么会怎样呢?

那么,数据B对应的节点保存到RedisService3中。因此,其中一台宕机后,干扰的只有前面的数据(原数据被保存到顺时针的下一个服务器),而不会干扰到其他的数据。

  • 扩展性

下面考虑另一种情况,假如增加一台服务器Redis4,具体位置如下图所示:

原本数据C是保存到Redis3中,但由于增加了Redis4,数据C被保存到Redis4中。干扰的也只有Redis3而已,其他数据不会受到影响。

因此,一致性哈希算法对于节点的增减都只需重定位换空间的一小部分即可,具有较好的容错性和可扩展性。

虚拟节点

前面部分都是讲述到Redis节点较多和节点分布较为均衡的情况,如果节点较少就会出现节点分布不均衡造成数据倾斜问题。

例如,我们的的系统有两台Redis,分布的环位置如下图所示:

这会产生一种情况,Redis1的hash范围比Redis2的hash范围大,导致数据大部分都存储在Redis1中,数据存储不平衡。

为了解决这种数据存储不平衡的问题,一致性哈希算法引入了虚拟节点机制,即对每个节点计算多个哈希值,每个计算结果位置都放置在对应节点中,这些节点称为虚拟节点

具体做法可以在服务器IP或主机名的后面增加编号来实现,例如上面的情况,可以为每个服务节点增加三个虚拟节点,于是可以分为 RedisService1#1、 RedisService1#2、 RedisService1#3、 RedisService2#1、 RedisService2#2、 RedisService2#3,具体位置如下图所示:

对于数据定位的hash算法仍然不变,只是增加了虚拟节点到实际节点的映射。例如,数据C保存到虚拟节点Redis1#2,实际上数据保存到Redis1中。这样,就能解决服务节点少时数据不平均的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布

参考:segmentfault.com/a/119000001…

参考:www.cnblogs.com/butterfly10…

觉得有收获的话帮忙点个赞吧,让有用的知识分享给更多的人

## 欢迎关注掘金号:五点半社

## 欢迎关注微信公众号:五点半社(工薪族的财商启蒙)##