2023年3月

本文作者:周波,阿里云智能高级开发工程师, Apache RocketMQ Committer 。

01
从问题中来的RocketMQ Connect

在电商系统、金融系统及物流系统,我们经常可以看到 RocketMQ 的身影。原因不难理解,随着数字化转型范围的扩大及进程的加快,业务系统的数据也在每日暴增,此时为了保证系统的稳定运行,就需要把运行压力分担出去。RocketMQ就担任着这样的角色,它的异步消息处理与高并发读写能力,决定了系统底层的重构不会影响上层应用的功能。而RocketMQ的另一个优势——可伸缩能力,使系统在面临流量的不确定性时,实现对流量的缓冲处理。此外,RocketMQ的顺序设计特性使其成为一个天然的排队引擎,例如,三个应用同时对一个后台引擎发起请求,确保不引起“撞车”事故。因此,RocketMQ被用在异步解耦、削峰填谷以及事务消息等场景中。

但是,数字化转型浪潮也带来了更多用户对数据价值的关注——如何让数据产生更大利用价值?RocketMQ自身不具备数据分析能力,但是有不少用户希望从 RocketMQ Topic 中获取数据并进行在线或离线的数据分析。然而,使用市面上的数据集成或数据同步工具,将 RocketMQ Topic 数据同步到一些分析系统中虽然是一种可行方案,却会引入新的组件,造成数据同步的链路较长,时延相对较高,用户体验不佳。

举个例子,假设业务场景中使用 OceanBase 作为数据存储,同时希望将这些数据同步到 Elasticsearch进行全文搜索,有两种可行的数据同步方案。

方案一:从 OceanBase 中获取数据,写入 Elasticsearch组件并进行数据同步,在数据源较少时此方案没什么问题,一旦数据增多,开发和维护都非常复杂,此时就要用到第二种方案。


方案二:引入消息中间件对上下游进行解藕,这能解决第一种方案的问题,但是一些较为复杂的问题还没有完全解决。比如,如何将数据从源数据同步到目标系统并保证高性能,如果保证同步任务的部分节点挂掉,数据同步依然正常进行,节点恢复依然可以断点续传,同时随着数据管道的增多,如何管理数据管道也变得十分困难。


总的来说,数据集成过程中的挑战主要有五个。

挑战一:
数据源多,市面上可能有上百个数据源,且各数据源的系统差异较大,实现任意数据源之间的数据同步工作量较大,研发周期很长。

挑战二:
高性能问题,如何高效地从源数据系统同步到目的数据系统,并保障其性能。

挑战三:
高可用问题,即Failover能力,当一个节点挂掉是否这个节点的任务就停止了,任务重新启动是否还可以断点续传。

挑战四:
弹性扩缩容能力,根据系统流量动态增加或减少节点数量,既能通过扩容满足高峰期业务,也能在低峰期缩减节点,节省成本。

挑战五:
数据管道的管理运维,随着数据管道的增多,运维监控的数据管道也会变得越来越复杂,如何高效管理监控众多的同步任务。

面对上述挑战 RocketMQ 如何解决?

第一,标准化数据集成 API (Open Messaging Connect API)。在 RocketMQ 生态中增加 Connect 组件,一方面对数据集成过程抽象,抽象标准的数据格式以及描述数据的 Schema,另一方面对同步任务进行抽象,任务的创建、分片都抽象成一套标准化的流程。

第二,基于标准的 API 实现 Connect Runtime。Runtime提供了集群管理、配置管理、位点管理、负载均衡相关的能力,拥有了这些能力,开发者或者用户就只需要关注数据如何获取或如何写入,从而快速构建数据生态,如与OceanBase、MySQL、Elasticsearc等快速建立连接,搭建数据集成平台。整个数据集成平台的构建也非常简单,通过 Runtime 提供的 RESTFull API 进行简单调用即可。

第三,提供完善的运维工具,方便管理同步任务,同时提供丰富的Metrics 信息,方便查看同步任务的TPS,流量等信息。

02
RocketMQ Connect 两大使用场景

这里为大家整理了 RocketMQ Connect的两大使用场景。

场景一,RocketMQ 作为中间媒介,可以将上下游数据打通
,比如在新旧系统迁移的过程中,如果在业务量不大时使用 MySQL 就可以满足业务需求,而随着业务的增长,MySQL 性能无法满足业务要求时,需要对系统进行升级,选用分布式数据库OceanBase 提升系统性能。

如何将旧系统数据无缝迁移到 OceanBase 中呢?在这个场景中RocketMQ Connect 就可以发挥作用,RocketMQ Connect可以构建一个从 MySQL 到 OceanBase 的数据管道,实现数据的平滑迁移。RocketMQ Connect 还可以用在搭建数据湖、搜索引擎、ETL 平台等场景。例如将各个数据源的数据集成到 RocketMQ Topic当中,目标存储只需要对接 Elasticsearch 就可以构建一个搜索平台,目标存储如果是数据湖就可以构建一个数据湖平台。

除此之外,RocketMQ 自身也可以作为一个数据源,将一个 RocketMQ 集群的数据同步到另一个集群,可以构建 RocketMQ 多活容灾能力,这是社区正在孵化的Replicator可以实现的能力。


场景二,RocketMQ 作为端点。
RocketMQ 的生态中提供了流计算能力组件-RocketMQ Streams,Connector将各个存储系统的数据集成到RocketMQ Topic当中,下游使用 RocketMQ Streams流计算的能力就可以构建一个实时的流计算平台。当然也可以配合业务系统的Service 实现业务系统快速从其它存储统一快速获取数据的能力。


还可以将 RocketMQ 作为端点的上游,将业务消息发到 Topic 中,使用 Connector 对数据做持久化或转存的操作。


如此一来,RocketMQ 就具备数据集成能力,可以实现任意任意异构数据源之间的数据同步,同时也具备统一的集群管理、监控能力及配置化搭建数据管道搭建能力,开发者或者用户只需要专注于数据拷贝,简单配置就可以得到一个具备配置化、低代码、低延时、高可用,支持故障处理和动态扩缩容数据集成平台。

那么, RocketMQ Connect 是如何实现的呢?

03
RocketMQ Connect 实现原理

在介绍实现原理前,先来了解两个概念。

概念一,什么是 Connector(连接器)?
它定义数据从哪复制到哪,是从源数据系统读取数据写入RocketMQ,这种是SourceConnector,或从RocketMQ读数据写入到目标系统,这种是SinkConnector。Connector决定需要创建任务的数量,从Worker接收配置传递给任务。


概念二,什么是 Task ?
Task 是 Connector 任务分片的最小分配单位,是实际将源数据源数据复制到 RocketMQ(SourceTask),或者将数据从RocketMQ 读出写入到目标系统(SinkTask)真正的执行者,Task是无状态的,可以动态的启停任务,多个Task可以并行执行,Connector 复制数据的并行度主要体现在 Task 上。一个 Task 任务可以理解为一个线程,多个 Task 则以多线程的方式运行。


通过Connect的API也可以看到Connector和Task各自的职责,Connector实现时就已经确定数据复制的流向,Connector接收数据源相关的配置,taskClass获取需要创建的任务类型,通过taskConfigs的数量确定任务数量,并且为Task分配好配置。Task拿到配置以后数据源建立连接并获取数据写入到目标存储。通过下面的两张图可以清楚的看到,Connector和Task处理基本流程。


一个 RocketMQ Connect 集群中会有多个 Connector ,每个 Connector 会对应一个或多个 Task,这些任务运行在 Worker(进程)中。Worker进程是Connector和Task运行环境,它提供RESTFull能力,接收HTTP请求,将获取到的配置传递给Connector和Task,它还负责启动Connector和Task,保存Connector配置信息,保存Task同步数据的位点信息,除此以外,Worker还提供负载均衡能力,Connect集群高可用、扩缩容、故障处理主要依赖Worker的负责均衡能力实现的。Worker 提供服务的流程如下:


Worker 提供的服务发现及负载均衡的实现原理如下:

服务发现:


用过 RocketMQ 的开发者应该知道,它的使用很简单,就是发送和接收消息。消费模式分为集群模式和广播模式两种,集群消费模式下一个 Topic 可以有多个 Consumer 消费消息,任意一个 Consumer 的上线或下线 RocketMQ 服务端都有感知,并且还可以将客户端上下线信息通知给其它节点,利用RocketMQ这个特性就实现了 Worker的服务发现。

配置/Offset同步:


Connector 的配置/Offset信息同步通过每个 Worker订阅相同的 Topic,不同Worker 使用不同的 Consumer Group 实现的, Worker 节点可以通过这种方式消费到相同Topic的所有数据,即Connector配置/Offset信息,这类似于广播消费模式,这种数据同步模式可以保证任何一个 Worker 挂掉,该Worker上的任务依旧可以在存活的 Worker 正常拉起运行 ,并且可以获取到任务对应的 Offset 信息实现断点续传, 这是故障转移以及高可用能力的基础。

负载均衡

RocketMQ 消费场景中,消费客户端 与Topic Queue 之间有负载均衡能力,Connector 在这一部分也是类似的,只不过它负载均衡的对象不一样,Connector 是 Worker 节点和 Task之间的负载均衡,与RocketMQ客户端负载均衡一样,可以根据使用场景选择不同负载均衡算法。


上文提到过 RocketMQ Connect 提供 RESTFull API能力。通过 RESTFull AP可以创建 Connector,管理Connector 以及查看 Connector 状态,简单列举:

  • POST /connectors/
  • GET /connectors/{connector name}/config
  • GET /connectors/{connector name}/status
  • POST /connectors/{connector name}/stop

目前 Connector 支持单机、集群两种部署模式。集群模式至少要有两个节点,才能保证它的高可用。并且集群可以动态增加或者减少,做到了动态控制提升集群性能和节省成本节省的能力。单机模式更多方便了开发者开发测试 Connector 。

如何如何实现一个 Connector呢?
还是结合一个具体的场景看一看,例如业务数据当前是写入MySQL 数据库中的,希望将MySQL中数据实时同步到数据湖Hudi当中。只要实现 MySQL Source Connector 、Hudi Sink Connector这两个 Connector 即可。


下面就以 MySQLSource Connector 为例,来看一下具体的如何实现。

实现 Connector最主要的就是实现两个 API 。第一个是 Connector API ,除了实现它生命周期相关的 API 外,还有任务如何分配,是通过 Topic、Table 还是通过数据库的维度去分。第二个API是需要创建的 Task,Connector 通过任务分配将相关的配置信息传递给 Task, Task 拿到这些信息,例如数据库账号,密码,IP,端口后就会创建数据库连接,再通过 MySQL 提供的 BINLOG 机智获取到表的数据,将这些数据写到一个阻塞队列中。Task有个 Poll 方法,实现Connector时只要调用到Poll方法时可以获取到数据即可,这样 Connector 就基本写完了。然后打包以Jar包的形式提供出来,将它加载到 Worker 的节点中。


创建 Connector 任务后, Worker 中会创建一个或者多个线程,不停的轮询 Poll方法,从而获取到MySQL表中的数据,再通过 RocketMQ Producer 发送到 RocketMQ Broker中,这就是 Connector 从实现到运行的整体过程(见下图)。


04
RocketMQ Connect 现状与未来

RocketMQ Connect 的发展历程分为三个阶段。

第一阶段:Preview 阶段

RocketMQ Connect发展的初期也即Preview阶段,实现了Open Messaging Connect API 1.0 版本,基于该版本实现了 RocketMQ Connect Runtime ,同时提供了 10+ Connector 实现(MySQL,Redis,Kafka,Jms,MongoDB……)。在该阶段,RocketMQ Connect 可以简单实现端到端的数据源同步,但功能还不够完善,不支持数据转换,序列化等能力,生态相对还比较贫乏。

第二阶段:1.0 阶段

在 1.0 阶段,Open Messaging Connect API 进行了升级,支持Schema、Transform,Converter等能力,在此基础上对 Connect Runtime 也进行了重大升级,对数据转换,序列化做了支持,复杂Schema也做了完善的支持。该阶段的 API、Runtime 能力已经基本完善,在此基础上,还有30+ Connecotor 实现,覆盖了 CDC、JDBC、SFTP、NoSQL、缓存Redis、HTTP、AMQP、JMS、数据湖、实时数仓、Replicator、等Connector实现,还做了Kafka Connector Adaptor可以运行Kafka生态的Connector。

第三阶段:2.0 阶段

RocketMQ Connect当前处于这个阶段,重点发展Connector生态,当 RocketMQ 的 Connector生态达到 100 + 时,RocketMQ 基本上可以与任意的一个数据系统去做连接。

目前 RocketMQ 社区正在和 OceanBase 社区合作,进行 OceanBase 到 RocketMQ Connect 的研发工作,提供 JDBC 和 CDC 两种模式接入模式,后续会在社区中发布,欢迎感兴趣的同学试用。

05
总结

RocketMQ是一个可靠的数据集成组件,具备分布式、伸缩性、故障容错等能力,可以实现RocketMQ与其他数据系统之间的数据流入与流出。通过 RocketMQ Connect 可以实现CDC,构建数据湖,结合流计算可实现数据价值。

前言

由于业务需求,在探究.Net7的CLR,发现了一个不通的地方,也就是通过GCInfo获取到了对象之后。它并没有在GcScanRoots(对象扫描标记)里面对它进行标记,那么如果没有标记这个对象如何被计划阶段构建呢?仔细研读,发现它跟之前的代码之所以不同,是因为它把标记抽取出来,另外形成一个数组循环标记。本篇来看下。


概括

1.问题:
假如说有以下示例代码:

static void Main(string[] args)
{
    Console.WriteLine("Tian Xia Feng Yun Chu Wo Bei!\r\n");
    Program PM= new Program();
    PM = null;
    GC.Collect();
}

调用GC.Collect()函数,GC垃圾回收的第一步,就是标记。这个标记实质上可以分为以下几步。
一:获取到所有的线程(GetAllThreadList)
二:遍历循环这些线程的帧
三:通过遍历到的帧,找到这些帧对应的GCInfo
四:通过GCInfo的偏移量和寄存器找到相对应的对象
五:对找到的对象进行标记。

以上四步,基本上没变。第五步标记的时候,它加入了一些新的代码。

uint8_t *mark_queue_t::queue_mark(uint8_t *o)
{
    Prefetch (o);
    size_t slot_index = curr_slot_index; //这里有一个slot的索引
    uint8_t* old_o = slot_table[slot_index];// 这里把这个索引的值从数组取出来
    slot_table[slot_index] = o;//把新对象赋值到索引所在的数组内存
    curr_slot_index = (slot_index + 1) % slot_count;
    if (old_o == nullptr)//这个地方是关键,因为假如说你按照上面的示例代码,之前并没有这个PM对象。所以这个old_o是等于nullptr的,所以它直接return了。那么下面就不存在标记了。问题是这个标记不标记??还是在别的地方标记了??
        return nullptr;
    BOOL already_marked = marked (old_o);
    if (already_marked)
    {
        return nullptr;
    }
    set_marked (old_o);
    return old_o;
}

二:解决
要解决这个问题,就需要知道数组slot_table里面的数值是何时被变动的。这个其实很简单在VS里面可以,打个条件断点--值更改时中断。
结果发现在函数get_next_marked里面对slot_table数组里面的值(也就是对象)进行了一个标记

uint8_t* mark_queue_t::get_next_marked()
{
    size_t slot_index = curr_slot_index; //获取到当前slot_table数组的总数索引
    size_t empty_slot_count = 0; 
    while (empty_slot_count < slot_count) //从零开始循环总数索引
    {
        uint8_t* o = slot_table[slot_index]; //一个个的取出来,保存到o变量。
        slot_table[slot_index] = nullptr; //然后把相应的索引位内存置0,以便下次标记的时候继续使用。
        slot_index = (slot_index + 1) % slot_count;
        if (o != nullptr) //如果这个o不等于null,那么下面的代码就是对它进行一个标记。
        {
            BOOL already_marked = marked (o); //判断它是否被标记
            if (!already_marked) // 如果没有
            { 
                set_marked (o); // 则对它进行标记
                curr_slot_index = slot_index; 
                return o;//把标记过的对象返回
            }
        }
        empty_slot_count++;//继续循环下一次,继续标记下一个
    }
    return nullptr;// 如果索引为空,则直接返回null
}

这个函数是被drain_mark_queue函数调用,而前者则是在GCScanRoot整个函数被完成之后调用的。
那么整体的就打通关节了,实质上它是抽取出来了,重新进行了标记。而非在GCScanRoot里面进行标记。


结尾:

作者:江湖评谈。公众号:jianghupt。扫码关注我,带你了解高阶技术,不局限于.Net。
image

概念简介

Paxos是一种基于消息传递具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。

发展历史

Paxos算法的发展历史追溯到古希腊,当时有一个名为“Paxos“的小岛, 岛上采用一会的形式通过法令, 议会中议员通过信使进行消息传递,议员与信使都是兼职的,他们随时都有可能会离开议会,并且信使有可能传递重复的信息,也有可能一去不复返,因此议会要保证在这种情况下法令仍然能够正确的产生,并且不会起冲突。

Paxos算法分析

对于Paxos算法而言要解决上述信息传递的一致性问题,那么要保证一下几点:

  • 在这些提案中,只有一个被选定
  • 如果没有提案被提出,就不会有选定的提案
  • 当提案被选定以后,进程应该可以获取被选定的提案信息

对于一致性来说,安全性需求如下

  • 只有被提出的提案才能被选定
  • 只有一个提案被选定
  • 如果某个进程认为某个提案被选定了,那么这个提案必须是真的被选定的那个

三种参与角色

  • Proposer(提议者)
  • Acceptor(决策者)
  • Leamner(最终决策学习者)
问题场景分析

一个元素参与者可能扮演多个角色
(Proposer | Acceptor | Leamner)
,假设不同的参与者之间可以通过收发消息来进行通信。每个参与者以任意的速度执行,可能会因为出错而停止,也可能会重启,消息在传输过程中可能会出现不可预知的延迟,也有可能会重复或者丢失,但消息不会被损坏,即消息内容不能被篡改。

Paxos算法场景问题分析

首先,我们采用将建立角色处理模式的场景化分析,先从Acceptor的模式开始处理和分析,分析对应的执行流程以及对应的问题。

单个Acceptor模式

在处于单Acceptor模式下的时候,如以下图所示。

最简单的选定方式是只有一个Acceptor, Proposer发送给该Acceptor提案以后, Acceptor直接选择第一个提案为被选定的提案。但这种做法一旦Acceptor出问题, 整个系统将无法正常工作。

多个Acceptor模式

Proposer向多个Acceptor集合发送提案, 每个Acceptor都可能会批准(Accept) 该提案, 当足够多个Acceptor批准这个提案的时候, 我们就认为该提案被选定了。

实现一致性的条件约束(1)

在没有失败和消息丢失的情况下,如果我们希望即使只有一个提案被提出,仍然可以选出一个提案,1个Acceptor必须批准他收到的第一个提案

该条件约束所出现的问题

如果多个提案被不同的Proposer同时提出, 这可能会导致虽然每个Acceptor都批准了他收到的第一个提案, 但是没有一个提案是多个人批准的,也就是没有多数的Acceptor集合,如下图所示。

为了解决此问题所以引入了【实现一致性的条件约束(2)】进行数据控制。

实现一致性的条件约束(2)

一个提案被选定需要被半数以上的Acceptor接受

它是在【实现一致性的条件约束(1)】的基础上, 一个Acceptor能够批准不止一个提案。我们使用全局的编号来唯一的标识每一个Acceptor批准的提案, 当一个具有某Value的提案被半数以上的Acceptor批准以后, 我们就认为该Value被选定。

注意:提案和value不是一个概念, 提案是由一个编号与value组成的结构体, 因此我们用【编号,Value】来表示一个提案

提案的结构体分析

提案的信息数据结构体主要有:提案编号+value两部分组成。

  • 提案编号:给每个提案加上一个提案编号,表示提案被提出的顺序,不同的编号可以有相同的内容。
  • value:提案的内容
该条件约束所出现的问题

虽然允许多个提案被选定, 但必须保证所有被选定的提案都具有相同的value值,否则又会出现不一致。

为了解决此问题所以引入了【实现一致性的条件约束(3)】进行数据控制。

实现一致性的条件约束(3)

如果提案编号为M, Value为V的提案(即【M,V】)被选定了,那么所有比M_编号更高的, 且被选定的提案, 其Value值必须也是V

因为提案编号是全序的, 【实现一致性的条件约束(3)】就保证了只有一个Value值被选定这一关键安全性属性。同时,一个提案被选定,其首先必须被至少一个Acceptor批准, 因此我们可以通过满足如下条件来满足【实现一致性的条件约束(3)】。

案例推荐

假设总的有5个Acceptor,Proposer2提出 [M1,V1] 的提案,Acceptor2~5(半数以上)均接受了该提案,于是对于Acceptor 2~5和Proposer2来讲, 它们都认为V1被选定。

Acceptor1刚刚从宕机状态恢复过来(之前Acceptor1没有收到过任何提案) , 此时Proposer1向Acceptor1发送了[M2, V2] 的提案(V2且M2>M1) ,对于Acceptorl来讲, 这是它收到的第一个提案。根据【实现一致性的条件约束(1)】(一个Acceptor必须接受它收到的第一个提案) ,从而Acceptor1必须接受该提案!同时Acceptor1认为V2被选定,这就出现了两个问题。

问题分析
  1. Acceptor1认为V2被选定,Acceptor2~5和Proposer2认为V1被选定,出现了不一致

  2. V1被选定了,但是编号更高的被Acceptor1接受的提案[M2,V2] 的value为V2,且V2不等于V1。且V2的编号还高于V1

实现一致性的条件约束(4)

如果一个提案【M,V】被选定后, 那么之后任何Proposer产生的编号更高的提案, 其Value的值都为V。

问题分析

如何确保在某个Value为V的提案被选定后, Proposer 提出的编号更高的提案的Value都是V呢?

实现分析

任意的N和V, 如果提案 [ N,V ] 被提出,那么存在一个半数以上的Acceptor组成的集合S,需要执行以下两个操作步骤:

  • 集合S内的每个Acceptor都没有批准过编号小于N的提案
  • 如果Acceptor已经接受过提案,那么就向Proposer响应已经接受过的编号小于N的最大编号的提案

Proposer生成提案

对于一个
Proposer
来说, 获取那些已经通过的提案远比预测未来可能会通过的提案来的简单。因此
Proposer
在产生一个编号为M的提案时, 必须要知道当前某一个将要或已经被半数以上Acceptor批准的编号小于M但未最大的编号的提案。并且,
Proposer
会要求所有Acceptor都不要批准任何编号小于M的提案。

Proposer生成提案之前(Prepare阶段)

应该先去学习已经被选定或者可能被选定的value,然后以该value作为自己提出的提案的value。如果没有value被选定, Proposer才可以自己决定value的值。这样才能达成一致。这个学习的阶段是通过一个
【Prepare阶段】
请求实现的。

  • 向Proposer承诺保证不再接受任何编号小于N的提案
  • 如果Acceptor已经接受过提案,那么就向Proposer响应已经接受过的编号小于N的最大编号的提案

提案生成算法

如果Proposer收到了平数以上的Acceptor的响应, 那么它就可以生成编号为N, Value为V的提案[N,V] 。这里的V是所有的响应中编号最大的提案的Value。如果所有的响应中都没有提案, 那么此时V就可以由Proposer自己选择。

Proposer生成提案之后(Accept请求)

Proposer将该提案发送给半数以上的Acceptor集合, 并期望这些Acceptor能接受该提案。我们称该请求为Accept请求。

注意:此时接受Accept请求的Acceptor集合不一定是之前响应Prepare请求的Acceptor集合

Acceptor批准提案

  • Acceptor可以忽略任何请求(包括Prepare请求和Accept请求) 而不用担心破坏算法的安全性。因此, 我们这里要讨论的是什么时候Acceptor可以响应一个请求。

  • 一个Acceptor只要尚未响应过任何编号大于N的Prepare请求, 那么他就可以接受这个编号为N的提案。

算法总结

阶段一

  1. Proposer选择一个提案编号M, 然后向Acceptor的某个超过半数的子集成员发送编号为M的Prepare请求。
  2. 如果一个Acceptor收到一个编号为M的Prepare请求, 且编号M大于该Acceptor已经响应的所有Prepare请求的编号, 那么它就会把已经批准过的最大的编号的提案作为相应反馈给Proposer, 同时该Acceptor会承诺不会在批准任何编号小于M的提案。

阶段二

  1. 如果Proposer收到来自半数以上的Acceptor对于其发出的编号为M的Prepare请求的响应,那么它就会发送一个针对【M,V】提案的Accept请求给Acceptor。

注意:V的值就是收到的响应中编号最大的提案的值,如果响应中不包含任何提案,那么他就是任意值

  1. 如果Acceptor收到的这个针对【M, V】的提案的Accept请求, 只要该Acceptor尚未对编号大于M的Prepare请求作出响应, 他就可以通过这个提案。

看到这里是不是觉得和我们分布式事务中的2PC的思路和流程差不多啊!

通知学习Learner的方案

方案1

一旦Acceptor批准了一个提案, 就将该提案发送给所有的Leamer

方案2

让所有的Acceptor将它们对提案的批准情况, 统一发送给一个Learner, 再由它通知其他的Learner

方案3

方案2的主节点存在单点问题, 可以将主Leaner的范围扩大, 即Acceptor可以将批准信息发送给一个特定的Learner集合, 该集合中每个Leamer都可以在一个提案被选定后通知其他Leaner。

给你们的问题

  1. 设置多少个Acceptor最为合适?

  2. 如何控制每个Acceptor最多只能批准一个提案?

模板语法

插值语法

Mustache插值采用
{{ }}
,用于解析标签体内容,将Vue实例中的数据插入DOM中

 <h1>Hello {{name}}</h1>

指令语法

指令用于解析标签,是vue为开发者提供的一套
模板语法

辅助开发者渲染页面的基本结构

(指令是vue开发中最基础、最常用、最简单的知识点)(支持绑定简单的数据值之外,还支持js表达式运算)

内容渲染指令

1、
v-text=" "
会覆盖元素内默认的值、无法写
HTML标签
语句

2、
{{ }}
插值表达式(Mustache) 不会覆盖元素中默认的内容

3、
v-html
可以渲染包含
HTML标签
的字符串

v-html
有安全性问题,在网站上动态渲染任意HTML是非常危险的,容易导致XSS攻击;永远不要再用户提交的内容上使用
v-html

属性绑定指令

v-bind:
为元素的属性动态绑定属性值

属性:placeholder、src等

简写:
:

<a v-bind:href="url">点击跳转</a>
<a :href="url">点击跳转</a>
事件绑定指令

v-on:
为DOM元素绑定事件监听

v-on:click=” “、v-on:input、v-on:keyup

事件处理函数,
需要在methods节点中进行声明

简写:
@
、如果事件处理函数的代码足够简单,只有一行代码,则可以简写到行内

事件对象event
<body>
    <div id="app">{{username}}</div>
    <button @click="addcount($event,88)">
        点击我
    </button>

    <script src="vue.js"></script>
    <script>
        const vm=new Vue({
            el:'#app',
            data:{
                username:'zs',
                count:0
            },
            methods:{
                addcount(e,88){
                    const nowBgColor =e.target.style.backgroundColor
                    console.log(nowB gColor)
                    this.count+=1
                }
            }
        })
    </script>
</body>

绑定事件并传参,使用() 小括号,但会占用event的位置

$event是vue提供的特殊变量用于占位,用来表示原生事件参数对象event,使用e接受

事件修饰符

vue提供事件修饰符,来辅助程序员更方便的对事件的触发进行控制

  • .stop
    阻止事件
    冒泡
    ()里到外

  • .prevent
    阻止默认行为()比如
    阻止跳转

  • .capture

    捕获
    模式触发当前事件处理函数(外到里)

  • .self
    只有在event.target是当前元素自身时触发事件处理函数

  • .once
    绑定的事件只触发一次

  • .passive
    事件的默认行为立即执行,无需等待事件回调执行完毕

按键修饰符

监听
键盘事件
时,我们经常需要
判断详细的按键
,此时可为键盘相关的事件添加
按键修饰符

  • .enter、.delete、.esc、.space、.tab(特殊:只适用于keydown)、.up、.down、.left、.right
  • 系统修饰符(用法特殊):ctrl、alt、shift、mta 配合keyup使用:按下按键的同时需要再按下其他键,随后事件才被触发;keydown正常触发事件
  • 也可以使用keyCode去指定具体的按键(不推荐)
  • Vue.config.keyCodes.自定义键名=键码 可以去定制按键别名
双向绑定指令

v-model 在不操作DOM的情况下,快速获取表单数据(只能配合
表单元素
一起使用)

<input type="text" v-model:value="username"/>
<input type="text" v-model="username"/>
v-model 指令的修饰符

方便用户对输入的内容进行处理

  • .number
    自动将用户输入值转化为数值
  • .trim
    自动过滤用户输入的首尾空白字符
  • .lazy
    在change时更新而非input时更新(不实时更新,文本框失去焦点更新)
条件渲染指令

按需控制DOM的显示和隐藏

v-if =" "
动态创建或移除DOM元素
,有更高的切换开销(重绘和回流)

v-else-if =" "

v-else =" "

v-show=" "
动态为元素添加或移除
display样式
,有更高的初始渲染开销。

列表渲染指令

v-for=" "
辅助开发者基于一个数组来循环渲染相似的UI结构

特殊语法:item in items

<body>
   
    <div id="root">
        <!-- 遍历数组 -->
        <ul>
            <li v-for="item in persons" :key="item.id">
                {{item.id}}-{{item.name}}-{{item.age}}
            </li>
        </ul>
        <!-- 遍历对象 -->
        <ul>
            <li v-for="(value,key) of cars" :key="key">
                {{key}}-{{value}}
            </li>
        </ul>
        <!-- 遍历字符串 -->
        <ul>
            <li v-for="(char,index) of str" :key="index">
                {{index}}-{{char}}
            </li>
        </ul>
        <!-- 遍历指定次数 -->
        <ul>
            <li v-for="(number,index) of 5" :key="index">
                {{index}}-{{number}}
            </li>
        </ul>

    </div>
    <script type="text/javascript">
        Vue.config.productionTip = false;
        const vm=new Vue({
            el:'#root',
            data:{
                persons:[
                    {id:'001',name:'张三',age:18},
                    {id:'002',name:'李四',age:17},
                    {id:'003',name:'王五',age:42},
                ],
                cars:{
                    name:'Audi',
                    price:"70W",
                    color:"black"
                },
                str:"hello"

            }
        })
   
    </script>
    
</body>
for in和for of

for in一般遍历对象,不建议遍历数组。遍历数组请使用for of或forEach。

v-for中的索引

(item,index) in items

注:v-for中的item项和index索引都是形参,任意命名

key作用和原理

当列表的数据变化(添加、删除)时,vue会尽可能复用已存在的DOM元素,从而提升渲染的性能,但这种性能优化策略
会导致有状态的列表无法被正确更新
,key的使用将减少不必要的DOM操作,提高更新效率。

解释:

Vue根据初始数据生成初始虚拟DOM(Vnodes),将虚拟DOM转为真实DOM,用户添加新的数据,产生新虚拟DOM(Vnodes),此时将两份虚拟DOM根据key进行对比(
diff 算法
),如果对比一样的直接复用,将之前的真实DOM直接渲染;不一样的无法复用,将其生成真实DOM。

  • 如果使用index作为key,diff对比时会完全错位,所有节点都无法复用,效率降低。
  • 如果不指定key时,vue自动使用遍历时的索引值index作为key。

为了给vue一个提示,以便它能跟踪每个节点的身份,需要为每项提供一个
唯一的key
属性

image

image

注:

  • key只能是字符串或数字类型

  • key的值必须具有唯一性

  • 建议把数据项id属性的值作为key的值,
    将index的值作为key的值没有任何意义

    https://blog.csdn.net/z591102/article/details/106682298/)

  • 建议使用v-for时一定要指定key的值(提升性能,防止列表状态紊乱)

  • 但不存在对数据的操作,仅是渲染列表用于展示,使用index作为key没有问题

image

其他内置指令

v-cloak
没有值,配合CSS,解决网速慢、服务器响应慢时
未经解析的模板
渲染到页面上,Vue实例创建并接管容器后,会删掉该属性。

[v-cloak]:{
    display:none;
}

v-once
没有值,记录初始值,在初次动态渲染后,就视为静态内容,以后数据的改变不会引起
v-once
所在结构的更新,可以用于性能优化。

v-pre
没有值,跳过其所在节点的编译过程,可以用它跳过:没有使用指令语法、插值语法的节点,会加快编译。

自定义指令

本质:将原生操作DOM进行二次封装。

<body>
   
    <div id="root">
        <!-- v-big放大10倍 -->
        <h2>当前的n值是:<span v-text="n"></span></h2>
        <h2>放大10倍后的n值是:<span v-big="n"></span></h2>
        <button @click="n++">点位n++</button>
        <hr>
        <!-- v-fbind在v-bind基础上,自动获得焦点 -->
        <input type="text" v-fbind:value="n">
    </div>
    <script type="text/javascript">
        Vue.config.productionTip = false;
        const vm=new Vue({
            el:'#root',
            data:{
               n:0,
            },
            directives:{
                //函数式
                //何时会被调用:1、指令和元素成功绑定;2、指令所在的模板被重新解析时
                big(element,binding){
                    element.innerText=binding.value*10
                },
                //错误写法:
                fbind(element,binding){
                    element.value=binding.value
                    element.focus()//.focus()调用时机不对,此时input还未被插入页面
                },
                //对象式
                fbind:{
                    //指令与元素成功绑定时调用
                    bind(element,binding){
                        element.value=binding.value
                    },
                    //指令所在元素被插入页面时调用
                    inserted(element,binding){
                        element.focus()
                    },
                    //指令所在模板被重新解析时调用
                    update (element,binding) {
                        element.value=binding.value
                    },
                }

            },
        })
   
    </script>
</body>

注意:

  • 命名多个单词使用
    -
    分隔;
  • 指令中
    this
    的指向是
    window
  • 自定义指令默认都是局部指令,全局写法:
    Vue.directive

计算属性

计算属性
本质上
就是一个
function函数
,它可以
实时监听
data中数据的变化,并
return一个计算后的新值
,供组件渲染DOM时使用。

开发者需要以
function函数
的形式声明到组件的
computed节点

<template>
  <div>
    <input type="text" name="" id="" v-model.number="count">

    <p>{{count}} 乘以2的值为:{{plus}}</p>//必须当做普通属性使用
  </div>
</template>

<script>
export default {
    name:'MyCount',
    data(){
        return{
            count:1,
        }
    },
    //必须定义在computed节点
    computed:{
        //必须是一个function函数
        plus(){
            //必须具有返回值
            return this.count*2
        }
        
        //相当于
        
        plus:{
        //当读取plus,get会被调用,且返回值作为plus的值
        //get何时被调用?1、初次调用plus;2、所依赖的数据发生变化
        get(){
            return this.count*2
        }
      }
    }

}
</script>

侧重于得到一个计算的结果,必须有return返回值

计算属性(computed)vs方法(methods):

相对于方法,
计算属性会缓存计算结果
,只有计算属性的
依赖项发生变化
时,才会
重新进行运算
,因此计算属性的性能会更好。

watch侦听器

watch侦听器
允许开发者监视数据的变化,从而
针对数据的变化做特定的操作

开发者需要在watch节点之下,定义自己的侦听器

<script>
export default {
    name:'MyCount',
    data(){
        return{
            username:'',
        }
    },
   watch:{
       //当username发生改变时,调用 handler
       username:{
           handler(newVal,oldVal){
             console.log(newVal,oldVal) 
           }  
       }
      //简写
       username(newVal,oldVal){
           console.log(newVal,oldVal)     
       }       
   }
  
   }
</script>
<body>
    <div id="app">{{username}}</div>
    #导入脚本文件
    <script src="vue.js"></script>
    <script>
        const vm=new Vue({
            data(){
            return{
                   username:'zs'
            }
            }
        })
        vm.$mount=('#app')//挂载
        vm.$wtach('username',{ //第二种写法
            handler(newVal,oldVal){
                console.log(newVal,oldVal)
            }
        })
        //简写
        vm.$wtach('username',function(newVal,oldVal){
            console.log(newVal,oldVal)            
        }
        })        
       
    </script>
</body>

应用场景

使用watch检测用户名是否可用,监听username值得变化,并使用axios发起ajax请求,
检测当前输入的用户名是否可用

<script>

import axios from 'axios'

export default {
    name:'MyCount',
    data(){
        return{
            username:'',
        }
    },
 watch:{
    async username(newVal,oldVal){
        console.log(newVal,oldVal)
        const {data:res}=await axios.get('https://www.escook.cn/api/finduser'+newVal)
        //await简化promise实例对象为数据
        console.log(res)
    }
 }

}
</script>
immediate选项

默认组件在初次加载完毕后不会调用watch侦听器,如果想让watch侦听器立即被调用,则需要使用
immediate
选项

<script>

import axios from 'axios'

export default {
    name:'MyCount',
    data(){
        return{
            username:'',
        }
    },
 watch:{
//不能直接定义成方法,要让监听的数据指向一个配置对象
  username:{
    async handler(newVal,oldVal){
     const {data:res}=await axios.get('https://www.escook.cn/api/finduser'+newVal)
        //await简化promise实例对象为数据
        console.log(res)
},
      //立即触发watch侦听器
      immediate:true,
      
 }
 }

}
</script>
deep选项

当watch侦听的是一个对象,如果
对象中的属性值发生了变化
,则
无法被监听到
,需要使用
deep
选项侦听所有属性的变化。

<script>

import axios from 'axios'

export default {
    name:'MyCount',
    data(){
        return{
            username:'',
        }
    },
 watch:{
//不能直接定义成方法,要让监听的数据指向一个配置对象
  username:{
    async handler(newVal,oldVal){
     const {data:res}=await axios.get('https://www.escook.cn/api/finduser'+newVal)
        //await简化promise实例对象为数据
        console.log(res)
},
      deep:true,//监视多级结构(对象)中所有属性的变化
 }
 }

}
</script>

监视多级结构(对象)中单个属性的变化

<script>

import axios from 'axios'

export default {
    name:'MyCount',
    data(){
        return{
            info:{username:'zs',password:'123456'}
            
        }
    },
 watch:{
//不能直接定义成方法,要让监听的数据指向一个配置对象
  'info.username':{//只想监听info.username的属性变化
    async handler(newVal,oldVal){
     const {data:res}=await axios.get('https://www.escook.cn/api/finduser'+newVal)
        //await简化promise实例对象为数据
        console.log(res)
},
   
      deep:true,
 }
 }

}
</script>

计算属性vs侦听器

  • computed能完成的功能,watch都可以完成,都能完成的,优先使用computed。
  • watch可以完成的,computed不一定能完成,例如watch可以处理
    异步任务
  • 计算属性和侦听器
    侧重的应用场景不同。
  1. 前者侧重监听
    多个值
    的变化,最终计算
    返回一个新值

  2. 后者侧重于
    单个数据
    的变化,最终
    执行特定的业务处理,不需要任何返回值

两个原则:

  • Vue所管理的函数,最好写成普通函数,这样this的指向才是vm或组件实例对象,否则指向window。
  • 所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数、Promise的回调函数),最好写成箭头函数,这样this的指向才是vm或组件实例对象,否则指向window。

绑定样式

绑定class样式

<!--字符串写法,适用于样式的类名不确定,需要动态指定-->
<h1 class="basic" :class:"className">Hello World!</h1>
<!--数组写法,适用于样式的个数和类名都不确定-->
<h1 class="basic" :class:"classArr">Hello World!</h1>
<!--对象写法,适用于样式的个数和类名都确定,但要动态觉得用不用-->
<h1 class="basic" :class:"classObj">Hello World!</h1>

绑定style样式

<!--动态指定-->
<h1 class="basic" :style="{fontSize: fsize+'px';}">Hello World!</h1>
<h1 class="basic" :style="styleObj">Hello World!</h1>
<h1 class="basic" :style="[styleObj1,styleObj2]">Hello World!</h1>

过滤器

(Vue3.x废弃,用计算属性、方法代替)

什么是过滤器?

过滤器(Filters)常用于
文本的格式化
,本质是一个函数

过滤器应该被添加到js表达式的
尾部
,由“
管道符
”进行调用

  • 用于插值表达式
  • 用于v-bind属性绑定

定义(局部)过滤器

在创建vue实例期间,可以在filters节点中定义过 滤器

私有和全局过滤器

私有过滤器:只能在被vm实例控制的区域下控制

如果想在多个vue实例之间共享过滤器,则可以按照如下格式声明全局过滤器:

 <script>
        vue.filter('capitalize',(str)=>{
            return str.charAt(0).toUpperCase() + str.slice(1)+'--'
        })
        new Vue...
</script>

注:如果二者冲突以私有过滤器为准,就近原则

连续调用多个过滤器

过滤器可以串联的地调用

{{message | capitalize | maxlength}

过滤器传参

过滤器本质是js函数,第一个参数永远是
管道符
前面的值,第二个参数开始才是arg1、arg2...

{{message | filterA(agr1,arg2)}
 
vue.filter('filterA',(msg,arg1,aarg2)=>{})

过滤器的兼容性

在vue3.x版本中已经剔除了过滤器相关功能,可以使用计算方法或属性来代替