《数据密集型应用系统设计》读书笔记——第二部分 分布式数据系统(一)

第二部分 分布式数据系统

前面我们讨论了数据系统的各个⽅方⾯面,但仅限于数据存储在单台机器上的情况。现在我们到了第二部分,进⼊更高的层次,并提出⼀个问题:如果多台机器参与数据的存储和检索,会发⽣什么?
你可能会出于各种各样的原因,希望将数据库分布到多台机器上:
可扩展性
  如果你的数据量、读取负载、写入负载超出单台机器的处理能力,可以将负载分散到多台计算机上。
容错与高可⽤性
  如果你的应用需要在单台机器(或多台机器,网络或整个数据中⼼)出现故障的情况下仍然能继续工作,则可使用多台机器,以提供冗余。⼀台故障时,另一台可以接管。
延迟
  如果在世界各地都有用户,你也许会考虑在全球范围部署多个服务器,从⽽每个用户可以从地理上最近的数据中心获取服务,避免了等待网络数据包穿越半个世界。

系统扩展能力

当负载增加需要更强的处理能力时,最简单的方法就是购买更强大的机器(有时称为垂直扩展或向上扩展(scale up))。许多处理器,内存和磁盘可以在同一个操作系统下相互连接,快速的相互连接允许任意处理器访问内存或磁盘的任意部分。在这种共享内存架构 (shared-memory architecture)中,所有的组件都可以看作⼀台单独的机器。
共享内存⽅法的问题在于,成本增长速度快于线性增⻓:一台有着双倍处理器数量,双倍内存⼤小,双倍磁盘容量的机器,通常成本会远超过原来的两倍。⽽且可能因为存在瓶颈,并不足以处理双倍的载荷。
共享内存架构可以提供有限的容错能力,高端机器可以使⽤热插拔的组件(不关机更换磁盘,内存模块,甚⾄处理器)——但它必然囿于单个地理位置的桎梏。
另一种⽅法是共享磁盘架构(shared-disk architecture),它使⽤多台具有独立处理器和内存的机器,但将数据存储在机器之间共享的磁盘阵列上,这些磁盘通过快速⽹连接。这种架构多适用于数据仓库等负载,但竞争和锁定的开销限制了共享磁盘方法的可扩展性。

无共享架构

相⽐之下,无共享架构(shared-nothing architecture)(有时称为水平扩展(horizontal scale))或向外扩展(scale out))已经相当普及。在这种架构中,运⾏数据库软件的每台机器/虚拟机都称为节点(node)。每个节点只使用各自的处理器,内存和磁盘。节点之间的任何协调,都是在软件层⾯使用传统网络实现的。
无共享系统不需要使用特殊的硬件,所以可以用任意机器——比如性价比最好的机器。也可以跨多个地理区域分布数据从而减少用户延迟,或者在损失一整个数据中心的情况下幸免于难。随着云计算拟机部署的出现,即使是小公司,也可以实现异地分布式架构。
虽然分布式无共享架构有许多优点,但它通常也会给应用带来额外的复杂度,有时也会限制可⽤数据模型的表达力。

复制与分区

将数据分布在多个节点时有两种常见的方式:

  • 复制:在多个节点上保存相同数据的副本,每个副本具体的存储位置可能不尽相同。复制方法可以提供冗余:如果某些节点发生不可用,则可以通过其他节点继续提供服务。复制也能提供系统性能。
  • 分区:将一个大块头的数据库拆分为多个较小的子集即分区。不同的分区分配给不同的节点(也成为分片)。

这些是不同的数据分布机制,但经常被放在一起组合使用。
在这里插入图片描述

第5章 数据复制

复制主要指通过互联网络在多台机器上保存相同数据的副本。通过数据复制方案,人们通常希望达到以下目的:

  • 使数据在地里位置上更接近用户,从而降低访问延迟
  • 当部分组件出现故障时,系统依然可以正常的工作,从而提高可用性
  • 扩展至多台机器以同时提供数据访问服务,从而提高吞吐量。

主节点与从节点

每个保存数据库完整数据集的节点称之为副本。对于每一笔数据的写入,所有副本都需要随之更新;否则,某些副本将出现不一致。最常见的解决方案是基于主节点的复制(也称为主动/被动,或主从复制)。如下图所示:
在这里插入图片描述

同步复制与异步复制

数据复制通常分为同步复制与异步复制两种。
同步复制的优点是,从库保证有与主库一致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上找到。缺点是,如果同步从库没有响应(⽐比如它已经崩溃,或者出现⽹络故障,或其它任何原因),主库就⽆法处理写入操作。主库必须阻止所有写入,并等待同步副本再次可⽤。
因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中一个跟随者是同步的,而其他的则是异步的。如果同步从库变得不可⽤或缓慢,则使一个异步从库同步。这保证至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为半同步。
主从复制还经常被配置为全异步模式。 在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写⼊都会丢失。 这意味着即使已经向客户端确认成功,写入也不能保证持久 。 然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。

配置新的从节点

简单地将数据文件从一个节点复制到另⼀个节点通常是不够的:客户端不断向数据库写入数据,数据总是在不断变化,标准的数据副本会在不同的时间点总是不一样。复制的结果可能没有任何意义。
可以通过锁定数据库(使其不可⽤于写入)来使磁盘上的文件保持⼀一致,但是这会违背⾼可用的目标。
幸运的是,拉起新的从库通常并不需要停机。从概念上讲,过程如下所示:

  1. 在某个时刻获取主库的⼀致性快照,而不必锁定整个数据库。大多数据库都具有这个功能,因为它是备份必需的。
  2. 将快照复制到新的从库节点。
  3. 从库连接到主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。
  4. 当从库处理完快照之后积压的数据变更,我们说它赶上了主库。现在它可以继续处理主库产生的数据变化了。
处理失效节点
从节点失效:追赶式恢复

在其本地磁盘上,每个从库记录从主库收到的数据变更。如果从库崩溃并重新启动,或者,如果主库和从库之间的⽹络暂时中断,则比较容易恢复:从库可以从⽇志中知道,在发⽣故障之前处理的最后⼀一个事务。因此,从库可以连接到主库,并请求在从库断开连接时发⽣的所有数据变更。当应用完所有这些变化后,它就赶上了主库,并可以像以前⼀样继续接收数据变更流。

主节点失效:节点切换

主库失效处理起来相当棘⼿:其中⼀个从库需要被提升为新的主库,需要重新配置客户端,以将它们的写操作发送给新的主库,其他从库需要开始拉取来自新主库的数据变更。这个过程被称为故障切换 (failover)。
故障切换会出现很多麻烦:

  • 如果使⽤异步复制,则新主库可能没有收到⽼主库宕机前最后的写入操作。在选出新主库后,如果老主库重新加入集群,新主库在此期间可能会收到冲突的写⼊,那这些写⼊该如何处理?最常见的解决方案是简单丢弃老主库未复制的写入,这很可能打破客户对于数据持久性的期望。
  • 如果数据库需要和其他外部存储相协调,那么丢弃写⼊内容是极其危险的操作。例如在GitHub的一场事故中,⼀个过时的MySQL从库被提升为主库。数据库使⽤用⾃增ID作为主键,因为新主库的计数器落后于老主库的计数器,所以新主库重新分配了一些已经被老主库分配掉的ID作为主键。这些主键也在Redis中使用,主键重用使得MySQL和Redis中数据产⽣不一致,最后导致⼀些私有数据泄漏到错误的⽤户手中。
  • 发⽣某些故障时可能会出现两个节点都以为⾃己是主库的情况。这种情况称为脑裂,⾮危险:如果两个主库都可以接受写操作,却没有冲突解决机制,那么数据就可能丢失或损坏。
  • 主库被宣告死亡之前的正确超时应该怎么配置?在主库失效的情况下,超时时间越长,意味着恢复时间也越⻓。但是如果超时设置太短,⼜可能会出现不必要的故障切换。

复制滞后问题

基于主库的复制要求所有写入都由主节点处理,但只读查询可以由任何副本处理理。
不幸的是,当应⽤程序从异步从库读取时,如果从库落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对主库和从库执⾏相同的查询,可能得到不同的结果,因为并非所有的写⼊都反映在从库中。这种不一致只是⼀个暂时的状态——如果停⽌写入数据库并等待一段时间,从库最终会赶上并与主库保持⼀致。出于这个原因,这种效应被称为最终一致性(eventually consistency)。

读自己的写

如果⽤户在写入后马上就查看数据,则新数据可能尚未到达副本。对用户⽽言,看起来好像是刚提交的数据丢失了,用户会不高兴,可以理解。
在这里插入图片描述
在这种情况下,我们需要读写⼀致性,也称为读己之写一致性。这是⼀个保证,如果⽤户重新加载⻚面,他们总会看到他们⾃己提交的任何更新。它不会对其他用户的写⼊做出承诺。
基于主从复制的系统实现读写一致性,有多种可行的方案,常见如下:

  • 读用户可能已经修改过的内容时,都从主库读;
  • 如果应用中的⼤部分内容都可能被用户编辑,那这种方法就没⽤了,因为大部分内容都必须从主库读取(扩容读就没效果了)。在这种情况下可以使用其他标准来决定是否从主库读取。例如可以跟踪上次更新的时间,在上次更新后的⼀分钟内,从主库读。
  • 还可以监控从库的复制延迟,防⽌向任何滞后超过一分钟的从库发出查询。
  • 客户端可以记住最近⼀次写⼊的时间戳,系统需要确保从库为该用户提供任何查询时,该时间戳前的变更都已经传播到了本从库中。如果当前从库不够新,则可以从另⼀个从库读,或者等待从库追赶上来。时间戳可以是逻辑时间戳(指示写⼊顺序的东⻄,例如⽇志序列号)或实际系统时钟(在这种情况 下,时钟同步变得⾄关重要)。
  • 如果副本分布在多个数据中⼼(出于可用性⽬的与⽤户尽量在地理上接近),则会增加复杂性。任何需要由主节点提供服务的请求都必须路由到包含主库的数据中⼼。

另⼀种复杂的情况是:如果同⼀个用户从多个设备请求服务,例如桌⾯浏览器和移动APP。这种情况下可能就需要提供跨设备的写后读一致性:如果用户在某个设备上输⼊了一些信息,然后在另⼀个设备上查看,则应该看到他们刚输入的信息。
在这种情况下,还有一些需要考虑的问题:

  • 记住⽤户上次更新时间戳的方法变得更加困难,因为一台设备上运⾏的程序不知道另⼀台设备上发生了什么。此时元数据必须做到全局共享。
  • 如果副本分布在不同的数据中心,很难保证来⾃不同设备的连接会路由到同一数据中心。 (例如,用户的台式计算机使⽤家庭宽带连接,而移动设备使⽤蜂窝数据⽹络,则设备的⽹络路线可能完全不同)。如果你的方法需要读主库,可能首先需要把来自同⼀用户的请求路由到同⼀一个数据中心。
单调读

单调读一致性保证当读取数据时,如果某个用户一次进行多次读取,则他绝不会看到回滚现象,即在读取较新值之后又发生读旧值的情况。这是一个比强一致性更弱,但⽐最终一致性更强的保证。单调读取仅意味着如果一个⽤户顺序地进行多次读取,则他们不会看到时间后退, 即,如果先前读取到较新的数据,后续读取不会得到更旧的数据。
实现单调读取的⼀种⽅式是确保每个⽤户总是从同一个副本进⾏读取(不同的⽤户可以从不同的副本读取)。例如,可以基于用户ID的散列来选择副本,⽽不是随机选择副本。但是,如果该副本失败,用户的查询将需要重新路由到另⼀个副本。

前缀一致读

前缀一致读保证:如果一系列写入按某个顺序发生,那么任何⼈读取这些写⼊时,也会看见它们以同样的顺序出现。
这是分区(分⽚)数据库中的⼀个特殊问题。如果数据库总是以相同的顺序应用写入,则读取总是会看到⼀致的前缀,所以这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运⾏行,因此不存在全局写入顺序:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
一种解决方案是,确保任何因果相关的写入都写入相同的分区,但该方案真实实现效率会大打折扣。

多主节点复制

基于主节点的复制有一个主要的缺点:只有一个主库,而所有的写入都必须通过它。如果出于任何原因 (例如和主库之间的⽹络连接中断)无法连接到主库, 就无法向数据库写⼊。
基于主节点的复制模型的⾃然延伸是允许多个节点接受写入。 复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据更改转发给所有其他节点。 称之为多主配置(也称多主、多活复制)。 在这种情况下,每个主节点同时扮演其他领导者的追随者。

适用场景

在单个数据中⼼内部使用多个主库很少是有意义的,因为好处很少超过复杂性的代价。 但在一些情况 下,多活配置是也合理的。

多数据中心

假如你有⼀个数据库,副本分散在好几个不同的数据中⼼(也许这样可以容忍单个数据中心的故障,或地理上更接近用户)。 使用常规的基于主节点的复制设置,主库必须位于其中⼀个数据中⼼心,且所有写入都必须经过该数据中心。
多主配置中可以在每个数据中⼼都有主库。 在每个数据中心内使⽤常规的主从复制:在数据中心之间,每个数据中心的主库都会将其更改复制到其他数据中心的主库中。
在这里插入图片描述

可以对比一下在多数据中心环境下,部署单主节点的主从复制方案与多主复制方案之间的差异:

  • 性能
    在单主节点配置中,每个写入都必须穿过广域网,进⼊主库所在的数据中心。这可能会增加写入时间,并可能违背了设置多个数据中⼼的初心(就近访问)。在多主配置中,每个写操作都可以在本地数据中心进⾏处理,并与其他数据中心异步复制。因此,数据中心之间的网络延迟对⽤户来说是透明的,这意味着感觉到的性能
    可能会更更好。
  • 容忍数据中⼼心停机
    在单主配置中,如果主库所在的数据中心发生故障,故障切换可以使另一个数据中⼼里的追随者成为领导者。在多主配置中,每个数据中⼼可以独立于其他数据中⼼心继运行,并且当发生故障的数据中⼼恢复时,复制会⾃动赶上。
  • 容忍⽹网络问题
    数据中⼼之间的通信通常穿过广域网,这可能不如数据中心内的本地网络可靠。单主配置对这数据中⼼间的连接问题⾮常敏感,因为通过这个连接进行的写操作是同步的。采⽤异步复制功能的多主配置通常能更好地承受⽹络问题:临时的⽹络中断并不会妨碍正在处理的写⼊。
    尽管多主复制有这些优势,但也有⼀个很大的缺点:两个不同的数据中⼼可能会同时修改相同的数据, 写冲突是必须解决的。
离线操作的客户端

多主复制的另⼀种适用场景是:应⽤程序在断⽹之后仍然需要继续工作。例如,考虑手机,笔记本电脑和其他设备上的⽇历应用。无论设备⽬前是否有互联网连接,你需要能随时查看你的会议(发出读取请求),输入新的会议(发出写入请求)。如果在离线状态下进⾏任何更更改,则设备下次上线时,需要与服务器和其他设备同步。
在这种情况下,每个设备都有⼀个充当主节点的本地数据库(它接受写请求),并且在所有设备上的⽇历副本之间同步时,存在异步的多主复制过程。复制延迟可能是几⼩时甚⾄几天,具体取决于何时可以访问互联网。
从架构的⻆度来看,这种设置实际上与数据中⼼之间的多主复制类似,每个设备都是⼀个“数据中心”,而它们之间的⽹络连接是极度不可靠的。

协同编辑

实时协作编辑应用程序允许多个⼈同时编辑⽂档。我们通常不会将协作式编辑视为数据库复制问题,但与前面提到的离线编辑⽤例有许多相似之处。当一个用户编辑文档时,所做的更改将立即应用到其本地副本(Web浏览器或客户端应⽤程序中的文档状态),并异步复制到服务器和编辑同一⽂档的任何其他⽤户。
如果要保证不会发生编辑冲突,则应⽤程序必须先取得文档的锁定,然后用户才能对其进行编辑。如果另⼀个用户想要编辑同⼀个文档,他们首先必须等到第⼀个⽤用户提交修改并释放锁定。这种协作模式相当于在主节点上进行交易的单主复制。
但是,为了加速协作,可能希望将更改的单位设置得⾮常⼩小(例如,⼀个按键),并避免锁定。这种方法允许多个用户同时进⾏编辑,但同时也带来了多领导者复制的所有挑战,包括需要解决冲突。

处理写冲突
同步与异步冲突检测

在单主数据库中,第二个写⼊将被阻塞,并等待第⼀个写入完成,或中止第二个写入事务,强制⽤户重试。另⼀方面,在多主配置中,两个写⼊都是成功的,并且在稍后的时间点仅异步地检测到冲突。那时要求⽤户解决冲突可能为时已晚。
原则上,可以使冲突检测同步,即等待写入被复制到所有副本,然后再告诉⽤户写入成功。但是,通过这样做,将失去多主复制的主要优点:允许每个副本独立接受写⼊。如果想要同步冲突检测,那么或许可以使用单节点复制。

避免冲突

处理冲突的最简单的策略就是避免它们:如果应⽤程序可以确保特定记录的所有写入都通过同一个领导者,那么冲突就不

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
抽奖系统设计⽅案 抽奖系统设计⽅案 1,项⽬简介 ,项⽬简介   本课题主要是基于VUE和SpringBoot框架实现⼀个抽奖系统服务端,该抽奖平台是⼀个⽀持多种不同的抽奖⽅式且⽀持⾼并发的多种⽤户系 统,抽奖系统⾓⾊共分为四类,包括基础的抽奖⽤户,抽奖发布者,进⾏数据信息管理的后端管理员以及⾃动执⾏抽奖的抽奖执⾏模块。普通⽤户 可以查看并参加抽奖;抽奖发布者可以发布抽奖,管理⾃⼰发布的抽奖信息和参加该抽奖的⽤户,获取系统返回的中奖⽤户并发奖;管理员可以通 过抽奖系统后端管理现有的抽奖及⽤户信息;抽奖执⾏模块则负责⾃动适时执⾏各类抽奖。 2,开发环境 ,开发环境   前台开发平台:web前端   后台开发平台:IntelliJ IDEA   数据库:MySQL & Redis   服务器:云服务器(BAE或SAE)   计算机硬件配置:     抓取服务器:内存1.5G以上     数据服务器:内存2G以上 3,使⽤的技术   前端:vue   后端     web框架:Springboot     持久层框架:JPA     认证授权框架:Shiro     分布式框架:Dubbo+Zookeeper     搜索框架:ElasticSearch   数据库:     mysql+redis 4,设计模式 ,设计模式 4.1 ⼋⼤设计原则 ⼋⼤设计原则   提到设计模式,我们⾸先需要了解设计模式的设计原则。  1. 依赖倒置原则(DIP) ⾼层模块(稳定)不应该依赖于低层模块(变化),⼆者都应该依赖于抽象(稳定)。 抽象(稳定)不应该依赖于实现细节(变化),实现细节(变化)应该依赖于抽象(稳定)。 2. 开放封闭原则(OCP) 对扩展开放,对更改封闭。 类模块应该是可以扩展的,但是不可修改。 3. 单⼀职责原则(SRP) ⼀个类应该仅有⼀个引起它变化的原因。 变化的⽅向隐含着类的责任。 4. Liskov替换原则(LSP) ⼦类必须能够替换它们的基类(IS-A)。 继承表达类型抽象。 5. 接⼝隔离原则(ISP) 不应该强迫客户程序依赖它们不⽤的⽅法。 接⼝应该⼩⽽完备。 6. 优先使⽤对象组合,⽽不是类继承 类继承通常为"⽩箱复⽤",对象组合通常为"⿊箱复⽤"。 继承在某种程度上破坏了封装性,⼦类⽗类耦合度过⾼。 ⽽对象组合则只要求被组合的对象具有良好定义的接⼝,耦合度较低。 7. 封装变化点 使⽤封装来创建对象之间的分界层,让设计者可以在分界层的⼀侧进⾏修改,⽽不会对另⼀侧产⽣不良的影响,从⽽实现层次间的松耦 合。 8. 针对接⼝编程,⽽不是针对实现编程 不将变量类型声明为某个特定的具体类型,⽽是声明为某个接⼝。 客户程序⽆需获知对象的具体类型,是需要知道对象所具有的接⼝。 减少系统中各部分的依赖关系,从⽽实现"⾼内聚,松耦合"的类型设计⽅案。  4.2 重构关键技法 重构关键技法    静态 -> 动态 早绑定 -> 晚绑定 继承 -> 组合 编译时依赖 -> 运⾏时依赖 紧耦合 -> 松耦合 4.3 本项⽬中运⽤的设计模式 本项⽬中运⽤的设计模式   对象创建过程中使⽤new,new依赖具体类,耦合度⾼。为了避免new,减少依赖,提供封装机制避免直接new,降低耦合度。   ⼀般的new 违背了 依赖倒置原则(依赖抽象,⽽不依赖具体)     例:A a = new A(); //此处的A是⼀个具体的类,⽽不是抽象   根据依赖倒置原则,我们应该尽可能的使⽤抽象设计,减少具体(可以降低耦合),但是抽象类是不可以实例化的(new),此时就需要⼀种 ⽅式来解决实例化问题。提供⼀个⼯⼚接⼝,把创建对象的任务往后推给⼦类,使当前类与new隔离。该种设计模式就是⼯⼚模式。   使⽤⼯⼚模式的好处:⼯⼚模式⽤于隔离类对象的使⽤者和具体类型之间的耦合关系。⾯对⼀个经常变化的具体类型,紧耦合关系(new)会导致 软件的脆弱。⼀旦更改具体类型,就要更改使⽤者中的代码,耦合度太⾼,⽽⼯⼚⽅法,降低了两者的耦合度,类型改变或增加时只需改变/增加⼯⼚ ⼦类即可,⽽使⽤者的源码不必改变。   ⼯⼚模式的通⽤类图结构图:(附部分注释,希望可以帮助读者快速理解)     5,架构风格 ,架构风格 5.1 微服务架构 微服务架构   "微服务架构是⼀种架构模式,它提倡将单⼀应⽤程序划分成⼀组⼩的服务,服务之间相互协调、互相配合,为⽤户提供最终价值。每个服务 运⾏在其独⽴的进程中,服务和服务之间采⽤轻量级的通信机制相互沟通(通常是基于HTTP的Restful API).每个服务都围绕着具体的业务进⾏构 建,并且能够被独⽴的部署到⽣产环境、类⽣产环境等。微服务架构有如下优点:     1,提升开发交流,每个服务⾜够内聚,⾜够⼩,代码容易理解;     

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值