[大厂实践] Netflix 分布式计数器抽象

本文介绍了 Netflix 的分布式计数抽象服务,能够在全球范围内提供高性能、低延时的计数服务,为 Netflix 的 A/B 测试、追踪、用户体验优化等领域提供有效的基础设施。原文:Netflix's Distributed Counter Abstraction

简介

上一篇文章中,我们介绍了 Netflix 的时间序列抽象服务,这是一种分布式服务,旨在以毫秒级的低延迟存储和查询大量时间事件数据。今天,我们将向大家介绍 分布式计数器抽象 服务。此服务基于时间序列抽象构建而成,能够在大规模范围内实现分布式计数,同时保持类似的低延迟性能。与我们所有的抽象技术一样,我们使用数据网关控制面来对这个服务进行分片、配置和全球部署。

分布式计数是计算机科学领域中一个颇具挑战性的问题。本文将探讨 Netflix 所面临的各种计数需求、在近乎实时的情况下实现准确计数所面临的挑战,以及我们所选择的方法背后的理论依据,包括其中所涉及的必要权衡。

注意:对于分布式计数器而言,"准确"或"精确"这类表述应谨慎对待。在此情况下,它们指的是计数结果非常接近准确值,并且具有极短的延迟时间。

用例与需求

在 Netflix,计数器的应用场景包括追踪数百万次用户操作、监测特定功能或体验在用户中的展示频率,以及在 A/B 测试中对数据的各方面进行计数,等等。

这些应用场景可以大致分为两大类:

  • 尽力而为:对于这一类别,计数的准确性或持久性并非至关重要。然而,该类别要求能够以极低的延迟近乎即时的获取当前计数,并且要将基础设施成本控制在最低水平。
  • 最终一致性:这一类别需要准确且持久的计数,并愿意以一定的精度延迟和稍高的基础设施成本为代价来换取这种一致性。

这两个类别还有一些一致的需求,例如高吞吐量和高可用性。下面的表格提供了这两个类别对于不同需求的详细概述。

需求 尽力而为 最终一致性
可用性 >=99.999 >=99.999
读/写吞吐量 ~100K QPS ~100K QPS
读/写延时 低毫秒级 低毫秒级
持久性 尽力而为 需要
精度 尽力而为 最终一致性
过时 很低,几乎没有 可接受轻微延迟
全局读/写 没有,单区 有,多区
清除/重置计数 支持 支持
幂等性 可选 需要
基数(唯一计数) 几千到数百万 几千到数百万
升序/降序 支持 支持
审计 没有 最好有
重计数 没有 最好有

分布式计数器抽象

为了满足需求,计数器抽象模块被设计成具有高度的可配置性,允许用户在不同计数模式之间进行选择,例如尽力而为模式或最终一致性模式,同时会考虑每个选项所涉及的已记录的权衡因素。在选择某个模式后,用户可以通过 API 进行操作,而无需担心底层存储机制和计数方法。

接下来我们详细了解一下 API 结构和功能。

API

计数器被划分到独立的命名空间中,用户会为各自特定用例分别设置命名空间。每个命名空间都可以通过服务控制面配置不同参数,例如计数器类型、生存时间(TTL)和计数器基数等。

计数器抽象 API 与 Java 的 AtomicInteger 接口类似:

AddCount/AddAndGetCount :在指定数据集中,根据给定增量值调整指定计数器的计数值,增量值可以为正数或负数。与之对应的 AddAndGetCount 还会返回执行加法操作后的计数值。

json 复制代码
{
  "namespace": "my_dataset",
  "counter_name": "counter123",
  "delta": 2,
  "idempotency_token": { 
    "token": "some_event_id",
    "generation_time": "2024-10-05T14:48:00Z"
  }
}

幂等性令牌可用于支持该功能的计数器类型,客户端可基于此令牌来安全的重试或对请求进行保护。分布式系统出现故障是不可避免的,而具备安全重试请求的能力则能提高服务的可靠性。

GetCount:检索数据集中指定计数器的计数值。

json 复制代码
{
  "namespace": "my_dataset",
  "counter_name": "counter123"
}

ClearCount:将数据集中指定计数器的计数重置为0。

json 复制代码
{
  "namespace": "my_dataset",
  "counter_name": "counter456",
  "idempotency_token": {...}
}

接下来我们看看抽象中支持的不同类型的计数器。

计数器的类型

该服务主要支持两种计数器类型:尽力而为型(Best-Effort)最终一致性型(Eventually Consistent) ,此外还有一种实验性类型:精确型(Accurate)。在接下来的章节中,我们将详细介绍这几种计数器的不同处理方式以及每种方式所涉及的权衡因素。

尽力而为型局部计数器

这种计数器由 EVCache 提供支持,EVCache 是 Netflix 开发的基于广受欢迎的 Memcached 构建的分布式缓存解决方案,适用于诸如 A/B 测试等场景。在这些场景中,会同时进行许多短期运行的实验,粗略计数就足够了。撇开资源配置、资源分配以及控制面管理的复杂性不谈,该解决方案的核心其实非常简单明了:

ts 复制代码
// 计数器缓存键
counterCacheKey = <namespace>:<counter_name>

// add操作
return delta > 0
    ? cache.incr(counterCacheKey, delta, TTL)
    : cache.decr(counterCacheKey, Math.abs(delta), TTL);

// get操作
cache.get(counterCacheKey);

// 从所有副本里清除计数器
cache.delete(counterCacheKey, ReplicaPolicy.ALL);

EVCache 在单区域内能够以极低的毫秒级延迟(甚至更低)实现极高的吞吐量,从而能够在共享集群中实现多租户设置,节省基础设施成本。然而,也存在一些权衡之处:它不支持增量操作的跨区域复制,并且不提供一致性保证,这对于准确计数可能是必需的。此外,它本身并不支持幂等性,因此无法安全重试或对请求进行保护。

最终一致性型全局计数器

虽然有些用户能够接受尽力而为计数方式的局限性,但另一些用户则更倾向于精确的计数、持久性和全球可用性,接下来我们将探讨实现持久且准确计数的各种策略,我们的目标是强调全球分布式计数所固有的挑战,并解释我们所选择方法背后的理由。

方法1:每个计数器存储一行

我们先从简单实现开始,在全局复制的数据存储中的一个表中每一行存储一个计数键。

我们来探讨一下这种方法的弊端:

  • 缺乏幂等性:存储数据模型中没有内置幂等性密钥,使得用户无法安全重试请求。实现幂等性可能需要使用外部系统来存储此类密钥,这可能会进一步降低性能或引发竞争条件。
  • 激烈竞争:为了可靠更新计数,每个写入者都必须使用锁或事务对给定计数器执行 Compare-And-Swap 操作。根据操作的吞吐量和并发性,这可能会导致严重竞争,严重影响性能。

辅助键(Secondary Keys) :在这种方法中,减少竞争的一种方式是使用辅助键,例如 桶标识符(bucket_id),通过将给定计数器拆分为多个桶来实现写操作的分布,同时使读操作能够跨桶进行聚合。难点在于确定合适的桶数量。固定数量可能仍会导致热门键发生竞争,而根据数百万个计数器动态分配每个计数器的桶数量则会带来更复杂的问题。

接下来看看能否对方案进行改进,以克服这些缺陷。

方法2:基于实例聚合

为解决热键问题以及防止对同一行数据的实时写入操作产生冲突,可以采用以下策略:每个实例在内存中汇总计数,然后定期将计数写入磁盘。在写入过程中引入足够的抖动机制还能进一步减少冲突。

然而,这一解决方案却带来了新的问题:

  • 数据丢失风险:该解决方案在实例故障、重启或部署期间会因数据在内存而面临丢失风险。
  • 无法可靠重置计数:由于计数请求分布在多台机器上,很难就计数器重置的确切时间点达成一致意见。
  • 缺乏幂等性:与之前方案类似,这种方法本身并不能保证幂等性。实现幂等性的一种方法是始终将相同的计数器集路由到同一个实例。然而,这种方法可能会引入额外的复杂性,例如领导者选举,以及写路径中的可用性和延迟方面的问题。

话虽如此,这种方法在这些缺陷可接受的场景中可能仍然适用。不过,我们看看是否可以通过不同的基于事件的方法来解决这些问题。

方法3:使用持久化队列

在这种方法中,我们将计数器事件记录到像 Apache Kafka 这样的持久化队列系统中,以防止任何潜在的数据丢失。通过创建多个主题分区,并将计数键哈希到特定分区,确保相同的计数器集由相同的消费者集进行处理。这种设置简化了实现幂等性检查和重置计数的操作。此外,通过利用诸如 Kafka StreamsApache Flink 等流处理框架,可以实现窗口聚合操作。

然而,这种方法也存在一些问题:

  • 潜在的延迟:如果对给定分区的所有计数都采用相同的消费者处理,可能会导致数据备份和延迟,从而导致计数数据过时。
  • 重新分配分区:这种方法需要根据计数器基数和吞吐量的增加来自动扩展和重新分配主题分区。

此外,所有预先聚合计数的方法都难以满足精确计数器的两个需求:

  • 计数审计:审计过程涉及将数据提取至离线系统进行分析,以确保增量数据的正确应用从而得出最终值。此过程还可用于追踪增量数据的来源。然而,当计数未进行存储而只是进行汇总时,审计就变得不可行了。
  • 潜在重算:与审计类似,如果需要对增量数据进行调整,并且需要在时间窗口内对事件进行重算,那么预先汇总计数会使这一操作变得不可行。

除了上述少数要求外,如果我们确定了合适的队列分区和消费者扩容方式,并能保持幂等性,这种方法仍然有效。不过,我们还是先探讨一下如何调整以满足审计和重算的要求。

方法4:单个增量的事件日志

在这种方法中,我们会记录每个计数器的每次递增情况以及对应的 事件时间(event_time)事件标识符(event_id)。事件标识符可以包含递增操作的来源信息。事件时间与事件标识符的组合也可以作为写操作的幂等性键。

然而,从最简单的角度来看,这种方法仍然存在缺陷:

  • 读取延迟:每次读取请求都需要扫描给定计数器的所有增量数据,这可能会降低性能。
  • 重复工作:多个线程在读取操作期间可能会重复对同一组计数器进行汇总工作,从而造成资源浪费和资源利用率低下。
  • 宽分区 :如果使用像 Apache Cassandra 这样的数据存储系统,为同一个计数器存储大量增量数据可能会导致分区范围过大,从而影响读取性能。
  • 大数据足迹:单独存储每个增量数据也可能随着时间的推移导致巨大的数据存储量。如果没有高效的数据保留策略,这种方法可能难以有效扩展。

这些问题的综合影响可能导致基础设施成本增加,这可能难以证明其合理性。然而,采用事件驱动的方法似乎是在解决我们遇到的挑战并满足需求方面迈出的重要一步。

还能如何进一步改进解决方案呢?

Netflix 的方法

我们采用了上述方法的组合,即将每次计数活动都记录为事件,并在后台利用队列和滑动时间窗口持续对事件进行汇总。此外,我们还采用了分桶策略以避免过度分区。在接下来的章节中,我们将探讨这种方法如何解决上述提到的缺陷并满足所有需求。

注意:从现在起,我们将把 汇总(rollup)聚合(aggregate) 这两个词互换使用。它们实际上意思相同,即收集各个计数器的增加/减少量,并得出最终值。

时间序列事件存储

我们选择了时间序列数据抽象作为事件存储方式,其中计数器的变更会以事件记录的形式被接收。将事件存储在时间序列中的好处包括:

  • 高性能:时间序列抽象已经满足了我们诸多需求,包括高可用性和吞吐量、可靠且快速的性能等等。

  • 降低代码复杂度:通过将大部分功能委托给现有服务,大幅降低了计数器抽象的代码复杂度。

时间序列抽象使用 Cassandra 作为底层事件存储系统,但也可以配置为与任何持久存储系统配合使用。其数据结构如下所示:

处理宽分区时间桶(time_bucket)事件桶(event_bucket) 列在将宽分区进行拆分时起着至关重要的作用,能够防止高吞吐量的计数器事件使某个分区不堪重负。

避免重复计数事件时间(event_time)事件标识(event_id)事件项键(event_item_key) 这几列构成了针对特定计数器事件的唯一标识符,使得客户端能够安全的重试操作,而不会出现重复计数的风险。

事件排序:时间序列会将所有事件按照时间降序排列,这样我们就能利用这一特性来处理诸如计数重置这类事件。

事件保留:时间序列抽象包含了保留策略,以确保事件不会被无限期存储,从而节省磁盘空间并降低基础设施成本。一旦事件已被聚合并转移到某个更具成本效益的存储设备上以用于审计,就无需在主存储中保留了。

现在,让我们来看看针对某个计数器,这些事件是如何被汇总的。

聚合计数事件

如前所述,对于每一个读取请求,收集所有单独的增量数据在读取性能方面会带来高昂的成本。因此,有必要进行后台聚合处理,以持续汇总计数并确保最佳的读取性能。

但如果同时正在进行写操作,如何才能安全的汇总计数事件呢?

这就是 最终一致性 概念变得至关重要的地方。通过有意将处理时间滞后于当前时间一段安全间隔,确保聚合操作始终在不可变的时间窗口内进行。

具体是这个样子:

我们来分步骤说明一下:

  • lastRollupTs:此字段表示计数器值上一次被汇总的时间。对于首次操作计数器的情况,此时间戳默认为较久远的合理时间。
  • 不可变窗口与滞后:聚合只能在不再接收计数器事件的不可变窗口内安全进行。时间序列抽象中的 acceptLimit 参数在此起着关键作用,它会拒绝时间戳超出此限制的传入事件。在聚合过程中,此窗口会向后略微移动,以考虑时钟偏差。

这确实意味着计数器值会比其最近的更新结果滞后一定时间(通常约为几秒)。这种方式确实存在因跨区域复制问题而导致错过事件的可能性。详情请参阅文末"未来工作"部分

  • 聚合过程:聚合过程会将自上次聚合以来在聚合窗口内发生的所有事件进行汇总,从而得出新值。

聚合存储

我们将聚合结果保存在持久化存储中,接下来的聚合操作将直接从这个保存点开始进行。

我们为每个数据集都创建一个这样的汇总表,并以 Cassandra 作为持久化存储系统。不过,计数器服务可以配置为与任何持久化存储系统配合使用。

LastWriteTs :每当给定计数器进行写入操作时,会在该表中以列式更新的形式记录 最后写入时间戳( last-write-timestamp) 。这是通过 Cassandra 的 使用时间戳(USING TIMESTAMP) 功能来实现的,以可预测的方式实现 最后写入获胜(LWW,Last-Write-Win) 语义。这个时间戳与事件的 event_time 相同。在后续章节中,我们将看到这个时间戳如何用于使某些计数器保持在活跃的汇总循环中,直到获取到最新值。

**滚动缓存(Rollup Cache)

为了提高读性能,这些值会在每个计数器对应的 EVCache 中进行缓存。我们将 上一次汇总计数(lastRollupCount)上一次汇总时间戳(lastRollupTs) 合并为每个计数器的单个缓存值,以避免计数与对应的检查点时间戳之间可能出现不匹配情况。

但我们如何确定哪些计数器需要触发合并操作呢?我们通过分析写入和读取路径来更好的理解这个问题。

添加/清除计数

添加或清除计数请求持久写入时间序列抽象,并更新聚合存储中的最后写入时间戳。如果持久化确认失败,客户端可以用相同的幂等令牌重试请求,而不会有计数过多的风险。根据持久性,我们发送一个阅后即焚请求来触发请求计数器的汇总。

获取计数

我们通过一次 快速点读操作(quick point-read operation) 返回最后聚合计算的结果,但会接受结果可能稍显过时这一小缺陷。我们还在读取操作期间触发一次聚合操作,以更新最后的聚合结果时间戳(last-rollup-timestamp),从而提高后续聚合操作的性能。如果之前的聚合操作失败,此过程还会自动修复出现的陈旧结果。

通过这种方式,计数值会不断收敛至其最新值。现在,我们看看如何利用聚合流水线(Rollup Pipeline)将这种方法扩展应用于数百万个计数器以及数千个并发操作。

聚合流水线

每个 计数器聚合(Counter-Rollup) 服务都会运行一个聚合流水线,以高效汇总数百万个计数器的计数值。这就是计数器抽象中大部分复杂性所在之处。在接下来的章节中,我们将分享如何实现高效聚合的关键细节。

轻量级聚合事件(Light-Weight Roll-Up Event):如上述写入和读取路径所示,对计数器执行的每一项操作都会向聚合服务发送一个轻量级事件:

json 复制代码
rollupEvent: {
  "namespace": "my_dataset",
  "counter": "counter123"
}

注意,此事件不包括增量数据,而只是向聚合服务器表明该计数器已被访问,现在需要进行聚合。准确了解需要聚合哪些特定计数器可以防止扫描整个事件数据集以进行聚合。

内存聚合队列(In-Memory Rollup Queues):每个聚合服务实例都会运行一组内存队列,用于接收聚合事件并实现聚合操作的并行处理。在该服务的首个版本中,我们选择了使用内存队列,以降低资源配置的复杂性、节省基础设施成本,并使队列数量的重平衡变得相对简单。然而,代价是在实例崩溃的情况下可能会遗漏聚合事件。有关更多详细信息,请参阅"未来工作"部分中的"陈旧计数"一节。

减少重复工作 :我们通过快速非加密哈希算法(如 XXHash)来确保相同的计数器集合最终会出现在同一个队列中。此外,还通过设置单独的汇总栈,并选择运行更少但更强大的实例,来尽量减少重复的汇总工作量。

可用性与并发问题 :单一汇总服务器实例可以最大程度减少重复的汇总工作,但可能会导致触发汇总操作的可用性问题。如果我们选择将汇总服务器进行水平扩展,可以用线程覆盖汇总值,同时避免使用任何形式的分布式锁机制,以保持高可用性和性能。这种方法是安全的,因为汇总是在不可变时间窗口内进行。尽管线程之间的 now() 函数的概念可能有所不同,导致汇总值有时会出现波动,但计数最终会在每个不可变汇总时间窗口内收敛到准确值。

重平衡队列:如果需要调整队列数量,只需进行一次控制面配置更新,然后重新部署即可实现队列数量的重平衡。

json 复制代码
      "eventual_counter_config": {
          "queue_config": {
            "num_queues" : 8,  // 改为 16 并重新部署
...

处理部署:在部署过程中,队列会优雅关闭,清除所有现有事件,而新的聚合服务实例则可能会以不同的队列配置启动。可能在一段短暂时间内,旧的和新的聚合服务器都处于运行状态,但正如之前所提到的,这种竞争情况是可以通过在不可变时间窗口内进行聚合操作来加以管理的。

减少汇总工作量:对于同一个计数器接收到多个事件并不意味着要进行多次汇总操作。我们将这些汇总事件收集到一个集合中,确保在汇总窗口内,同一个计数器只会被汇总一次。

高效聚合:每个聚合消费者会同时处理一批计数器。在每个批次中,它会并行查询底层的时间序列抽象,以在指定的时间范围内聚合事件。时间序列抽象会优化范围扫描操作,从而实现低毫秒级的延迟。

动态分批处理:汇总服务器会根据计数器的基数动态调整需要扫描的时间分区数量,以避免因大量并行读取请求而使底层存储不堪重负。

自适应回压(Adaptive Back-Pressure):每个消费者在完成一个批次的数据处理后,才会开始处理下一批次的数据汇总。它会根据前一批次的执行情况来调整批次之间的等待时间。这种做法在数据汇总过程中提供回压机制,以防止对底层的时间序列存储造成过大的压力。

处理融合

为防止低基数计数器出现滞后现象,从而避免扫描时间分区过多,计数器会保持持续的汇总循环状态。而对于高基数计数器,持续循环会占用汇总队列中过多的内存。这就是前面提到的 最后写入时间戳(last-write-timestamp) 所起的关键作用。汇总服务器会检查这个时间戳,以确定给定计数器是否需要重新排队,从而确保持续进行汇总操作,直到完全跟上写入操作为止。

现在,让我们来看看如何利用这种计数器类型来实现近乎实时的最新计数功能。

实验内容:精确全局计数器

我们正在对一种略微修改后的最终一致性计数器进行实验。同样,对于"精确"这个词请持保留态度。这种计数器与同类计数器的主要区别在于,代表自上一次汇总时间戳以来的计数值的"增量"是实时计算得出的。

实时汇总这个差异可能会对操作性能产生影响,这取决于需要扫描的事件数量以及用于获取该差异的分区数量。这里同样适用分批汇总原则,以避免同时扫描过多分区。

相反,如果对数据集中的计数器进行频繁访问,那么计数的差异时间间隔就会很短,从而使得这种获取当前计数的方法非常有效。

现在,让我们来看看如何通过统一的控制面配置来管理这一切复杂性。

控制平面

数据网关平台控制面负责管理所有抽象概念和命名空间(包括计数器抽象)的控制设置。以下是一个支持具有低基数的最终一致性计数器的命名空间的控制面配置示例:

json 复制代码
"persistence_configuration": [
  {
    "id": "CACHE",                             // 计数器缓存配置
    "scope": "dal=counter",
    "physical_storage": {
      "type": "EVCACHE",                       // 缓存存储类型
      "cluster": "evcache_dgw_counter_tier1"   // 共享 EVCache 集群
    }
  },
  {
    "id": "COUNTER_ROLLUP",
    "scope": "dal=counter",                    // 计数器抽象配置
    "physical_storage": {
      "type": "CASSANDRA",                     // 聚合存储类型
      "cluster": "cass_dgw_counter_uc1",       // 物理集群名
      "dataset": "my_dataset_1"                // 命名空间/数据集   
    },
    "counter_cardinality": "LOW",              // 支持的计数器基数
    "config": {
      "counter_type": "EVENTUAL",              // 计数器类型
      "eventual_counter_config": {             // 最终计数器类型
        "internal_con...store
    "physical_storage": {
      "type": "CASSANDRA",                     // 持久化存储类型
      "cluster": "cass_dgw_counter_uc1",       // 物理集群名
      "dataset": "my_dataset_1",               // 键空间名
    },
    "config": {
      "time_partition": {                      // 事件的时间分区
        "buckets_per_id": 4,                   // 内部事件桶
        "seconds_per_bucket": "600",           // LOW 的较小宽度
        "seconds_per_slice": "86400",          // 时间片表的宽度
      },
      "accept_limit": "5s",                    // 不可变边界
    },
    "lifecycleConfigs": {
      "lifecycleConfig": [
        {
          "type": "retention",                 // 事件保留
          "config": {
            "close_after": "518400s",
            "delete_after": "604800s"          // 7 天事件留存
          }
        }
      ]
    }
  }
]

通过采用这种控制面配置,我们利用部署在同一主机上的容器构建了多个抽象层,每个容器都能获取与其自身范围相关的配置信息。

基础设施配置

与时间序列抽象概念一样,自动化系统会依据用户关于其工作负载和基数的诸多输入,来确定合适的基础设施配置以及相关的控制面设置。

性能

该服务在全球范围内通过不同 API 端点和数据集每秒处理的请求量接近 75,000 次:

同时为其所有端点提供个位数毫秒延迟:

未来工作

尽管系统已经相当稳定,但为了使其更加可靠并进一步完善其功能,我们仍需继续努力。其中一些工作包括:

  • 区域汇总:跨区域复制问题可能导致其他区域的事件丢失。另一种策略是为每个区域建立一个汇总表,然后再汇总到全局汇总表中。该设计的关键挑战是有效的在各区域之间传递计数的清零信息。

  • 错误检测和失效计数:如果汇总事件丢失或者汇总操作失败且未重试,可能会出现失效计数过高的情况。对于频繁访问的计数器,这种情况不会成为问题,因为它们会一直处于汇总循环中。而对于不常访问的计数器,这个问题会更加突出。通常,此类计数器的首次读取会触发汇总操作,从而自动解决此问题。但对于无法接受初始读取时有潜在失效值的使用场景中,我们计划实施改进的错误检测、汇总交接以及持久化队列以实现可靠的重试。

结论

分布式计数在计算机科学领域仍是一个颇具挑战性的难题。本文探讨了多种实现和部署大规模计数服务的方法。虽然可能存在其他实现分布式计数的方法,但我们的目标是实现极高的性能、低廉的基础设施成本,并同时保持高可用性和提供幂等性保证。在此过程中,我们为了满足 Netflix 的各种计数需求而做出了各种权衡。希望这篇文章能对读者产生启发。


你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

相关推荐
EnigmaGcl3 小时前
领域驱动设计,到底在讲什么?
后端·架构
元闰子4 小时前
分离还是统一,这是个问题
后端·面试·架构
文火冰糖的硅基工坊4 小时前
[硬件电路-124]:模拟电路 - 信号处理电路 - 测量系统的前端电路详解
前端·嵌入式硬件·fpga开发·架构·信号处理·电路
爷_5 小时前
手把手教程:用腾讯云新平台搞定专属开发环境,永久免费薅羊毛!
前端·后端·架构
Ashlee_code5 小时前
北极圈金融科技革命:奥斯陆证券交易所的绿色跃迁之路 ——从Visma千倍增长到碳信用衍生品,解码挪威资本市场的技术重构
科技·算法·金融·重构·架构·系统架构·区块链
前端搬砖仔噜啦噜啦嘞6 小时前
Cursor AI 编辑器入门教程和实战
前端·架构
llm10 小时前
MediaPlayer介绍
java·架构
Ashlee_code12 小时前
关税战火中的技术方舟:新西兰证券交易所的破局之道 ——从15%关税冲击到跨塔斯曼结算联盟,解码下一代交易基础设施
java·python·算法·金融·架构·系统架构·区块链
你我约定有三13 小时前
分布式微服务--Nacos作为配置中心(一)
分布式·微服务·架构