分布式架构——数据访问层的设计分析

(一)数据库从单机到分布式的挑战和应对

(1)数据库减压解决方式

1:升级单机硬件,垂直扩展
2:优化应用层面,减低数据库访问压力
3:引入缓存、添加数据搜索引擎,减少数据库读压力
4:数据库的数据和访问分到多台数据库上,水平扩展

(2)数据水平划分和垂直划分分析

定义:
垂直划分:将一个数据库中不同业务单元的数据分到不同的数据库里面,根据业务拆分,有一定的数据独立性,比如用户数据库,订单库
水平划分:根据一定的规则把同一业务单元的数据拆分到多个数据库中。根据规则划分,水平划分的并集构成一个业务数据的总和

垂直划分问题分析

1、单机的 ACID保证被打破了。数据到了多机后,原来在单机通过事务来进行的处理逻辑会受到很大的影响。我们面临的选择是,要么放弃原来的单机事务,修改实现,要么引人分布式事务。

2、一些Join操作会变得比较困难,因为数据可能已经在两个数据库中了,所以不能很方便地利用数据库自身的Join了,需要应用或者其他方式来解决。·靠外键去进行约束的场景会受影响。

水平划分问题分析
1:同样有可能有ACID被打破的情况。
2:同样有可能有Join操作被影响的情况。
3:靠外键去进行约束的场景会有影响。
4:依赖单库的自增序列生成唯一ID会受影响。
5:针对单个逻辑意义上的表的查询要跨库了。

(二)分布式事务

(1)分布式事务模型与规范

1:X/OPen组织即The Open Group组织提出一个分布式事务的规范——XA
2:X/Open组织定义了分布式事务处理模型——X/OPen DTP模型
3:DTP模型中三个组件:AP、RM、TM

事务:一个事务是一个完整的工作单元,由多个独立的计算任务组成,这多个任务在逻辑上是原子的。

全局事务:一次性操作多个RM资源管理器的事务就是全局事务

分支事务:在全局事务中,每一个资源管理器有自己独立的任务,这些任务的集合是这个资源管理器的分支任务

控制线程:用来表示一个工作线程,主要是关联AP,TM,RM三者的线程,也就是事务上下文环境。即用来标识全局事务和分支事务关系的线程。

AP:Application Program (AP),即应用程序,可以理解为使用DTP模型的程序。它定义了事务边界,并定义了构成该事务的应用程序的特定操作。

RM:Resource Manager (RM),资源管理器,可以理解为一个DBMS系统,或者消息服务器管理系统。应用程序通过资源管理器对资源进行控制,资源必须实现XA定义的接口。资源管理器提供了存储共享资源的支持。

TM:Transaction Manager ™,事务管理器,负责协调和管理事务,提供给AP应用程序编程接口并管理资源管理器。事务管理器向事务指定标识,监视它们的进程,并负责处理事务的完成和失败。事务分支标识(称为XID)由TM指定,以标识一个RM内的全局事务和特定分支。它是TM中日志与RM中日志之间的相关标记。两阶段提交或回滚需要XID,以便在系统启动时执行再同步操作(也称为再同步(resync)),或在需要时允许管理员执行试探操作(也称为手工干预)。
在这里插入图片描述
说明:
AP和RM是一定需要的,而TM是我们额外引入的。之所以引入TM,是因为在分布式系统中,俩台机器理论是无法达到一致的状态,需要引入一个单点进行协调。事务管理器控制着全局事务,管理者事务的生命周期,并协调资源。

(2)两阶段提交2PC

两阶段提交协议,即2PC,Two Phase Commitment Protocol。之所以称为两阶段提交,是相对于单库的事务提交方式来说的。我们在单库上完成相关的数据操作后,就会直接提交或者回滚,而在分布式系统中,在提交之前增加了准备的阶段,所以称为两阶段提交。

第一阶段准备
第二阶段提交或者回滚

在实际当中,由于TM事务管理器自身的稳定性、可用性的影响,以及网络通信中可能产生的问题,出现的情况会复杂很多。此外,事务管理器在多个资源之间进行协调,它自身要进行很多日志记录的工作。网络上的交互次数的增多以及引人事务管理器的开销,是使用两阶段提交协议使分布式事务的开销增大的两个方面。
因此,在进行垂直拆分或者水平拆分后,需要想清楚是否一定要引入两阶段的分布式事务,在必要的情况下才建议使用。

(3)CAP和BASE

CAP理论是Eric Brewer在2000年7月份的PODC会议上提出的,CAP的涵义如下。

Consistency: all nodes see the same data at the same time,即所有的节点在同一时间读到同样的数据。这就是数据上的一致性(用C表示),也就是当数据写入成功后,所有的节点会同时看到这个新的数据。

Availability: a guarantee that every request receives a response about whether itwas successful or failed,保证无论是成功还是失败,每个请求都能够收到一个反馈。这就是数据的可用性(用A表示),这里的重点是系统一定要有响应。

Partition-Tolerance: the system continues to operate despite arbitrary message lossor failure of part of the system,即便系统中有部分问题或者有消息的丢失,但系统仍能够继续运行。这被称为分区容忍性(用Р表示),也就是在系统的一部分出现问题时,系统仍能继续工作。

但是,在分布式系统中并不能同时满足上面三项。我们可以选择其中两个来提升,而另外一个则会受到损失。那么,在进行系统设计和权衡时,其实就是在选择CA、AP或是CP。

选择CA,放弃分区容忍性,加强一致性和可用性。这其实就是传统的单机数据库的选择。
选择AP,放弃一致性,追求分区容忍性及可用性。这是很多分布式系统在设计时的选择,例如很多NoSQL 系统就是如此。
选择CP,放弃可用性,追求一致性和分区容忍性。这种选择下的可用性会比较低,网络的问题会直接让整个系统不可用。

在分布式系统中,我们一般还是选择加强可用性和分区容忍性而牺牲一致性(AP)。当然,这里所讲的并不是不关心一致性,而是首先满足和P,然后看如何解决C的问题。

我们再来看看BASE模型,BASE涵义如下。
Basically Available:基本可用,允许分区失败。
Soft state:软状态,接受一段时间的状态不同步。
Eventually consistent:最终一致,保证最终数据的状态是一致的。

当我们在分布式系统中选择了CAP中的A和P后,对于C,我们采用的方式和策略就是保证最终一致,也就是不保证数据变化后所有节点立刻一致,但是保证它们最终是一致的。在大型网站中,为了更好地保持扩展性和可用性,一般都不会选择强一致性,而是采用最终一致的策略来实现。

(三)Paxos协议

(1)基本认识

1:使用Paxos协议有一个前提,那就是不存在拜占庭将军问题。拜占庭将军问题是一个没有办法保证可信的通信环境的问题,Paxos的前提是有一个可信的通信环境,也就是说信息都是准确的,没有被篡改。

2:Paxos算法的提出过程是,虚拟了一个叫做Paxos的希腊城邦,并通过议会以决议的方式介绍 Paxos 算法。

3:Paxos 算法首先把议员的角色分为了Proposers、Acceptors和 Learners,议员可以身兼数职

Proposers:提出议案者,就是提出议案的角色。
Acceptors:收到议案后进行判断的角色。Acceptors收到议案后要选择是否接受(Accept)议案,若议案获得多数Acceptors的接受,则该议案被批准(Chosen)。
Learners:只能“学习”被批准的议案,相当于对通过的议案进行观察的角色。

4:相关名词
Proposal:议案,由Proposers提出,被Acceptors批准或否决。
Value:决议,议案的内容,每个议案都是由一个{编号,决议}对组成。

5:决议( Value)只有在被Proposers提出后才能被批准(未经批准的决议称为“议案(Proposal)”)。
在Paxos算法的执行实例中,一次只能批准 (Chosen)一个 Value。Learners只能获得被批准(Chosen)的Value。

(2)Basic-Paxos基本流程

1:准备阶段Prepare
Proposer提出一个提案,编号为N,此N大于这个Proposer之前提出的提案编号。请求Acceptors的quornm接收。

2:Promise
如果N大于此Accpetor之前接收的任何提案编号则接收,否则拒绝

3:Accept
如果达到了多数派,proposer会发出accept请求,此请求包含提案编号以及内容

4:如果此Acceptor在此期间没有任何大于N的提案,则接收此提案内容,否则忽略

详细的Paxos算法过程:
https://zh.wikipedia.org/wiki/Paxos%E7%AE%97%E6%B3%95

说明:
1:Paxos的核心原则就是少数服从多数
2:Paxos中如果同时有多人提出议案的话,可能会出现碰撞失败,然后双方都需要增加议案的编号再提交的过程。而再次提交仍然存在编号冲突,因此双方都需要再增加编号去提交,这就会出现活锁。解决的办法是在整个集群当中设置一个Leader,所有的议案都由它来提,这样就可以避免这种冲突了。这其实就是把提案的工作变成一个单点,而引发的新问题是如果这个Leader出问题了该如何处理,那就需要在选一个Leader出来。

(四)多机的Sequence问题与处理

当转变为水平分库时,原来单库中的Sequence 及自增Id的做法需要改变。在大家比较熟悉的Oracle里,提供对Sequence 的支持;在 MySQL里,提供对Auto Increment字段的支持,我们都能很容易地实现一个自增的不重复Id的序列。在分库分表后,这就成了一个难题。我们可以从下面两个方向来思考和解决这个问题:(唯一性和连续性)

如果我们只是考虑ld的唯一性的话,那么可以参考UUID的生成方式,或者根据自己的业务情况使用各个种子(不同维度的标识,例如IP、MAC、机器名、时间、本机计数器等因素)来生成唯一的ld。这样生成的ld虽然保证了唯一性,但在整个分布式系统中的连续性不好。

这里说的连续性是指在整个分布式环境中生成的Id的连续性。在单机环境中,其实就是一个单点来完成这个任务,在分布式系统中,我们可以用一个独立的系统来完成这个工作。

(五)多机的数据查询问题分析

(1)Join问题

在分库后,之前的一些Join操作需要进行修改,如果需要Join的数据已经分布在多个库了,那就需要完成跨库的Join操作,这会比较麻烦,解决的思路有如下几种
1:将原来一个Join的联合查询在应用层面转换为多个步骤操作,先根据XX查询到XX,然后在根据前面的XX查询后面XXX。。。
2:适当的添加冗余信息,即把一些需要多表查询的字段信息,放在一张表里,冗余字段,减少过多的多表联合查询
3:借助外部系统(例如搜索引擎)解决一些跨库的问题

(2)外键约束

外键约束的问题比较难解决,不能完全依赖数据库本身来完成之前的功能了。如果要对分库后的单库做外键约束,就要求分库后每个单库的数据是内聚的,否则就只能靠应用层的判断、容错等方式了。

(3)合并查询

这个场景和前面的跨库Join还不同,跨库Join是在不同的逻辑表之间的Join,在分库后这些Join可能需要跨多个数据库,而合并查询是针对一个逻辑表的查询操作,但因为物理上分到了多个库多个表,因而产生了数据的合并查询。

考虑方面:
1:排序:多个数据源查询出来后,在应用层进行排序
2:函数处理:即使用Max、Min、Sum、Count等函数对多个数据来源的值进行相应的函数处理。
3:求平均值,从多个数据来源进行查询时,需要把SQL 改为查询Sum和Count,然后对多个数据来源的Sum求和、Count求和后,计算平均值,这是需要注意的地方。
4:非排序分页,这需要看具体实现所采取的策略,是同等步长地在多个数据源上分页处理,还是同等比例地分页处理。同等步长的意思是,分页的每页中,
来自不同数据源的记录数是一样的;同等比例的意思是,分贝的母贝中,米日个同数据源的数据数占这个数据源符合条件的数据总数的比例是一样的。

(六)数据访问层的设计与实现

数据访问层就是方便应用进行数据读/写访问的抽象层,我们在这个层上解决各个应用通用的访问数据库的问题。在分布式系统中,我们也把数据访问层称为分布式数据访问层,有时也简称为数据层。

(1)对外提供数据访问层的方式

第一种方式是为用户提供专有API,不过这种方式是不推荐的,它的通用性很差,甚至可以说没有通用性。一般来说采用这种方式是为了便于实现功能,或者这种方式对一些通用接口方式有比较大的改动和扩展。

第二种方式是通用的方式。在Java应用中一般是通过JDBC方式连接数据库,数据层自身可以作为一个 JDBC的实现,也就是暴露出JDBC的接口给应用,这时应用的使用成本就很低了,和使用远程数据库的JDBC驱动的方式是一样的,迁移成本也非常低。

还有一种方式是基于ORM或类ORM接口的方式,可以说这种方式介于上面两种方式之间。应用为了开发的高效和便捷,在使用数据库时一般会使用ORM
或类ORM框架,例如 iBatis、 hibernate, Spring JDBC等,我们可以在自己应用使用的ORM框架上再包装一层,用来实现数据层的功能,对外暴露的仍然是原来框架的接口。这样的做法对于某些功能来说实现成本比较低,并且在兼容性方面有一定的优势,例如原来系统都用iBatis 的话,对于应用来说,iBatis之上的封装就比较透明了。

在这里插入图片描述

此外,使用ORM/类 ORM框架可能会有一些框架自身的限制带来困难。例如,使用iBatis的同时想去动态改动SQL就会比较困难,而这在直接基于JDBC 驱动方式的实现中就没有那么困难。

(2)按照数据层流程的顺序看数据层设计

在这里插入图片描述
【SQL解析阶段的处理】
1:对SQL支持的程度,是否需要支持所有的SQL,这需要根据具体的场景来做决定
2:支持多少SQL的方言,对于不同厂商超出标准SQL的部分要支持多少,这需要根据具体的场景来做决定
3:在进行SQL解析过程中,对于解析的缓存可以提升解析速度
4:通过SQL解析可以得到SQL中的关键信息,例如表名,字段,where条件等。而在数据层中,一个很重要的事情是根据执行的SQL得到被操作的表,根据参数及规则来确定目标数据源连接

【规则处理阶段】
1:采用固定哈希算法作为规则。
常见的方式是根据表的某个字段(用户id)取模,然后将数据分散到不同的数据库和表中。这种方式对于业务固定且不经常扩展的系统可以接受,如果对于业务复杂且需要后面扩展的系统来说不友好

2:采用一致性哈希算法。
一致性哈希所带来的最大变化是把节点对应的哈希值变为了一个范围,而不再是离散的。在一致性哈希中,我们会把整个哈希值的范围定义得非常大,然后把这个范围分配给现有的节点。如果有节点加入,那么这个新节点会从原有的某个节点上分管一部分范围的哈希值;如果有节点退出,那么这个节点原来管理的哈希值会给它的下一个节点来管理。假设哈希值范围是从0到100,共有四个节点,那么它们管理的范围分别是[0,25)、[25,50)、[50,75)、[75,100]。如果第二个节点退出那么剩下节点管理的范围就变为[0,25)、[25,75)、[75,100],可以看到,第一个和第四个节点管理的数据没影响,而第三个节点原来所管理的数据也没有影响,只需要把第二个节点负责的数据接管过来就行了。如果是增加一个节点,例如在第二个和第三个节点之间增加一个,则这五个节点所管理的范围变为[0,25)、[25,50)、[50,63)、[63,75)、[75,100],可以看到,第一个、第二个、第四个节点没有受影响,第三个节点有部分数据也没受影响,另一部分数据要给新增的节点来管理。

在这里插入图片描述

问题:
新增一个节点时,陈了新增的节点外,只有一个节点受影响,这个新增节点和受影响的节点的负载明显比其他节点低的;减少一个节点时,除了减去的节点外,只有一个节点受影响,它要承担自己原来的和减去的节点的工作,压力明显比其他节点要高。这似乎要增加一倍节点或减去一半节点才能保持各个节点的负载均衡。如果这样的话,一致性哈希的优势就不明显了。

3:虚拟节点对一致性哈希的改进
为了应对上述问题,我们引入虚拟节点的概念。即4个物理节点可以变为很多个虚拟节点,每个虚拟节点支持连续的哈希环上的一段。而这时如果加入一个物理节点,就会相应加入很多虚拟节点,这些新的虚拟节点是相对均匀地插入到整个哈希环上的,这样,就可以很好地分担现有物理节点的压力了﹔如果减少一个物理节点,对应的很多虚拟节点就会失效,这样,就会有很多剩余的虚拟节点来承担之前虚拟节点的工作,但是对于物理节点来说,增加的负载相对是均衡的。所以可以通过一个物理节点对应非常多的虚拟节点,并且同一个物理节点的虚拟节点尽量均匀分布的方式来解决增加或减少节点时负载不均衡的问题。

【改写SQL】

1:不同库的表名,单个库分表后的表名标准
2:分库分表后的索引名修改

(七)读写分离的挑战和应对

(1)介绍

在这里插入图片描述

上图所示是一个常见的应用使用读写分离的场景。通过读写的分离,可以分担主库(Master)的读的压力。这里面存在一个数据复制的问题,也就是把主库的数据复制到备库(Slave)去。

(2)数据结构相同,多从库对应一主库的场景

1:相对于主从部署结构较为单一的方式可以使用数据库系统提供的同步机制,比如MySQL的Replication可以解决复制的问题,并且延迟也相对较小

在这里插入图片描述
2:如果遇到类似上图所示的主库是一个相对复杂的组合结构部署的方式,那么该如何处理呢?
应用通过数据层访问数据库,通过消息系统就数据库的更新送出消息通知,数据同步服务器获得消息通知后会进行数据的复制工作。分库规则配置则负责在读数据及数据同步服务器更新分库时让数据层知道分库规则。数据同步服务器和DB主库的交互主要是根据被修改或新增的数据主键来获取内容,采用的是行复制的方式。
可以说这是一个不优雅但是能够解决问题的方式。比较优雅的方式是基于数据库的日志来进行数据的复制。

(3)主/备库分库方式不同的数据复制

数据库复制在读写分离中是一个比较关键的任务。一般情况下进行的是对称的复制,也就是镜像,但是也会有一些场景进行非对称复制。这里的非对称复制是指源数据和目标数据不是镜像关系,也指源数据库和目标数据库是不同的实现。这个时候我们就不能简单的数据复制,需要控制数据的分发,针对实际的业务场景进行一些处理。

(4)引入数据变更平台

复制到其他数据库是数据变更的一种场景,还有其他场景也会关心数据的变更,例如搜索引擎的索引构建、缓存的失效等。我们可以考虑构建一个通用的平台来管理和控制数据变更。

(5)如何做到数据平滑迁移

对数据库做平滑迁移的最大挑战是,在迁移的过程中又会有数据的变化。可以考虑的方案是,在开始进行数据迁移时,记录增量的日志,在迁移结束后,再对增量的变化进行处理。在最后,可以把要被迁移的数据的写暂停,保证增量日志都处理完毕后,再切换规则,放开所有的写,完成迁移工作。

参考流程步骤:
1:首先我们确定要开始扩容,并且开始记录数据库的数据变更的增量日志

在这里插入图片描述

这时增量日志和新库表都还是空的。我们用id来标识记录,用v标识版本号(这不是数据库表的业务字段,而是我们为了讲清楚平滑迁移过程而加上的标志)。

2:接下来,数据开始复制到新库表,并且也有更新进来

在这里插入图片描述
可以看到, id=1和 id=3的数据已经在新库表中了,但是 id=1的记录版本是旧的,而id=3的记录版本已经是新的了。当我们把源库表中的数据全部复制到新库表中后,一定会出现的情况是,由于在复制的过程中会有变化,所以新库表中的数据不全是最新的数据。

3:当全量迁移结束后,我们把增量日志中的数据也进行迁移

在这里插入图片描述

可以发现,这个做法并不能够保证新库表的数据和源库表的数据一定是一致的,因为我们处理增量日志时,还会有新的增量日志进来,这是一个逐渐收敛的过程。
4:然后我们进行数据比对,这时可能会有新库数据和源库数据不同的情况,把它们记录下来。
5:接着我们停止源数据库中对于要迁移走的数据的写操作,然后进行增量日志的处理,以使得新库表的数据是新的。
6:最后更新路由规则,所有新数据的读或写就到了新库表,这样就完成了整个迁移过程。

猜你喜欢

转载自blog.csdn.net/Octopus21/article/details/109956587