InfoQ

Article: Distributed Spring Batch Without a Message Broker

8.5内容质量
Article: Distributed Spring Batch Without a Message Broker

TL;DR · AI 摘要

Spring Batch可通过关系数据库实现分布式处理而无需消息代理,适合中小团队的横向扩展需求。

核心要点

  • 使用关系数据库作为协调平面可替代消息代理实现分布式Spring Batch
  • 通过心跳和两阶段恢复机制处理节点故障,确保分区不丢失
  • 数据库协调方案在数据库争用临界点后不适用于超大规模集群

结构提纲

按章节快速跳转。

  1. Spring Batch作为JVM批处理框架面临横向扩展时的挑战。

  2. 利用关系数据库作为协调平面替代消息代理实现分布式处理。

  3. 通过心跳检测和两阶段恢复机制处理节点失效问题。

  4. 定义可转移工作和不可转移工作的不同执行语义合同。

  5. 数据库协调方案适用于中小集群但存在数据库争用上限。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • 分布式Spring Batch无消息代理
    • 核心机制
      • 数据库协调替代消息代理
      • 分区分配与心跳检测
    • 故障处理
      • 两阶段恢复机制
      • 分区重新分配
    • 正确性保证
      • 可转移工作语义
      • 不可转移工作语义

金句 / Highlights

值得收藏与分享的关键句。

#Spring Batch#分布式系统#Java#消息队列#数据库协调
打开原文

无需消息代理的分布式 Spring Batch - InfoQ

InfoQ 首页 文章 无需消息代理的分布式 Spring Batch

Java

QCon 旧金山(11 月 16-20 日):深入的技术会议。改变你思维方式的同行对话。

无需消息代理的分布式 Spring Batch

2026 年 10 月 21 日 19 分钟阅读

作者:

  • Janardhan Reddy Chejarla

审阅者:

  • Michael Redlich

#### 关注我们

YouTube

232K 粉丝

LinkedIn

26K 粉丝

Instagram

新

RSS

19K 读者

X

57.1k 粉丝

Facebook

21K 点赞

Bluesky

收听本文 -

0:00

音频准备播放

您的浏览器不支持音频元素。

正常

1.25x

1.5x

喜欢

新下拉阅读列表

  • 阅读列表

关键要点

  • Spring Batch 可以通过使用其本身已要求的数据库作为协调平面,实现无需消息代理的水平扩展。这种方案适合许多团队运行的小型到中型集群。
  • 该模式解决了消息代理通常为您处理的问题:根据当前节点数量分配工作量、在没有固定协调者的情况下为每个执行分配主节点职责、通过比较并设置写入方式声明分区,确保没有分区被静默丢失。
  • 节点故障通过心跳和两阶段恢复处理。它将过时节点标记为不可达,然后在宽限期窗口后将其移除,并重新分配其未完成的可转移分区。
  • 正确性是一个明确的契约。可转移工作至少执行一次,必须是幂等的;不可转移工作最多执行一次,失败而非转移。分区永远不会被静默丢失。
  • 数据库协调最适合操作简单性和成本至关重要的场景。其上限是数据库竞争,超过该点,在非常大的集群或亚秒级延迟场景中,消息代理仍然是更好的选择。

介绍

Spring Batch 已经是 JVM 上批量处理的默认框架十多年。作业由步骤构建,如面向块的读取器、处理器和写入器,以及一个使运行可重启的元数据存储库。对于单机上的夜间抽取、转换、加载(ETL)、财务对账和数据验证工作负载,很难有比它更好的选择。

当一台机器不再足够时压力就会显现。原本能适应其窗口的对账运行随着业务增长变得无法容纳,自然的反应是将工作分散到多个节点。Spring Batch 的既定方案是远程分区。一个管理步骤将工作划分为分区,并通过消息代理(如 Apache Kafka、RabbitMQ 或 JMS 提供商)通过 Spring Integration 发送到工作节点。

远程分区是一种经过验证的方法。当平台已经包含消息代理时,这是正确的选择。但消息代理是一个额外的操作层级,许多团队否则永远不会运行一个。他们已经运营着一个管理良好的关系数据库(无论怎样,Spring Batch 都需要一个用于其作业存储库)。团队对仅为了分发批量分区而建立、保护和监控消息集群几乎没有兴趣。

#### 相关赞助商

#### 相关赞助商

  • 2026 年 10 月 29 日,美国东部时间下午 1 点 从 Token 到 Features:为 AI 辅助工程构建成本归属架构 演讲者:Martin Reynolds - Harness 字段 CTO,Barani Mohandoss - Trimble 云治理、FinOps 与 E-工具总监

本文描述了一种替代方案:仅使用共享数据库,协调一个小型到中型集群中的分布式、容错的Spring Batch作业。这不是对消息代理的否定,而是说明在不使用消息代理的情况下完成该作业所需付出的代价,以及该方法的适用边界和消息代理仍为更优工具的场景。

单节点批处理的终点与消息代理问题的起点

若将分布式批处理简化到核心要素,通过协调机制需要回答四个问题。

第一,工作如何划分,由多少个工作者执行?划分过于粗糙会导致节点空闲;划分过于细致则会因开销而得不偿失。合适的数量取决于作业启动时的可用容量。

第二,某个运行实例的负责人是谁?必须有某个实体来分配工作、跟踪执行进度并最终完成作业。在基于消息代理的分区方案中,这个角色由运行管理步骤的节点承担。

第三,工作者如何在不被其他工作者同时执行同一分区的情况下取得分区所有权?如果某个分区被重复执行,可能导致数据损坏;如果未被执行,则作业会悄无声息地不完整。

第四,当节点在运行过程中宕机时会发生什么?必须检测到其正在进行的工作,并在安全的前提下自动恢复,同时不违反第三条要求。

消息代理会帮助你回答其中一些问题。它的队列会将每条消息发送给单一消费者,调度与执行相互解耦,因此即使工作者运行缓慢或宕机,也不会阻塞生产者。若移除消息代理,所有问题都必须通过SQL显式回答,且需要依赖为持久化共享状态而设计的存储,而非消息分发机制。本文其余部分将探讨如何更好地回答这些问题。

数据库协调分区的结构设计

该设计将Spring Batch本身所需的数据库视为唯一的协调方案,不使用消息代理或类似Apache ZooKeeper的协调服务,也不依赖任何部署平台。集群状态的每个部分都存储在普通SQL表中,因此整个集群的状态可通过SELECT语句进行检查。

Spring Batch自身的模式通过三个表进行扩展。BATCH_NODES表是节点注册表,每行对应一个节点,包含其状态(活动或不可达)、最后心跳时间戳和当前负载。BATCH_PARTITIONS表跟踪分区生命周期,每行对应一个分区,通过该分区自身的step-execution ID作为键(每个分区本身都是一个Spring Batch StepExecution,与管理运行范围的管理步骤不同),包含分配的节点、状态(待处理、已声明、已完成或失败)以及在失败时是否可以转移到其他节点。BATCH_JOB_COORDINATION表将每个作业执行绑定到其主节点和管理步骤,并包含其自身的状态。此处重要的是这三个表的结构,而非完整的列列表。

这种设计并未消除对共享的、持久化作业仓库的需求,但这一需求是分布式分区固有的特性,而非该模式引入的新要求。分区的负载数据以及描述其工作切片的执行上下文,由主节点写入并由执行该分区的节点读取。主节点随后读取结果以进行聚合。这种交接过程涉及跨JVM的Spring Batch元数据交换,因此存储必须是共享且持久化的。Spring Batch自身的远程分区也要求同样的条件。在远程分区中,代理仅携带分区的指针,而非其数据。这种设计移除了代理,而非仓库。单一的共享数据库同时充当元数据存储和协调平面。

协调机制在设计上是去中心化的。整个集群中不存在被选举出的全局主节点。这种做法会重新引入主节点选举机制和单点故障问题,而这正是移除代理所避免的。相反,启动作业的节点在本次执行中成为该作业的主节点(后续章节将详细说明其职责)。在两个不同节点上启动的两个作业,将同时拥有两个不同的主节点。由于主节点职责仅限于单个作业执行,主节点失效仅影响该作业,而不会波及整个集群。不存在需要选举的协调者,也不存在可能丢失的协调者。

重用Spring Batch的分区钩子:入口点

对Spring Batch开发者而言,首要的实用问题是:我的代码应在哪里插入,以及需要修改多少内容?答案是几乎无需修改,这使得该方法具有可行性。这种简洁性源于Spring Batch一直提供的扩展点。

Spring Batch已将分区步骤拆分为两个独立职责。Partitioner接口决定如何拆分工作,它生成一组命名的ExecutionContext对象,每个分区对应一个对象,每个对象描述输入的一个切片。单独的PartitionHandler决定这些分区如何执行。内置的TaskExecutorPartitionHandler在本地线程中执行它们,而Spring Batch自身的远程分区则使用MessageChannelPartitionHandler通过代理传输它们。管理步骤由这两部分加上一个StepExecutionAggregator组合而成,该聚合器将结果合并:

code
new StepBuilder("reconcileStep.manager", jobRepository)
    .partitioner("reconcileStep", clusterAwarePartitioner) // 如何拆分工作
    .partitionHandler(clusterAwarePartitionHandler)         // 如何分布分区
    .step(reconcileStep)                                    // 普通的工作者步骤
    .aggregator(aggregator)
    .build();

这行partitionHandler(...)代码就是整个扩展点。由于Spring Batch将分区分布视为可插拔策略,该方法提供了自定义的PartitionHandler,它将分区记录写入数据库,并在代理处理器通常占用的槽位中监控这些记录。分区器是Spring Batch原生Partitioner的子类。其他部分无需改动。工作者步骤(如上文的reconcileStep)是普通的基于块或任务的步骤,结果通过标准的StepExecutionAggregator聚合,重启则通过标准的JobRepository实现。熟悉本地分区的开发者只需编写相同的管理步骤,并修改两个Bean——分区器和分区处理器即可。

该配置涵盖了管理器部分。将相同的可部署单元转换为集群成员属于配置范畴,而非代码修改。通过一个标志位即可启用该行为,自动配置会注册节点、启动心跳机制并启动轮询器,无需引入消息代理客户端或额外的Bean。几个时间间隔参数用于调节该行为,包括心跳频率、工作节点轮询频率、单节点并发数,以及两个控制故障转移的心跳超时阈值,这些参数都配有合理的生产环境默认值(节点在约十秒后被标记为不可达,约三十秒后被移除)。

从此时起,每个节点(包括主节点和工作节点)都会运行一个循环。启动时,节点会将自身信息插入到BATCH_NODES表中,并按照配置的时间间隔发送心跳,同时发布当前负载信息(即当前正在运行的分区数量)。每次轮询时,节点会扫描BATCH_PARTITIONS表中分配给自己的行记录,通过后续展示的CAS(Compare-and-Set)更新操作进行认领,然后执行保持编程模型一致的关键操作:通过名称查找对应的工作步骤并执行。该名称并非随意指定,而是通过管理器的.partitioner("reconcileStep", ...)方法传递的名称,该名称被记录在协调行中,因此工作节点能够解析相同的Spring Batch步骤Bean,并使用该分区的ExecutionContext执行。

每个节点同时最多运行配置指定数量的分区,当某个步骤执行完成后,会将其标记为COMPLETED或FAILED。

因此,这种协调机制依托于Spring Batch已有的扩展点,通过配置而非新增代码即可启用。

在Spring Batch集群中的实现

图1. 架构图:每个作业都有一个独立的主节点和工作节点,通过共享数据库进行协调。(来源:作者原创)

图1展示了运行时架构。一组对等的JVM节点持续注册到BATCH_NODES表中并发送心跳。当其中一个节点启动作业时,该节点会作为该次运行的主节点。它会查询活跃节点,根据节点数量调整拆分规模,写入分区记录,并监控直至完成,而所有节点(包括主节点)都会轮询并执行分配给自己的分区任务。

主节点步骤如何驱动运行

主节点步骤并非特殊存在。它就是一个普通的Spring Batch分区(管理器)步骤,运行在启动作业的节点上。使该节点成为主节点的关键在于PartitionHandler的执行位置。当Spring Batch执行管理器步骤时,它会在该节点上调用处理器的handle(...)方法,而基于数据库的PartitionHandler会在该节点上阻塞,直至整个分布式运行完成。

在该单次调用中,主节点按顺序执行四个操作。首先,它会记录运行状态,在BATCH_JOB_COORDINATION表中写入新行,将本次作业执行与管理器步骤的执行ID关联作为运行标识,并标记为STARTED状态。然后,它会在BATCH_PARTITIONS表中为每个分区写入一行PENDING状态记录,每条记录都标记了分配的目标节点。接着,它启动监控循环,以短时间间隔唤醒,重新分配因节点移除而成为孤儿的分区(这些分区的分配节点已从BATCH_NODES表中消失),并检查是否有分区仍处于PENDING或CLAIMED状态。最后,当所有任务处理完毕后,如果任何分区最终状态为FAILED,主节点会将管理器步骤标记为失败;否则,它会将协调行标记为COMPLETED,将工作节点的步骤执行结果提交给StepExecutionAggregator,并完成整个作业。

企业场景

考虑在动态分配的云节点上进行夜间财务对账。数据集规模足够大,如果某个节点错过批次窗口,一个管理良好的关系型数据库已经就位。运维团队对仅为此工作负载设置一个代理没有兴趣。同样的场景也出现在中等规模的ETL和数据验证任务中,每个分片都是必须在节点故障时可恢复的输入切片。这些就是该模式针对的条件。

将工作量适配到实时集群

标准的远程分片在了解可用容量之前就分割工作。网格大小通常是盲目选择的,经常作为固定常量,然后将这些碎片发送到代理中,等待容量释放时被拾取。没有代理的情况下,你必须直接面对这个问题。在分割之前,主节点会询问数据库当前哪些节点处于活动状态,并将该数量传递给分片器。分片器暴露一个方法,接收当前活动节点数量并返回分片,因此分割规模会适配实际存在的集群。

每个分片都表示为Spring Batch的ExecutionContext,这是框架用于分片输入的关键值载体。定义一个工作切片的参数(行范围、文件名和ID窗口)被放入其中,而工作者步骤会读取这些参数以了解需要处理什么内容。由于分片器接收到实时节点数量,它可以为当前集群选择分片数量,每个活动节点分配一个或两个分片,或者根据工作负载需求进行调整。

分配策略随后将这些分片映射到节点上:使用轮询实现均匀分布,或固定节点数量将任务限制在n个节点上,或者使用最低负载策略,将每个分片分配给当前负载最低的节点,将工作导向最空闲的节点。由于分配既考虑容量规模又感知负载,分割会匹配实际存在的集群,而不是提前固定的数字。分配在此时固定下来。这是放置操作,而非运行时重新平衡,因此提前完成的节点不会从较慢的对等节点拉取分片,只有节点故障才会转移工作。这种运行时重新平衡是代理的拉取语义免费提供的功能,而该模式则放弃了这一特性。

在不重复执行的情况下声明工作

一旦分片被写入并分配,每个工作者会独立轮询其PENDING状态的分片,并在执行前将其转换为CLAIMED状态。这个转换是关键的正确性时刻,每个工作者转换都由一个单一的受保护语句驱动,目标状态作为参数传入。

code
-- 声明后,后续完成或失败:一个受保护的UPDATE
UPDATE batch_partitions
SET    status = ?,                 -- CLAIMED,然后是COMPLETED或FAILED
       last_updated = CURRENT_TIMESTAMP
WHERE  step_execution_id = ?
  AND  assigned_node = ?
  AND  status IN ('PENDING', 'CLAIMED');

WHERE 子句是关键所在。它解释了为什么 CLAIMED 会同时出现在两侧。它只允许状态仍为 PENDING 或 CLAIMED 的行存在,因此一旦某个分区达到终端状态(COMPLETED 或 FAILED)或被重新分配,后续的写入操作将无法影响它。对于声明操作,行的初始状态是 PENDING。同时,允许 CLAIMED 状态使得重新声明操作具有幂等性,并允许同一语句处理后续的 COMPLETED 和 FAILED 写入操作,这些操作从 CLAIMED 状态开始。

两个特性防止了重复执行。首先,每个分区在分区分配时仅被分配给一个节点,因此在正常运行情况下,只有该节点会轮询其对应的行。其次,工作者节点的写入操作和主节点的重新分配操作都受到保护。工作者节点的写入操作受到 assigned_node 和 PENDING/CLAIMED 状态的保护,而重新分配操作则通过该状态保护,在将行重新置为 PENDING 状态时会切换到新节点。因此,如果一个卡顿的节点在其分区完成的同时主节点正进行重新分配,第二次写入将不会匹配任何内容。如果主节点先执行,完成操作将失败,因为 assigned_node 不再指向该节点。如果节点先执行,重新分配将失败,因为状态已处于终端状态。无论如何,只会有一个写入操作生效。这不是一个通过 SELECT ... FOR UPDATE SKIP LOCKED 语句实现的争用式拉取队列。分区是预先分配的,因此受保护的更新操作管理生命周期,而非调度。代理通过队列提供的“一个分区对应一个节点”的保证,来源于行级别的状态转换。

从节点故障中恢复

图2. 节点生命周期与两阶段故障转移。(来源:作者原创。)

节点会发生故障。此处的活跃性是直接跟踪的,而非通过代理确认推断得出。每个节点定期使用数据库时钟而非自身时钟更新心跳时间戳。陈旧性检查对比的是同一时钟,因此活跃性不受机器本地时钟偏差的影响。检测和移除是去中心化的。每个节点都运行清理扫描,因此单个监控节点的故障不会导致集群失明。不过,移除节点的分区重新分配由每个受影响任务的主节点在前述监控循环中完成。

恢复是一个两阶段过程,如图2所示。第一阶段,心跳时间早于不可达阈值(默认10秒)的节点将从活跃状态切换为不可达状态。第二阶段,仍处于陈旧状态超过更长清理阈值(30秒)的节点将被移除。其未完成的分区将通过协议解决。可转移的分区将有资格被重新分配,不可转移的分区将被标记为 FAILED,使任务干净地失败而非冒险再次运行。两个阈值之间的差距是宽限期窗口,这是有意为之。短暂的垃圾回收暂停或网络波动会使节点标记为不可达,但不会影响其工作,节点在下次心跳时会自动恢复。只有持续的缺席才会升级为移除。重新分配过程复用与声明相同的比较-设置机制,因此在此期间达到 COMPLETED 状态的分区将永远不会被恢复。

最佳实践与常见陷阱:使数据库协调机制具备生产环境就绪性

将可转移性视为契约而非提示

最重要的设计决策是可转移性被声明而非推断。每个分区携带一个比特的元数据。如果运行分区的节点发生故障,该工作是否可以安全地在其他地方重新运行?分区器实现会标记每个分区是否可转移,框架会严格按照该标记执行,而非自行判断。(在参考实现中,这是一个分区器覆盖的方法。)

可转移分区声明工作是幂等的,可以安全地重新运行,提供至少一次的保证。当节点失效时,分区会被重新分配并可能运行两次。不可转移分区声明相反的情况,提供最多一次的保证。当节点失效时,分区会被标记为失败而非迁移,且不会在其他地方运行。数值范围求和是可转移的,但向外部账本追加记录或调用一次性付费API则不可转移。该选择是正确性契约,因此需按分区判断工作是否幂等。

依赖隔离机制,但不要仅依赖它

失去自身心跳的节点会停止申领工作,并在运行已申领分区前中止,将其留待主节点重新分配。这种隔离机制缩小了两个节点同时运行同一可转移分区的时间窗口,但属于尽力而为。硬暂停的JVM可能恢复并完成已重新分配的工作,因此可转移工作必须真正幂等。系统永不放松的规则是:不会静默丢失任何分区。分区要么完成,要么作业失败。

将重启设计为幂等操作

工作节点故障是自动处理的,但主节点故障更复杂。当存活节点发现协调行仍在进行且主节点已离开时,它会原子性地申领该卡住的作业并标记为失败,使作业能够干净重启而非永远卡住。恢复机制是失败后重启而非原地恢复,因此即使主节点在所有分区完成但尚未最终化时死亡,作业仍可重启。因此重启必须是幂等的。这一要求也将最多一次保证绑定到单次执行。丢失的主节点会强制重启,而其工作节点可能仍在完成任务,因此需要在重启时添加对不可转移工作的对账步骤。自动接管(无需重启直接恢复作业)已在路线图中。当前待解决的问题是:当两个节点可能同时充当主节点时,如何确保接管过程的安全性。

将数据库作为关键依赖运行

一旦数据库成为协调平面,它就成为整个集群的硬依赖,因此需要以适合工作负载的方式运行复制数据库并配置备份和故障转移。这种设计的回报是所有协调状态都是普通SQL。诊断卡住的运行时,可通过查询BATCH_PARTITIONS表的SELECT语句进行,而非依赖代理控制台。为热查询和完成扫描查询建立索引,确保它们永远不会退化为表扫描。

知道天花板在哪里

该模式针对约2到20个节点,其瓶颈是数据库竞争而非固定节点数。心跳、轮询和状态更新都是数据库流量。在小型到中型集群中,这种流量较为温和,但会随着节点数和作业并发量增加,直到与批处理工作负载争夺相同的连接池和I/O资源。请在自己的负载下验证该范围。

权衡取舍:何时使用数据库协调,何时选择消息代理

数据库协调并不是对基于代理的分区的通用替代方案。当数据库已经存在且管理良好时,当操作简便性和成本比原始规模更重要时,当添加消息中间件不切实际(受限环境、有限的操作能力、跨云可移植性)时,以及当集群规模处于中小型范围时,应选择数据库协调。

当集群规模超过该范围扩展到数十个节点时,当需要亚秒级协调延迟时,当批处理平台已经与其他工作负载共享成熟的流式传输骨干时,或者当工作自然映射到代理的传递和排序语义时,应选择使用代理。共享数据库也会成为你必须以高标准管理的依赖项。如果这种方法不现实,代理的职责分离可能更合适。

将数据库协调方法与其邻近方案进行对比会有帮助。Spring Batch 自身的远程分区通过代理进行调度,但没有内置的对当前有多少工作节点存活的感知,也没有在工作节点接收分区后死亡时的自动重新分配,这些正是该模式填补的空白。基于 Spring Cloud Task 部署器的分区处理器也是无代理的,但它通过平台(如 Kubernetes)为每个分区提供一个工作节点,而该模式则协调一个持续运行的对等 JVM 节点集群,这些节点会轮询工作。Quartz、db-scheduler 和 JobRunr 等数据库协调调度器也共享通过数据库协调并在节点丢失时进行故障转移的直觉,但它们的目标是调度或通用后台任务,而不是 Spring Batch 的分区步骤执行。

结论

从分布式 Spring Batch 中移除代理并不是删除了一个需求,而是将其转移到 SQL 中。代理隐式回答的四个问题(规模、协调、单一所有权和节点故障)现在有了显式答案:通过分区器提供一个活跃节点计数,每个作业一个主节点且没有持续协调器,使用比较并设置生命周期转换,以及两阶段心跳故障转移。回报是水平扩展和自动恢复,而堆栈中除了 Spring Batch 已经需要的数据库之外无需其他组件。

数据库已经存在,并且在那些代理只会增加复杂组件的常见情况下被充分利用。数据库确实有一个实际的上限。了解这个上限的位置最为有用。结论是,对于许多团队来说,将关系数据库用作协调平面已经足以运行正确且容错的分布式批处理任务,这些团队的规模从未要求使用代理。

作者注

此处描述的设计源自 spring-batch-db-cluster-partitioning,这是一个作者构建并维护的开源框架,经过同行评审并发表在《开放源代码软件期刊》中。它是一个实际案例,因为这是作者完全了解的实现;无论读者采用它还是构建自己的方案,这些模式都可以迁移。完整的多数据库模式、代码和可运行的多节点示例都可以在开源仓库中找到。

作者部分主容器

关于作者

章节标题

每个作者的主容器

#### Janardhan Reddy Chejarla

显示更多

显示更少

#### 本内容属于 Java 主题

##### 相关主题:

  • 开发
  • 架构与设计
  • Spring Batch
  • Spring
  • 数据分区
  • 消息传递
  • Java
  • 分区
  • 面向服务的架构
  • 分布式系统
  • 可扩展性
  • 批量处理
  • 性能
  • 架构
  • 企业架构
  • 相关编辑内容
  • InfoQ热门文章 AWS推出Well-Architected Agent预览版 逆向工程任何项目:将AI代理引入二进制分析 Web应用的未来:AI代理能否实时生成UI? Google采用Pyrefly实现更快更精准的Python类型检查 AWS推出Strands Decider 2B和Strands Box用于AI代理开发 Java新闻速递:JDK 28 JEP、Azul Payara、Gatherers4j、Google ADK、Yupiik、Apache Camel CVEs

InfoQ新闻通讯

每周二发送的InfoQ上周内容回顾。加入超过25万名高级开发者的社区。查看示例

我们保护您的隐私。