深入理解消息队列(场景,对比,原理和设计思想)

发表于 3年以前  | 总阅读数:296 次

导语 | 消息队列也通常称为消息中间件,提到消息队列,大部分互联网人或多或少都听过该名词。对于后端工程师而言,更是日常开发中必备的一项技能。随着大数据时代的到来,apache旗下的Kafka一度成为消息队列的代名词,提起消息队列大家自然而然就想到了Kafka。然而消息队列本身是工程领域内一种解决问题的通用方案。它的背后有着一些通用的设计思想和经典模型,这些是消息队列的精髓和灵魂。它们独立于任何一种消息队列的具体实现(例如Kafka),但每种消息队列(除了Kafka外,还有RocketMQ、Pulsar等)的实现中到处体现着这些设计思想。本文腾讯Cloud9项目组后台开发工程师文小飞(Jaydenwen)主要从抽象层面来简单谈谈消息队列背后的一些设计思想,辅助理解消息队列这一类组件。

本文主要解决三个问题:

1. 消息队列适合什么场景? 2. 消息队列有哪些主流产品、各自的优缺点? 3. 消息队列背后的设计思想(整体核心模型、数据存储考量、数据获取方案对比、消费者消费模型)

一、消息队列适合哪些场景

消息队列:它主要用来暂存生产者生产的消息,供后续其他消费者来消费。它的功能主要有两个:a.暂存(存储)、b.队列(有序:先进先出)。其他大部分场景对数据的消费没有顺序要求,主要用它的暂存能力 。从目前互联网应用中使用消息队列的场景来看,主要有以下三个:

  • 异步处理数据
  • 系统应用解耦
  • 业务流量削峰

下面对上述每一种场景进行简单描述。

(一)异步处理数据

第一个例子我们以现实生活中送快递来类比,在该例子中我们把暂存快递的快递柜比作暂存数据的消息队列。我们来看一下在现实生活中,没有快递柜时,快递员把快递送到目的地后,一般需要联系收货人来签收快递,如果收货人此时有空,那一切都很顺利。但如果收货人此时不方便(开会、正在吃午饭、外出出差……)。那对于快递员而言,就很尴尬,需要一直等待(开会or吃午饭)或者将快递拿回去(外出出差),导致白跑一趟。这对于快递员而言简直太不友好。

从这儿可以看出,当快递员送货时,是一个同步状态,即需要等待收货人签收后才能去送下一趟单子,对快递员而言效率太低。上述例子虽然有点牵强,大家凑合理解,意思能大概理解到位就ok。

接着我们再来看一下,当有了快递柜后,对于快递员而言,每次需要送快递时,只需要将快递投掷到快递柜,然后再通过短信或者电话通知收货人具体的快递信息即可。他就可以继续去派送下一单。而对于收获人而言,也可以根据具体方便的时间来取件。这样一来,二者完全异步了,不用相互等待了。

在这个例子中,如果把快递员比作生产者,收货人比作是消费者,则快递柜就类似于消息队列。我们可以通过采用消息队列来实现异步数据的处理。

(二)系统应用解耦

案例二,我们以目前最主流的推荐系统中内容的流转来举例。在推荐系统中当创作者发布了一条内容后,该内容会首先经过安全部分的相关审核,通过审核后的内容,通常需要进行内容入库存储、送入模型进行特征的计算和生成。

假如后期我们想提升推荐的效果,需要单独构建一份冷启动的推荐池,此时也需要用到这部分内容,那问题来了,在没有使用消息队列时,对于上游服务而言,需要通过扩展新的逻辑来实现该功能。同时在该场景里,会存在依赖三个下游服务,如果其中一个下游服务失败后,该如何处理,是重试还是返回失败等这些细节的处理。如果后期这部分数据还想在其他渠道分发,那又该如何对接。明显这种场景下面临着系统紧耦合的问题。

我们再来看一下,如果我们一开始就引入了消息队列,那问题又会变成怎样的呢?当内容审核通过后,就直接将数据生产出来丢到消息队列中,下游的多个服务再从消息队列消费数据。当后续这一份数据需要扩展供其他系统使用时,也只要通过新的消费者来接入到消息队列消费就ok。上游生产消息的模块不要做任何的改动。这样我们就通过消息队列进行了系统应用之间的解耦。这是消息队列的第二个用途。

(三)业务流量削峰

消息对应的第三个使用场景便是削峰。在现如今的互联网世界中,电商场景中每年的618秒杀活动、双11抢购便是最典型的案例。这种场景中系统的峰值流量往往集中于一小段时间内,平常的流量比较可控,所以为了防止系统在短时间内的峰值流量冲垮,往往采用消息队列来削弱峰值流量,高峰值期间产生的订单消息等数据首先送入到消息队列中暂存,然后供下游系统根据自己的消费能力来逐步处理。同时这类消息往往对时延的要求不是很高,比较适合采用消息队列暂存。

(四)小结

最后我们在对本节的内容做一个简单的总结,上面通过三个简单的实例介绍了消息队列的典型的三个使用场景:异步、解耦、削峰。换个角度来理解可以看到,消息队列主要适用于处理对消息要求不是很实时,同时一份数据可能会多处使用的场景,不同的使用方处理速率不同。更多的消息队列的使用场景读者可以自行找资料阅读和总结。

二、消息队列“家族”有哪些成员

(一)消息队列主流产品

上图根据时间线展示了不同时间点产生的消息队列产品,主要的产品有:ActiveMQ(2003)、RabbitMQ(2006)、Kafka(2010)、RocketMQ(2011)、Pulsar(2012)。这些消息队列中或多或少我们都听过一些,部分也在项目中真实使用过。下面对上述几个消息队列做一个简单的介绍。

ActiveMQ:ActiveMQ由Apache软件基金会基于Java语言开发的一个开源的消息代理。能够支持多个客户机或服务器。计算机集群等属性支持ActiveMQ来管理通信系统。

RabbitMQ:RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。RabbitMQ支持多种消息传递协议、传递确认等特性。

Kafka:Apache Kafka是由Apache软件基金会开发的一个开源消息系统项目,由Scala写成。Kafka最初是由LinkedIn开发,并于2011年初开源。2012年10月从Apache Incubator毕业。该项目的目标是为处理实时数据提供一个统一、高通量、低等待的平台。Kafka是一个分布式的、分区的、多复本的日志提交服务。它通过一种独一无二的设计提供了一个消息系统的功能。

RocketMQ:Apache RocketMQ是一个分布式消息和流媒体平台,具有低延迟、强一致、高性能和可靠性、万亿级容量和灵活的可扩展性。它有借鉴Kafka的设计思想,但不是kafka的拷贝。

Pulsar:Apache Pulsar是Apache软件基金会顶级项目,是下一代云原生分布式消息流平台,集消息、存储、轻量化函数式计算为一体,采用计算与存储分离架构设计,支持多租户、持久化存储、多机房跨区域数据复制,具有强一致性、高吞吐、低延时及高可扩展性等流数据存储特性,被看作是云原生时代实时消息流传输、存储和计算最佳解决方案。

(二)不同消息队列对比

上图详细的展示了几种消息队列的各自功能及优缺点,首先,ActiveMQ和RabbitMQ二者属于同一量级,在吞吐量上比后面三者差一个量级;其次,支持强一致性的有RocketMQ和Pulsar,在对消息一致性要求比较高的场景可以采用这些产品。同时kafka虽然会有数据丢失的风险,但其吞吐量比较高同时社区非常活跃,在大数据的绝大部分场景里,都可以采用Kafka;最后Kafka、RocketMQ、Pulsar这几款是基于磁盘存储数据的,内存加速访问。而ActiveMQ、RabbitMQ采用内存存储数据,也支持数据持久化到磁盘。

三、消息队列背后的设计思想

在前面,第一节内容中,主要介绍了为什么要使用消息队列,消息队列适合解决哪些问题?在第二节内容中,又介绍了有哪些可选择的消息队列,以及他们之间各自的优缺点。这一节是最重要的内容,主要会介绍一下上述消息队列背后的通用的一些设计思想。部分思想可以扩展到其他的业务模型或者领域内。后面讲到对应内容也会有所提及。

(一)消息队列核心模型

上图是几乎所有消息队列设计的一个核心模型。对于一个消息队列而言,从数据流向的维度,可以拆解为三大部分:生产者、消息队列集群、消费者,数据是从生产者流向消息队列集群,最终再从消息队列集群流向消费者,下面对这几个概念进行一一阐述。

生产者:生产数据的服务,通常也称为数据的输入提供方,这里的数据通常指我们的业务数据,例如推荐场景中用户对内容的点击数据、内容曝光数据、电商中的订单数据等等。生产者通常是作为客户端的方式存在,但在支持事务消息的消息队列中,生产者也被设计为服务端,实现事务消息这一特性。其次生产者通常会有多个,消息队列集群内部也会有多个分区队列,所以在生产者发送数据时,通常会存在负载均衡的一些策略,常见的有按key hash、轮询、随机等方**式。其**本质是一条数据,被消息队列封装后也被称为一条消息,该条消息只能发送到其消息队列集群内部的一个分区队列中。因此只需按照一定的策略从多个队列中选择一个队列即可

消息队列集群: 消息队列集群是消息队列这种组件实现中的核心中的核心,它的主要功能是存储消息、过滤消息、分发消息

其中存储消息主要指生产者生产的数据需要存储到消息队列内部;存储消息可以说是消息队列的核心,一个消息队列吞吐量的高低、性能优劣都和它的存储模型脱不开关系。这部分内容会在下一部分(3.2)进行介绍。

过滤消息只指消息队列可以通过一定的规则或者策略进行消息的过滤,该项能力通常也被称为消息路由;过滤消息属于高阶的特性功能,AMQP协议对这些能力抽象的比较完备,部分消息队列可以选择性的实现该协议来达到该功能,关于AMQP协议内容读者可以自行搜索资料阅读,此处不再展开。

分发消息是指消息队列通常需要将消息分发给处理同一逻辑的多个消费者处理或者处理不同逻辑的不同消费者处理。分发消息可以说和消费者模型想挂钩,这块会涉及到不同的数据获取方式,也会涉及到消费者消费消息的模型。

此外绝大部分的消息队列也都支持对消息进行分类,分类的标签称为topic(主题),一个topic中存放的是同一类消息。

消费者:最终消息队列存储的消息会被消费者消费使用,消费者也可以看做消息队列中数据的输出方。消费者通常有两种方式从消息队列中获取数据:推送(push)数据、拉取(pull)数据。其次消费者也经常是作为客户端的角色出现在在消息队列这种组件中。

(二)消息队列数据组织方式

在这一节中,我们详细看看消息队列存储消息这个环节的一些权衡考量,通常数据的存储无外乎就是两种,一种是存储在非易失性存储中,例如磁盘这种介质;另一种是选择存储在易失性存储中,典型的就是内存。关于二者的对比大家可以参考下表,此处就不再赘述。

通常在大部分组件设计时,往往会选择一种主要介质来存储、另一种介质作为辅助使用。就拿redis来说,它主要采用内存存储数据,磁盘用来做辅助的持久化。拿RabbitMQ举例,它也是主要采用内存存储消息,但也支持将消息持久化到磁盘。而RocketMQ、Kafka、Pulsar这种,则是数据主要存储在磁盘,通过内存来助力提升系统的性能。关系型数据库例如mysql这种组件也是主要采用磁盘组织数据,合理利用内存提升性能。

针对采用内存存储数据的方案而言,难点一方面在于如何在不降低访问效率的情况下,充分利用有限的内存空间来存储尽可能多的数据,这个过程中少不了对数据结构的选型、优化;另一方面在于如何保证数据尽可能少的丢失,我们可以看到针对此问题的解决方案通常是快照+广泛意义的wal文件来解决。此类典型的代表就是redis啦。

针对采用磁盘存储数据的方案而言,难点一方面在于如何根据系统所要解决的特点场景进行合理的对磁盘布局。读多写少情况下采用b+树方式存储数据;写多读少情况下采用lsm tree这类方案处理。另一方面在于如何尽可能减少对磁盘的频繁访问,一些做法是采用mmap进行内存映射,提升读性能;还有一些则是采用缓存机制缓存频繁访问的数据。还有一些则是采用巧妙的数据结构布局,充分利用磁盘预读特性保证系统性能。

总的来说,针对写磁盘的优化,要不采用顺序写提升性能、要不采用异步写磁盘提升性能(异步写磁盘时需要结合wal保证数据的持久化,事实上wal也主要采用顺序写的特性);针对读磁盘的优化,一方面是缓存、另一方面是mmap内存映射加速读

上述这些存储方案上权衡的选择在Kafka、RocketMQ、Pulsar中都可以看到。其实抛开消息队列而言,这些存储方案的选择上无论是关系型数据库还是kv型组件都是通用的。

下图列举了几种磁盘上的数据组织方式,仅供大家参考。

(三)获取数据的推、拉两种方案对比

在前面3.1节中提到,消费者在从消息队列中获取数据时,主要有两种方案:

  • 等待推送数据
  • 主动拉取数据

目前的消息队列实现时,都会选择支持两种的至少一种,关于这两种方案的对比,大家可以参考下表。

在此处,个人想抛开消息队列谈一点关于这两种方案的理解,其实推拉模型不仅仅只用于消息队列这种组件中,更一般意义上,它解决的其实是数据传送双方的一个问题。本质是数据需要从一方流向另一方。顺着这个思路来看,下面这三个例子都是遵循这个原则。

网络中传输的数据:在IO多路复用中,以epoll为例,当内核检测到监听的描述符有数据到来时,epoll_wait()阻塞会返回,上层用户态程序就知道有数据就绪了,然后可以通过read()系统调用函数读取数据。这个过程就绪通知,类似于推送,但推送的不是数据,而是数据就绪的信号。具体的数据获取方式还是需要通过拉取的方式来主动读。

feeds流系统用户时间线后台实现方案(读扩散、写扩散): 读扩散和写扩散更是这样一个case。对于读扩散而言,主要采用拉取的方式获取数据。而对于写扩散而言,它是典型的数据推送的方式。当然在系统实现中,更复杂的场景往往会选择读写结合的思路来实现。

生活中的点外卖例子:当下单点外卖时,通常也会有两种方式可以选择:外卖派送、到店自取。不过通常外卖派送比较实时,我们通常就选择这种方式了而已。可以看出外卖派送其实就是一种推的方式,而到店自取,则是拉的方式。

(四)消息队列消费模型

本节我们来介绍最后一部分内容,消息队列中消费者的消费模型。下图中上半部分展示了最简单的一种消费模型。一个生产者、一个消费者。但往往我们的一份数据通常会被不同场景所使用。那这个时候,首先就会存在每种场景需要使用全量的数据、而且不同场景之间不会相关影响,彼此独立。方便理解起见,我们假设有N个场景需要使用这同一份数据,每个场景需要消费全量的数据。而在N个场景中的一种场景里,又会有多个消费者一起分摊消费这些数据。我们假设一个场景里有M个消费者。由于每个场景中包含M个消费者,我们也将其采用消费者组来描述。通过上面的介绍,我们可以用下面一句话总结消息队列中的消费模型:

消费者消费者模型其实是一个1:N:M的关系,一份数据被N个消费者组独立使用,每个消费者组中有M个消费者进行分摊消费

其实这种模型也称为发布订阅模型,对于一条消息而言,组间广播、组内单播。一条消息只能被一个消费者组中的一个消费者使用。在消费者组内部也存在一些负载均衡的策略。常用的有:轮询、随机、hash、一致性hash等方案。

(五)小结

第三部分内容我们重点介绍了关于消息队列背后的一些设计思想,其中包括:消息队列的核心模型、数据存储模型、推拉方案获取数据对比、消费者消费模型。其中数据存储模型不仅仅适用于消息队列,也同样适用于其他数据存储组件的方案选择。同理数据获取的推拉两种方案也不仅仅局限于消息队列。我们可以在很多业务场景里看到同类思想的影子。

四、总结

到此,本文也就告一段落了。本文主要从理论、抽象层面泛泛的谈了下关于消息队列的一些思想和理念。主要介绍了消息队列的使用场景,主流的消息队列可选方案以及他们之间的优缺点。最后介绍了一些关于消息队列背后的设计理念。本文只是抛砖引玉,希望上述内容能辅助大家一起重新认识消息队列。后面会逐步挑选上述的几种消息队列(kafka、RocketMQ、Pulsar),重点分析其内部实现机制,敬请期待。限于个人水平有限,理解有误之处欢迎大家批评指正。

参考资料 1. ActiveMQ与RabbitMQ的区别

2. Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理

3. Kafka、RabbitMQ、RocketMQ等消息中间件的对比

4. Apache Pulsar 在腾讯计费场景下的应用

5. kafka Push vs. pull

6. 消息队列-推/拉模式学习 & ActiveMQ及JMS学习

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/OlRR1vtfeEMiJaqFnUwg6Q

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237289次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8126次阅读
 目录