科普文:分布式系统的架构设计模式

一、分布式架构基本概念

        分布式架构是一种计算机系统设计方法,它将一个复杂的系统划分为多个自治的组件或节点,并通过网络进行通信和协作。每个组件或节点在功能上可以相互独立,但又能够通过消息传递或共享数据来实现协同工作。分布式架构主要用于解决大规模系统的可扩展性、容错性和性能方面的挑战。

分布式架构广泛应用于互联网服务、大数据处理、云计算、物联网等领域。它可以提供高可用性、高性能和灵活性,同时能够适应不断增长和变化的需求。然而,分布式架构也带来了一些挑战,如复杂性管理、一致性问题和调试难度等,需要仔细设计和合理规划来解决。

分布式事务:

分布式事务是指涉及多个独立组件或服务的事务操作,这些组件可以分布在不同的节点或系统中,并通过网络进行通信。分布式事务需要确保事务的原子性、一致性、隔离性和持久性(ACID特性),同时要解决跨节点或系统的数据一致性和并发控制等问题。

以下是分布式事务的一些关键概念和特点:

  1. 原子性(Atomicity):分布式事务的操作要么全部成功提交,要么全部失败回滚,保证事务的原子性,不会出现部分操作成功的情况。

  2. 一致性(Consistency):分布式事务要保证事务执行前后系统数据的一致性,即事务操作前后系统应处于一个合法的状态。

  3. 隔离性(Isolation):分布式事务要求不同事务之间相互隔离,避免干扰和交叉影响,要求事务操作互相独立。

  4. 持久性(Durability):分布式事务要求事务一旦提交,其结果应该持久保存在系统中,即使系统发生故障也能够恢复。

  5. 分布式事务协议:常见的分布式事务协议包括两阶段提交(Two-Phase Commit, 2PC)、三阶段提交(Three-Phase Commit, 3PC)、Paxos协议、Raft协议等,用于协调不同节点上的事务操作并保证一致性。

  6. 并发控制:分布式事务需要考虑并发操作时可能出现的数据竞争和冲突问题,通常采用锁机制、多版本并发控制(MVCC)等技术来确保数据的一致性和正确性。

  7. 可扩展性:分布式事务需要具备水平扩展的能力,能够处理大规模数据和高并发请求,同时保持事务的性能和稳定性。

        分布式事务在现代分布式系统中广泛应用,例如分布式数据库、分布式消息队列、微服务架构等场景都需要支持分布式事务来保证数据一致性和系统可靠性。然而,分布式事务也面临着性能开销高、复杂度高、死锁风险等挑战,因此需要根据具体业务需求和系统特点来选择合适的分布式事务实现方式。

分布式锁:

        分布式锁是一种用于在分布式系统中进行并发控制的机制,可以确保在多个节点上对共享资源的互斥访问,从而避免数据竞争和冲突。分布式锁通常用于控制对共享资源的访问,以确保系统的一致性和正确性。

        以下是分布式锁的一些关键概念和特点:

  1. 互斥性:分布式锁能够确保同一时刻只有一个节点能够获取锁,从而避免多个节点同时对共享资源进行修改或访问。

  2. 可重入性:分布式锁通常支持可重入操作,即同一个节点可以多次获取同一把锁而不会产生死锁。

  3. 锁超时:分布式锁通常支持设置锁的超时时间,以防止因节点故障或其他原因导致锁无法释放而引起系统阻塞。

  4. 锁的实现方式:常见的分布式锁实现方式包括基于数据库的实现(使用行级锁或乐观锁)、基于缓存的实现(使用Redis、Memcached等分布式缓存)、基于ZooKeeper、etcd等分布式协调服务的实现,以及基于分布式锁算法的自定义实现等。

  5. 容错性:分布式锁需要考虑节点故障或网络分区等异常情况下的容错处理,确保锁的可靠性和稳定性。

  6. 性能和成本:选择合适的分布式锁实现需要考虑其性能开销和成本,尽量减少对系统性能的影响,并兼顾系统的可扩展性和可维护性。

        分布式锁在分布式系统中广泛应用于诸如分布式任务调度、分布式缓存同步、分布式队列消费者控制等场景,能够有效地解决并发访问的问题,保证数据的一致性和系统的稳定性。然而,分布式锁也面临着锁粒度控制、死锁检测、性能优化等挑战,需要根据具体业务需求和系统特点来选择合适的分布式锁实现方式,并进行合理的设计和优化。

分布式缓存:

        分布式缓存是一种用于在分布式系统中提高数据访问速度和减轻后端数据存储压力的技术,通过将数据存储在分布式节点中,以提供快速的数据访问和高可用性。分布式缓存通常用于存储频繁访问的数据,如数据库查询结果、计算结果、静态资源等,以加速数据读取和提高系统性能。

        以下是分布式缓存的一些关键概念和特点:

  1. 数据分片和分布:分布式缓存通常将数据分散存储在多个节点中,每个节点存储部分数据,并通过一致性哈希或其他路由算法来确定数据在节点间的分布。

  2. 数据一致性:分布式缓存需要保证存储在不同节点上的数据之间的一致性,通常采用复制、失效和重新加载等机制来保证数据的准确性和可靠性。

  3. 高可用性:分布式缓存通常支持节点的水平扩展和容错能力,以保证系统在节点故障或网络分区时依然能够提供服务。

  4. 缓存策略:分布式缓存通常支持多种缓存策略,如基于时间的过期策略、LRU(Least Recently Used)策略、LFU(Least Frequently Used)策略等,以根据业务需求进行灵活配置。

  5. 缓存穿透和雪崩:分布式缓存需要考虑缓存穿透(即请求的数据在缓存中不存在,导致请求直接访问数据库)和缓存雪崩(即大量缓存同时失效,导致请求集中访问后端存储)等问题,通常通过预热、降级、限流等手段来应对。

  6. 与数据库同步:分布式缓存通常需要与后端数据库进行同步,以确保缓存中的数据与数据库中的数据一致,通常采用缓存更新策略和数据同步机制来实现。

        常见的分布式缓存系统包括Redis、Memcached、Ehcache等,它们提供了丰富的功能和灵活的配置选项,能够满足不同场景下的缓存需求。分布式缓存在各种互联网应用中得到了广泛的应用,如网站页面缓存、接口结果缓存、会话管理等,能够提高系统的性能和可伸缩性。然而,分布式缓存也面临着缓存一致性、数据安全、缓存预热、缓存清理等挑战,需要结合具体业务需求和系统特点来进行合理的设计和使用。

分布式消息中间件:

        分布式消息中间件是一种用于在分布式系统中实现异步通信和解耦的技术,通过消息队列的方式将消息发送者和接收者解耦,以提高系统的可伸缩性、可靠性和性能。分布式消息中间件通常用于处理大量消息传递、事件通知、任务调度等场景,能够实现消息的可靠传递、消息的持久化存储、消息的广播和订阅等功能。

以下是分布式消息中间件的一些关键概念和特点:

  1. 消息队列:分布式消息中间件通常基于消息队列实现,消息发送者将消息发送到队列中,消息接收者从队列中获取消息进行处理。消息队列能够解耦消息的生产和消费过程,提高系统的可扩展性和灵活性。

  2. 消息模型:分布式消息中间件支持多种消息模型,如点对点(Point-to-Point)模型和发布订阅(Publish-Subscribe)模型。点对点模型中,消息发送者将消息发送到特定的接收者;发布订阅模型中,消息发送者将消息发布到主题(Topic),多个订阅者可以订阅该主题接收消息。

  3. 可靠性和持久化:分布式消息中间件通常支持消息的持久化存储,确保消息在发送和接收过程中不会丢失。消息中间件提供了消息确认机制、消息重试机制、消息幂等性等功能,以确保消息的可靠传递。

  4. 高可用性和水平扩展:分布式消息中间件通常具有高可用性和水平扩展能力,能够容忍节点故障、网络分区等异常情况,并支持集群部署和负载均衡,以满足大规模消息处理的需求。

  5. 延迟和吞吐量:分布式消息中间件需要考虑消息的传递延迟和系统的吞吐量,通常通过优化消息处理流程、调整消息队列配置等手段来提高系统性能。

        常见的分布式消息中间件包括Kafka、RabbitMQ、ActiveMQ、RocketMQ等,它们提供了丰富的功能和灵活的配置选项,能够满足不同场景下的消息传递需求。分布式消息中间件在各种互联网应用中得到了广泛的应用,如异步任务处理、日志采集、事件驱动架构等,能够提高系统的可靠性和扩展性。然而,分布式消息中间件也面临着消息顺序性、消息重复、消息幂等性等挑战,需要根据具体业务需求和系统特点来设计合理的消息传递方案。

分布式存储:

        分布式存储是一种将数据分散存储在多个节点上的技术,通过将数据划分为多个部分并存储在不同的物理节点上,以提高系统的可扩展性、可靠性和性能。分布式存储通常用于存储大规模数据、处理高并发读写操作、实现数据冗余和备份等场景。

以下是分布式存储的一些关键概念和特点:

  1. 数据划分和分片:分布式存储将数据按照一定的规则进行划分和分片,并存储在不同的节点上。数据划分可以基于哈希算法、范围算法、一致性哈希等方式进行,以保证数据均匀分布和负载均衡。

  2. 冗余和备份:分布式存储通常采用冗余和备份机制来提高数据的可靠性和容错能力。常见的冗余和备份策略包括副本复制、数据分散、纠删码等,以确保数据不会丢失或损坏。

  3. 可伸缩性和性能:分布式存储具有良好的可伸缩性和性能表现,可以通过增加节点来扩展存储容量和处理能力。分布式存储通常支持数据的并行读写操作,能够提供高吞吐量和低延迟的访问性能。

  4. 一致性和同步:分布式存储需要考虑数据的一致性和同步问题。一致性通常通过副本复制策略、一致性协议等来保证,同步机制可以采用同步复制、异步复制等方式来实现。

  5. 容错和恢复:分布式存储通常具有容错和恢复能力,能够应对节点故障、网络分区等异常情况。容错机制可以基于冗余、数据检验和错误纠正等技术来实现,恢复机制可以通过数据重建、数据迁移等方式来恢复数据完整性和可用性。

        常见的分布式存储系统包括Hadoop HDFS、GFS(Google 文件系统)、Ceph、GlusterFS等,它们提供了丰富的功能和灵活的配置选项,能够满足不同场景下的存储需求。分布式存储在大数据处理、云计算、分布式文件系统等领域得到了广泛的应用,能够处理海量数据、提供高可靠性和高性能的存储服务。然而,分布式存储也面临着数据一致性、数据安全、数据迁移等挑战,需要结合具体业务需求和系统特点来进行合理的设计和使用。

二、分布式事务原理和应用

1、什么是分布式事务

什么是事务

        事务是由一个或多个操作运行的一个逻辑工作单元,这个工作单元具有ACID四种特性:

  1. 原子性(Atomic):事务必须是原子工作单元,对数据进行修改,要么全部执行,要么全部都不执行。

  2. 一致性(Consistent):事务在完成时,必须使所有数据都保持一致状态,事务结束时所有的内部数据结构都必须是正确的;如果事务是并发多个,系统也必须如同串行事务一样操作。其主要特征是保护性和不变性(Preserving an Invariant)。

  3. 隔离性(Isolation):由并发事务所做的修改必须与任何其他并发事务所做的修改隔离

  4. 持久性(duration):事务完成之后,对系统的影响是永久性的,不会被回滚。

        这里的事务主要是指本地事务 :是由关系数据库来控制事务,关系数据库通常都具有ACID特性,传统的单体应用通常会将数据全部存储在一个数据库中,会借助关系数据库来完成事务控制。

什么是分布式事务

        分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,这种一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务。分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库数据的一致性和完整性。

如何进行分布式事务控制

CAP理论

        CAP理论是分布式事务处理的理论基础:分布式系统在设计时只能在一致性(Consistency)、可用性(Availability)、分区容忍性(PartitionTolerance)中满足两种,无法兼顾三种。

  • 一致性(Consistency):多个节点的数据副本需要保持同一时刻数据一致性(强一致性)。

  • 可用性(Availability):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。

  • 分区容错性(Partition Tolerance):将同一服务分布在多个节点中,从而保证某一个节点宕机,仍然有其他节点提供相同的服务。

        分布式系统不可避免的出现了多个系统通过网络协同工作的场景,节点之间难免会出现网络中断、网延延迟等现象,这种现象一旦出现就导致数据被分散在不同的节点上,这就是网络分区。

        在保证分区容错性的前提下一致性和可用性无法兼顾,如果要提高系统的可用性就要增加多个节点,如果要保证数据的一致性就要实现每个节点的数据一致,姐点越多可用性越好,但是数据一致性越差。所以,在进行分布式系统设计时,同时满足“一致性”、“可用性”和“分区容忍性”三者是几乎不可能的。

BASE理论

        CAP理论告诉我们一个不得不接受的事实——我们只能在C、A、P中选择两个条件。而对

于业务系统而言,我们往往选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是,所谓的“牺牲一致性”并不是完全放弃数据一致性,而是牺牲强一致性换取弱一致性。

  • BA:Basic Available 基本可用 整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用”的区别是:

    1、“一定时间”可以适当延长 当举行大促时,响应时间可以适当延长

    2、给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。

  • S:Soft State:柔性状态 同一数据的不同副本的状态,可以不需要实时一致,如订单的"支付中"、“退款中”等状态。

  • E:Eventual Consisstency:最终一致性 经过一段时间后,所有节点数据都将会达到一致。如订单的"支付中"状态,最终会变 为“支付成功”或者"支付失败",使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。

2、分布式事务一致性解决方案

2.1 两阶段提交协议(2PC)

        X/Open这个组织定义的一套分布式事务的标准,也就是定义了规范和API接口,由各个厂商进行具体的实现。这个标准提出了使用二阶段提交(2PC – Two-Phase-Commit)来保证分布式事务的完整性。

        如下图所示,我们知道,在分布式事务中,多个小事务的提交与回滚,只有当前进程知道,其他进程是不清楚的。而为了实现多个数据库的事务一致性,就必然需要引入第三方节点来进行事务协调,如下图所示。

 

图片

        从图中可以看出,通过一个全局的分布式事务协调工具,来实现多个数据库事务的提交和回滚,在这样的架构下,事务的管理方式就变成了两个步骤

  1. 开启事务并向各个数据库节点写入事务日志

  2. 根据第一个步骤中各个节点的执行结果,来决定对事务进行提交或者回滚。

        这就是所谓的2PC提交协议。

2PC提交流程

图片

  1. 表决阶段:此时TM(协调者)向所有的参与者发送一个事务请求,参与者在收到这请求后,如果准备好了(写事务日志)就会向TM发送一个执行成功消息作为回应,告知TM自己已经做好了准备,否则会返回一个失败消息;

  2. 提交阶段:TM收到所有参与者的表决信息,如果所有参与者一致认为可以提交事务,那么TM就会发送提交消息,否则发回滚消息;对于参与者而言,如果收到提交消息,就会提交本地事务,否则就会取消本地事务。

2PC的优缺点

优点:实现强一致性,部分关系数据库支持(Oracle、MySQL等)。
缺点:整个事务的执行需要由协调者在多个节点之间去协调,增加了事务的执行时间,性能低下。解决方案有:springboot+Atomikos or Bitronix

2.2 三阶段提交(3PC)

        3PC是2PC的升级版,分为CanCommit、PreCommit、doCommit三个阶段,解决了2PC单点故障问题,并减少阻塞。

CanCommit阶段:询问阶段

图片

        类似2PC的准备阶段,协调者向参与者发送CanCommit请求,询问是否可以执行事务提交操作,然后开始等待参与者的响应。

PreCommit阶段:事务执行但不提交阶段

图片

        TM根据参与者的反应情况来决定是否可以进行事务的PreCommit操作:

协调者从所有的参与者获得的反馈都是Yes响应:

  1. 发送预提交请求协调者向参与者发送PreCommit请求;

  2. 参与者接收到PreCommit请求后,执行事务操作,并将undo(执行前数据)和redo(执行后数据)信息记录到事务日志中;

  3. 参与者成功的执行了事务操作,则返回ACK(确认机制:已确认执行)响应,同时开始等待最终指令。

有任何一个参与者向协调者发送了No响应,或者等待超时:

  1. 协调者向所有参与者发送中断请求请求。

  2. 参与者收到来自协调者的中断请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

DoCommit阶段:事务提交阶段

图片

执行提交

  1. 协调接收到所有参与者返回的ACK响应后,协调者向所有参与者发送doCommit请求。

  2. 参与者接收到doCommit请求之后,执行最终事务提交,事务提交完之后,向协调者发送Ack响应并释放所有事务资源。

  3. 协调者接收到所有参与者的ACK响应之后,完成事务。

中断事务

  1. 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),协调者向所有参与者发送中断请求;

  2. 参与者接收到中断请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后,向协调者发送ACK消息,释放所有的事务资源。

  3. 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

3PC的优缺点

        优点:相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。

        缺点:会导致数据一致性问题。由于网络原因,协调者发送的中断响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到中断命令并执行回滚的参与者之间存在数据不一致的情况。

2.3 TCC事务补偿

        TCC的方案,是一种两阶段提交的基于应用层的改进方案。将整个业务逻辑的每个分支分成了Try、Confirm、Cancel三个操作,try部分完成业务的准备工作,confirm部分完成业务的提交、cancel部分完成事务的回滚。

        TCC事务解决方案本质上是一种补偿的思路,它把事务运行过程分成Try,Confirm/cancel

        两个阶段,每个阶段由业务代码控制,这样事务的锁力度可以完全自由控制。

        需要注意的是,TCC事务和2pc的思想类似,但并不是2pc的实现,TCC不再是两阶段提交,它对事务的提交/回滚是通过执行一段confirm/cancel业务逻辑来实现,并且也没有全局事务来把控整个事务逻辑。

图片

TCC优缺点

优点:最终保证数据的一致性,在业务层实现事务控制,灵活性好。

缺点:

  • TCC属于应用层的一种补偿方式,每个事务操作每个参与者都需要实现try/confirm/cancel三个接口,开发成本高。

  • 在try、confirm、cancel失败后要不断重试,幂等性需要额外实现。

2.4 Saga

        Saga模式是一种长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

SAGA也是有两阶段的,一阶段是正向事务,二阶段是补偿事务

图片

saga模式依然要求我们自己实现正向服务和补偿服务。但是它于TCC模式的区别之处在于:

  • saga的模式设计使得它天然适合于长流程的业务。TCC要实现同样的长流程的话,需要多写一个confirm操作,并且要考虑如何将业务拆分为两部分

  • saga模式在正向服务中时就已经提交了本地事务了,而补偿事务也比较好实现,将正向服务的结合逆向补偿即可。

  • 比起TCC模式,saga模式更适用于一些老服务、第三方服务或者其他无法改造的服务,要接入到我们的分布式事务中时,就可以将其作为一个正向服务存在,而直接实现他的补偿服务即可。而TCC因为要对业务进行拆分为try-confirm-cancel,所以它不适用于不可改造的服务

同时,saga模式同样不需要全局锁,只需要结合本地事务加本地锁即可,所以性能依旧有保证。

优缺点

优点:

  • 一阶段提交本地事务,无锁,高性能

  • 事件驱动架构,参与者可异步执行,高吞吐

  • 补偿服务易于实现

  • 对业务无侵入

缺点:不保证隔离性,需要额外处理

2.5 基于RocketMQ的事务消息方案

        有一些第三方的MQ是支持事务消息的,如RocketMQ,RocketMQ4.3版本之后,开始支持事务消息,方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,如RabbitMQ 和 Kafka 都不支持(RabbitMQ、Kafka基于ACK机制)。

RocketMQ4.3 整体实现流程如下图所示:

图片

        RocketMQ事务消息的设计,主要是解决Producer端发送消息和本地事务执行结果的原子性问题,因此RocketMQ的设计中Broker和Producer端提供了双向通信的能力,使得Broker天生可以作为一个事务协调者。

        而RocketMQ本身提供的存储机制,为事务消息提供了持久化的能力。再加上RocketMQ的高可用机制以及可靠性消息设计能力,为事务消息在系统发生异常时仍然能够保证消息的成功投递。

        因此它的实现思想其实就是本地消息表的实现思路,只不过本地消息表移动到了MQ的内部,最终解决Producer端的消息发送和本地事务的原子性问题。

优缺点

优点: 实现了最终一致性,不需要依赖本地数据库事务。

缺点: 目前主流MQ中只有RocketMQ支持事务消息。

3、解决方案对比分析

2PC

3PC

TCC

Saga

RocketMQ的事务消息

一致性

强一致性

强一致性

最终一致性

最终一致性

最终一致性

容错性

复杂度

性能

维护成本

4、主流事务框架

Atomikos

        Atomikos是为Java平台提供的开源的事务管理工具,它有JTA/XA规范的实现, 也有TCC机制的实现方案, 前者是免费开源的, 后者是商业付费版的。Atomikos可以嵌入到Spring Boot应用中。使用spring-boot-starter-jta-atomikos,Starter去获取正确的Atomikos库。Spring Boot会自动配置Atomikos,并将合适的 depends-on 应用到Spring Beans上,确保它们以正确的顺序启动和关闭。

Bitronix

        Bitronix是一个流行的开源JTA事务管理器实现,可以使用 spring-bootstarter-jta-bitronix-starter为项目添加合适的Birtronix依赖。和Atomikos类似,Spring Boot将自动配置Bitronix,并对beans进行后处理(post-process)以确保它们以正确的顺序启动和关闭。

Seata

        Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

三、分布式缓存原理和应用


1 高并发下的分布式缓存

        我们先从最开始的来说,我们现在一般都是B/S架构,一般都是中间挂一个服务器,后面有一个数据库。如下图:

图片


        如果客户端的访问量很大的话,那对于后端的服务来说就有一定压力了,压力主要有两个环节;一个环节是关于服务器,一个环节是我们数据库;当我们的并发量增加时,最先达到瓶颈的是我们数据库,这就会导致客户端的访问速度,访问效率降低,这时该如何解决呢?这个时候我们就需要加缓存层;

图片

         我们可以把经常访问的数据称之为热点数据,存放在缓存中,当我们去数据库拿数据的时候,先去缓存中找一下,找到了直接返回,没找到再去数据库中查找;这时可以阻挡一部分接触数据库的请求
如果热点数据很大,一台redis缓存存放不了,这时我们可以采用多台redis机器,这就牵扯到了redis集群,这就是我们说的分布式缓存;

2 Redis集群模式

2.1 主从备份模式

        有一台master,它的下面会挂一群slave;
如果客户端去读数据,是找的这些slave;如果客户端去写数据,找的是master,写完之后master会将数据同步给slave

图片


        思考一个问题,如果使用了这种集群模式,对于上面说到的问题可以解决吗?搭建这样的集群模式最终所能存储的数据是由单台机器的容量所决定的。哨兵模式:哨兵是当主节点挂了之后,需要监控将从切换为主,其他的从节点跟随新的主节点。

        所以搭建这样的集群模式是解决不了海量的热点数据存储的一个问题;所以我们有了下面这种模式的集群;

2.2 切片模式的集群

        我们可以把海量的热点数据进行一个切片,切成一块一块的,每一块存放在一台redis上;
cluster模式:分治,分片的,每一个节点存放的是一部分数据,单个节点挂掉会损失一部分数据。解决的是容量,压力问题。

图片


        那么我们有什么办法将这些数据均匀的分布在这三台机器上 ?有一个切片规则,可以计算下数据key的hash值;比如上面的三台机器,我们可以hash(key) % 3 = [0,1,2]它的计算结果在 0,1,2之间,这样就可以把大量的热点数据均匀的存放在三台redis机器上;但是这样的切片规则是有问题的,什么问题?不利于集群的扩展, 当我们增加一台redis时,这时我们切片规则需要改变并且原本三台上的部分数据需要根据切片规则迁移到新增加的redis机器上

3 缓存击穿,穿透,雪崩

3.1缓存穿透

        假设我们现在以id=-1的请求去请求获取数据,这时数据库里没有,缓存里更没有;当有大量不存在的请求过来时,这些请求在缓存中查找不到,便会去数据库中查找,这时就有可能把数据库压垮!!

如何解决呢??

        我们可以在缓存与数据库之间搞个过滤器,假设我们每次都是通过ID去查询的,过滤器中保存数据库所有的主键ID;当请求过来之后,首先判断过滤器中是否存在这个ID,不存在的话直接过滤掉请求;

        但是上面的方案会存在一个问题,当我们数据库数据越来越大时,过滤器所占用的内存也会随之增大,过滤效率就会降低,客户端请求的链路时间也会越长,客户体验会不好;这时就引出了一个算法-布隆算法。我们可以使用布隆算法来降低过滤器对内存的占用;布隆算法主要通过错误率来换取空间的占用;

3.2缓存雪崩

        redis集群集体宕机, 那所有去redis里请求数据的都会打到数据库上,那一刻数据库可能就撑不住,这种一般很低概率发生;一般都是缓存里的数据都设置了同一有效时间,此时有效期一到redis会把他们删除,此时请求这些数据都会请求到数据库,数据库可能也会宕机;这种情况让redis里的数据陆续失效就可以了而不是同时失效。

四、分布式锁原理和应用


1 锁是什么?

1.1 单机锁

图片


        假设有两个线程,他们都需要对堆中一个对象进行修改,同时修改可能导致数据错误问题,此时我们可以用一个第三方的锁标示,让一个线程获取锁后再进行数据的操作,在锁未释放的时候其他线程要先获取锁才能操作数据

1.2 分布式锁

        当有两台数据时,如果他们访问的数据都在各自的堆里面,其实是不需要分布式锁的;在高并发场景下,要访问数据往往在第三方服务第三方机器里,此时线程不能将各自堆里的对象作为锁标识,因为各自堆里的对象都是独立的,此时我们就需要引入第三方的锁标识,这个锁就叫做分布式锁;

图片

1.3 有了分布式锁之后还需要JVM锁吗?

        在分布式锁的情况下,能保证多机多线程访问资源的一致性,这个时候还需要单个机器里的JVM锁吗?

        答案是需要的!那为什么单机内部还需要一把锁?当没有JVM锁,此时两个机器四个线程去争抢一把分布式锁,此时是四个线程一起争抢资源,倘若两个线程先在单机的JVM内部争抢一把锁,抢到了再去抢占分布式锁,此时便只有两个线程去抢占一把分布式锁,减少了网络的IO,起到了过滤的作用

        所以分布式锁肯定是要比JVM锁慢的,那为什么还需要使用分布式锁呢,它主要解决了并发量的问题。

2、分布式锁的几种实现方式

分布式锁有以下几个方式:

  • MySql

  • Redis

  • RedLock

  • Zk

一、基于 Mysql 实现分布式锁

        Mysql数据库可以使用select xxx for update来实现分布式锁。

        for update是一种行级锁,也叫排它锁。如果一条select语句后面加上for update,其他事务可以读取,但不能进进行更新操作。

        使用for update行级锁可以实现分布式锁,通过行级锁锁住库存,where后条件一定要走索引,不然会触发表锁,会降低MySQL的性能。

        不过基于MySQL实现的分布式锁,存在性能瓶颈,在Repeatable read隔离级别下select for update操作是基于间隙锁锁实现,这是一种悲观锁,会存在线程阻塞问题。

        当有大量的线程请求的情况下,大部分请求会被阻塞等待,后续的请求只能等前面的请求结束后,才能排队进来处理。

二、Redis 实现分布式锁

加锁

加锁通常使用 set 命令来实现,伪代码如下:

set key value PX milliseconds NX

解锁

解锁需要两步操作:

1)查询当前“锁”是否还是我们持有,因为存在过期时间,所以可能等你想解锁的时候,“锁”已经到期,然后被其他线程获取了,所以我们在解锁前需要先判断自己是否还持有“锁”

2)如果“锁”还是我们持有,则执行解锁操作,也就是删除该键值对,并返回成功;否则,直接返回失败。

Redis 分布式锁过期了,还没处理完怎么办?

        为了防止死锁,我们会给分布式锁加一个过期时间,但是万一这个时间到了,我们业务逻辑还没处理完,怎么办?

        首先,我们在设置过期时间时要结合业务场景去考虑,尽量设置一个比较合理的值,就是理论上正常处理的话,在这个过期时间内是一定能处理完毕的。

        之后,我们再来考虑对这个问题进行兜底设计。

        关于这个问题,目前常见的解决方法有两种:

        1、守护线程“续命”:额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间。

        2、超时回滚:当我们解锁时发现锁已经被其他线程获取了,说明此时我们执行的操作已经是“不安全”的了,此时需要进行回滚,并返回失败。

        同时,需要进行告警,人为介入验证数据的正确性,然后找出超时原因,是否需要对超时时间进行优化等等。

守护线程续命的方案有什么问题吗

         Redisson 使用看门狗(守护线程)“续命”的方案在大多数场景下是挺不错的,也被广泛应用于生产环境,但是在极端情况下还是会存在问题。

问题例子如下:

1、线程1首先获取锁成功,将键值对写入 redis 的 master 节点

2、在 redis 将该键值对同步到 slave 节点之前,master 发生了故障

3、redis 触发故障转移,其中一个 slave 升级为新的 master

4、此时新的 master 并不包含线程1写入的键值对,因此线程2尝试获取锁也可以成功拿到锁

5、此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据

        解决方法:上述问题的根本原因主要是由于 redis 异步复制带来的数据不一致问题导致的,因此解决的方向就是保证数据的一致。

当前比较主流的解法和思路有两种:

1)Redis 作者提出的 RedLock;2)Zookeeper 实现的分布式锁。

接下来介绍下这两种方案。

三、分布式锁 Redlock

        首先,该方案也是基于文章开头的那个方案(set加锁、lua脚本解锁)进行改良的,所以 antirez 只描述了差异的地方,大致方案如下。

        假设我们有 N 个 Redis 主节点,例如 N = 5,这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,为了取到锁,客户端应该执行以下操作:

        1、获取当前时间,以毫秒为单位。

        2、依次尝试从5个实例,使用相同的 key 和随机值(例如UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个Redis实例请求获取锁。

        3、客户端通过当前时间减去步骤1记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功。

        4、如果取到了锁,其有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。

        5、如果由于某些原因未能获得锁(无法在至少N/2+1个Redis实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

        可以看出,该方案为了解决数据不一致的问题,直接舍弃了异步复制,只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。

        该方案看着挺美好的,但是实际上我所了解到的在实际生产上应用的不多,主要有两个原因:1)该方案的成本似乎有点高,需要使用5个实例;2)该方案一样存在问题。

该方案主要存以下问题:

        1)严重依赖系统时钟。如果线程1从3个实例获取到了锁,但是这3个实例中的某个实例的系统时间走的稍微快一点,则它持有的锁会提前过期被释放,当他释放后,此时又有3个实例是空闲的,则线程2也可以获取到锁,则可能出现两个线程同时持有锁了。

        2)如果线程1从3个实例获取到了锁,但是万一其中有1台重启了,则此时又有3个实例是空闲的,则线程2也可以获取到锁,此时又出现两个线程同时持有锁了。

        针对以上问题其实后续也有人给出一些相应的解法,但是整体上来看还是不够完美,所以目前实际应用得不是那么多。

四、基于zk实现分布式锁

        ZooKeeper是以Paxos算法为基础分布式应用程序协调服务。Zk的数据节点和文件目录类似,所以我们可以用此特性实现分布式锁。

        基本实现步骤如下:

        1、客户端尝试创建一个znode节点,比如/lock。那么第一个客户端就创建成功了,相当于拿到了锁;而其它的客户端会创建失败(znode已存在),获取锁失败。

        2、持有锁的客户端访问共享资源完成后,将znode删掉,这样其它客户端接下来就能来获取锁了。

        注意:这里的znode应该被创建成ephemeral的(临时节点)。这是znode的一个特性,它保证如果创建znode的那个客户端崩溃了,那么相应的znode会被自动删除。这保证了锁一定会被释放。

可能存在的问题

        看起来这个锁相当完美,没有Redlock过期时间的问题,而且能在需要的时候让锁自动释放。但其实也存在这其中也存在问题。

        ZooKeeper是怎么检测出某个客户端已经崩溃了呢?

        实际上,每个客户端都与ZooKeeper的某台服务器维护着一个Session,这个Session依赖定期的心跳(heartbeat)来维持。如果ZooKeeper长时间收不到客户端的心跳(这个时间称为Sesion的过期时间),那么它就认为Session过期了,通过这个Session所创建的所有的ephemeral类型的znode节点都会被自动删除。

        假如按照下面的顺序执行:

1、客户端1创建了znode节点/lock,获得了锁。

2、客户端1进入了长时间的GC pause。

3、客户端1连接到ZooKeeper的Session过期了。znode节点/lock被自动删除。

4、客户端2创建了znode节点/lock,从而获得了锁。

5、客户端1从GC pause中恢复过来,它仍然认为自己持有锁。

        由上面的执行顺序,可以发现最后客户端1和客户端2都认为自己持有了锁,冲突了。所以说,用ZooKeeper实现的分布式锁也不一定就是安全的,该有的问题它还是有。

zk的watch机制

        ZooKeeper有个很特殊的机制--watch机制。这个机制可以这样来使用,比如当客户端试图创建 /lock 节点的时候,发现它已经存在了,这时候创建失败,但客户端不一定就此对外宣告获取锁失败。

        客户端可以进入一种等待状态,等待当/lock节点被删除的时候,ZooKeeper通过watch机制通知它,这样它就可以继续完成创建操作(获取锁)。这可以让分布式锁在客户端用起来就像一个本地的锁一样:加锁失败就阻塞住,直到获取到锁为止。

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-09 21:04:02       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-09 21:04:02       71 阅读
  3. 在Django里面运行非项目文件

    2024-07-09 21:04:02       58 阅读
  4. Python语言-面向对象

    2024-07-09 21:04:02       69 阅读

热门阅读

  1. area_center 区域和区域中心。

    2024-07-09 21:04:02       32 阅读
  2. Linux

    2024-07-09 21:04:02       23 阅读
  3. 从vs中删除自带的Microsoft Git Provider

    2024-07-09 21:04:02       18 阅读
  4. 设计模式的一点理解

    2024-07-09 21:04:02       18 阅读
  5. QT 设置控件的展开和消失

    2024-07-09 21:04:02       22 阅读
  6. qt 读取配置文件

    2024-07-09 21:04:02       21 阅读
  7. 王道考研数据机构:中缀表达式转为后缀表达式

    2024-07-09 21:04:02       30 阅读