写点什么

使用 Akka 的 Actor 模型和领域驱动设计构建反应式系统

  • 2018 年 3 月 22 日
  • 本文字数:9491 字

    阅读完需:约 31 分钟

核心要点

  • 面向 Actor 编程是面向对象编程的一种替代方案;
  • 借助 Actor,开发高并发的系统会变得非常容易;
  • Actor 并不局限于单个节点上的单个进程,它可以作为分布式集群运行;
  • Actor 和 Actor 模型提供了“反应式”编程所需的所有内容;
  • Actor 与领域驱动设计是绝佳的组合。

随着移动和数据驱动应用的爆发性增长,用户需要在任何地点实时访问任何内容。系统的弹性和响应性不再是“最好能有”的特征,而是已经成为重要的业务需求。业务越来越需要从静态、脆弱的架构转换为灵活、弹性的系统。

因此,反应式开发得到了迅速地增长。为了支持反应式开发, Actor 模型结合领域驱动设计能够满足现代弹性的需求。

Actor 模型的一些历史

Actor 模型最初是随着 Smalltalk 的出现而构思出来的,当时面向对象编程(Object-Oriented Programming,OOP)本身刚刚出现不久。

大约在 2003 年左右,计算机的核心特性经历了一个重要的变化,处理器的速度达到了一个顶点。在接下来的近十五年里,时钟速度是呈线性增长的,而不是像以往那样以指数级的速度增长。

但是,用户的需求在持续增长,计算机领域必须要找到某种方式来应对,多核处理器应运而生。计算处理变成了“团队协作”,效率的提升通过多个核心的通信来实现,而不是传统的时钟速度的提升。

这也是线程发挥作用的地方,这个概念要比看上去复杂得多。以某个稀缺资源的简单计数器为例,比如仓库中某个货品的数量或者某个活动的可售门票。在这样的例子中,可能会有很多请求同时获取一个或多个仓库中的货品或门票。

我们考虑一种常用的实现方式,每个购买请求都对应一个线程。在这种方式中,很可能会有多个并发运行的线程都去调整计数器。为了满足语义方面的需求,我们的模型必须要确保在同一个时间只能有一个线程去递减计算器的值。这样做的原因在于递减操作涉及到两个步骤:

  1. 检查当前的计数器,确保它的值要大于或等于要减少的值;
  2. 递减计数器。

下面的样例阐述了这两个步骤为什么要作为一个整体操作来完成。每个请求代表了购买一个或多个货品,也可能是购买一张或多张门票。假设有两个线程并发地调整计数器,该计数器目前的值是5。线程一想要将计数器的值递减3,而线程二想要将计数器的值递减4。它们都会检查当前计数器的值,并且会断定计数器的值大于要递减的数量。然后,它们都会继续运行并递减计数器的值。最后的结果就是5 - 4 - 3 = -2。这样的结果会造成货品的过度分配,违反了特定的业务规则。

防止这种过度分配的一种原生方式就是将检查和递减这两个步骤放到一个原子操作中。将两个步骤锁定到一个操作中能够消除购买已售罄物品的可能性,比如两个线程同时尝试购买最后一件商品。如果没有锁的话,就有可能多个线程同时断定计数器的值大于或等于要购买的数量,然后错误地递减计数器,从而导致出现负数值。

每次一个线程的方式有一个潜在的问题,那就是在高度竞争的阶段,有可能出现很长的线程队列,它们都在等待递减计数器。在现实世界中,类似的例子就是人们排队等待购买某个活动的门票。

这种方式有个较大的弱点就是可能会造成众多的阻塞线程,每个线程都在等待轮到它们去执行一个序列化的操作。

如果应用的设计者不小心的话,内在的复杂性有可能会将多核心处理器、多线程的应用变成单线程的应用,或者导致工作线程之间存在高度竞争。

多线程环境的更好方案

Actor 模型优雅地解决了这个难题,为真正多线程的应用提供了一个基础支撑。Actor 模型的设计是消息驱动和非阻塞的,吞吐量自然也被考虑了进来。它为开发人员提供了一种简便的方式来进行多核心编程,不再需要关心复杂难懂的并发。让我们看一下它是如何运行的。

Actor 包含发送者和接收者;设计简单的消息驱动对象实现异步性。

我们重新回顾一下上面所描述的门票计数器场景,将基于线程的实现替换为 Actor。当然,Actor 也要在线程中运行,但是 Actor 只有在有事情可做的时候才会使用线程。在我们的计数器场景中,请求者代表了 Customer Actor。门票的数量现在由 Actor 来维护,它持有当前计数器的状态。Customer Actor 和 Tickets Actor 在空闲(idle)或没有事情做的时候(也就是没有消息要处理)都不会持有线程。

要初始购买操作,Customer Actor 需要发送一条 buy 消息给一个 Tickets Actor。在这样的 buy 消息中包含了要购买的数量。当 Tickets Actor 接收到 buy 消息时,它会校验购买数量不超过当前剩余的数量。如果购买请求是合法的,数量就会递减,Tickets Actor 会发送一条信息给 Customer Actor,表明订单被成功接受。如果购买数量超出了剩余数量的话,Tickets Actor 将会发送给 Customer Actor 一条消息,表明订单被拒绝了。Actor 模型本身确保处理是按照同步的方式进行的。

在下面的图中,我们展现了一些 Customer Actor,它们各自发送 buy 消息给 Tickets Actor。这些 buy 消息会在 Tickets Actor 的收件箱(mailbox)中排队。

图3:Customer Actor 发送buy 消息

Ticket Actor 处理每条消息。如下展示的是请求购买五张门票的第一条消息。

图4:Tickets Actor 处理消息

Tickets Actor 检查购买数量没有超出剩余门票的数量。在当前的情况下,门票数量是 15,因此购买请求能够接受,剩余门票数量会递减,Tickets Actor 还会发送一条消息给发出请求的 Customer Actor,表明门票购买成功。

图5:Tickets Actor 处理消息队列

Tickets Actor 会处理其收件箱中的每条消息。需要注意,这里没有复杂的线程或锁。这是一个多线程的处理过程,但是 Actor 系统会管理线程的使用和分配。

在下面的图中,我们会看到当请求的数量超过剩余值时,Tickets Actor 会如何进行处理。这里所展现的是当我们请求两张门票,但是仅剩一张门票时的情况。Tickets Actor 会拒绝这个购买请求并向发起请求的 Customer Actor 发送一条“sold out”的消息。

图6:Tickets Actor 拒绝购买请求

当然,在线程方面有一定经验的开发人员会知道,可划分为两个阶段的行为检查和门票数量递减能够通过同步的操作序列来完成。以在Java 中为例,我们可以使用同步的方法或语句来实现。但是,基于Actor 的实现不仅在每个Actor 中提供了自然的操作同步,而且还能避免大量的线程积压,防止这些线程等待轮到它们执行同步代码区域。在门票样例中,每个Customer Actor 会等待响应,此时不会持有线程。这样所形成的结果就是基于Actor 的方案更容易实现,并且会明显降低系统资源的占用。

Actor:对象的合法继承者

Actor 是对象的自然继承者,这并不是什么新的理念,事实上也不是那么具有革命性。Smalltalk 的发明者 Alan Kay 定义的很多对象范式(paradigm)依然在使用。他强调消息的重要性,甚至还说对象的内部实现其实是次要的。

尽管 Smalltalk 最初并不是异步的,但它依然是基于消息的,本质上来讲一个对象是通过向另外一个对象发送消息来完成功能的。因此,现代的 Actor 模型遵循了 Alan Kay 最早的面向对象设计理念。

如下是 Akka Actor 系统中一个简单的 Java Actor 实现样例(我们为每个 Actor 设置了一个唯一的“魔力”序列数字,使用它来阐述状态)。

复制代码
public class DemoActor extends AbstractActor {
private final int magicNumber;
public DemoActor(int magicNumber) {
this.magicNumber = magicNumber;
}
@Override
public Receive createReceive() {
return receiveBuilder()
.match(Integer.class, i -> {
getSender().tell(i + magicNumber, getSelf());
})
.build();
}
public static Props props(int magicNumber) {
// Akka Props is used for creating Actor instances
return Props.create(DemoActor.class, () -> new DemoActor(magicNumber));
}
}

Actor 的实现形式为一个类,这个类扩展自 Akka 的抽象基类。Actor 的实现必须要覆盖一个方法,也就是 createReceive 方法,该方法负责创建一个消息接收的构建器,定义传入到 Actor 实现中的消息对象该如何进行处理。

还要注意这个 Actor 是有状态的。Actor 的状态可以非常简单,就像本例中的魔数一样,也可以非常复杂。

要创建 Actor 的实例,我们需要一个 ActorSystem。ActorSystem 启动之后,创建 Actor 一般只需要一行代码。

复制代码
ActorSystem system = ActorSystem.create("DemoSystem");
ActorRef demo = system.actorOf(DemoActor.props(42), "demo");

Actor 创建操作的返回值是一个 actor 引用(actor reference),这个 actor 引用用来给 Actor 发送消息。

demo.tell(123, ActorRef.noSender());上面展现了定义、创建运行实例以及给 Actor 发送消息的基本步骤。当然,实际要做的会比这个简单例子更多一些,但是在大多数情况下,使用 Actor 开发系统需要学习如何以 Actor 系统的形式实现应用和服务,这些 Actor 之间通过交换异步消息进行交互。

为更好的网络构建更好的应用

除了内核和线程以外,如今的环境还允许开发人员利用更快速的存储设备、大量的内存以及高度可伸缩且广泛连接的设备。这些技术都通过用户可接受的价格以云托管方案和快速网络的方式连接在一起。

但是随着系统越来越按照分布式的方式来实现,延迟的增加是不可避免的。分布式系统会被停机或网络分区所中断,这可能是由于一个或多个服务器脱离集群、生成新服务器所导致的延迟造成的。对象模型并不适合处理这种问题。因为每个请求和每个响应都是异步的,所以 Actor 模型能够帮助开发人员处理该问题。

借助 Actor 模型,我们能够很自然地减少延迟。鉴于此,我们不再预期得到即时的结果,系统只会在需要发送或接收消息的时候做出反应。当探测到延迟降级时,系统会自动做出反应和调整,而不是将系统关闭。

在节点所组成的分布式集群中,每个节点都运行 Actor 的一个子集,在这样的环境中,Actor 之间通过异步消息进行交互是非常自然的事情。另外,Actor 有一项基本的功能,那就是消息的发送者和接收消息的 Actor 并不一定局限在同一个 JVM 进程中。Akka 最棒的特性之一就是我们可以构建在集群中运行的系统。Akka 集群是运行在独立 JVM 中的一组节点。从编程的角度来说,将消息发送至另外一个 JVM 中运行的 Actor 与将消息发送给本地 JVM 中的 Actor 一样容易。如下面的图所示,分布在多个集群节点上的 Actor 可以发送消息给其他集群节点上的 Actor。

能够在集群环境中运行增加了Actor 系统在整体架构上的动态性。在一台服务器、一个进程和一个JVM 中运行是一回事儿,在一个跨网络的JVM 集群中运行系统则完全是另外一回事儿。

在单个JVM 的场景下,Actor 运行在Actor 系统之中,JVM 要么处于运行中,要么没有运行。但是在集群中运行的时候,任何一个时间点集群的拓扑结构都可能发生变化。集群节点可能瞬间就能添加进来或移除掉。

从技术上来讲,只要有一个节点处于启动状态,集群本身就是启动的。某个节点上的Actor 可能会非常高兴地与其他节点上的Actor 交换信息,然后,没有任何预警,节点突然就可能会宕机,位于该节点上的Actor 也就被移除了。其他的Actor 该如何应对这些变化呢?

集群节点的丢失会影响到消息的交换,既会影响消息的发送者也会影响消息的接收者。

对于消息的接收者来说,始终存在预期的消息永远接收不到的可能性。接收的Actor 要将这种情况考虑进去,需要存在一个B 计划。在处理异步消息时,预期的消息可能接收不到是难免的。在大多数场景中,处理传入消息的丢失并不需要感知到集群的存在。

而另一方面,对于消息的发送者来说,通常要在一定程度上感知到集群的存在。路由器Actor(Router Actor)负责将消息发送到其他Actor 的物流事宜,接收消息的Actor 可能会以分布式的方式存在于集群之中。路由器Actor 接收到消息,但是它自己并不会处理消息。它将消息转发给一个工作者Actor。这些工作者Actor 通常被称为被路由者(routee)。路由器Actor 负责按照一定的路由算法将消息转发给其他的被路由者Actor。实际的路由算法因情况而异,它要依赖于每个路由器的需求。路由算法的例子包括轮询(round robin)、随机或最小收件箱等等。

考虑下图所示的样例场景(要记住发送消息的客户端并不知道接收消息的Actor 会如何处理消息)。对于客户端来说,接收消息的Actor 就是一个黑盒。接收消息Actor 可能会将工作委托给其他的工作者Actor 来完成。在这种情况下,接收消息的Actor 就是一个路由器。它将接收到的消息路由给代理Actor,让它们来完成实际的工作。

在这个样例场景中,路由器Actor 可能需要感知集群的存在,它会将消息路由到集群中分布式节点的Actor 上。那么,集群感知意味着什么呢?

集群感知Actor 会用到当前集群状态的组成信息,以便于决定如何将传入的消息路由到集群中分布式的其他Actor 上。集群感知Actor 最常见的场景之一就是路由器。集群感知路由器会基于当前集群的状态决定如何将消息路由至目标Actor。例如,路由器知道分布式集群中被路由的Actor 的位置,然后按照分布式工作的算法将消息发送至目标Actor。

Actor 模型如何支持反应式系统

正如反应式宣言(Reactive Manifesto)所定义的那样,“反应式是即时响应性的(responsive),反应式是具有弹性的(resilient),反应式是具有适应性的(elastic),反应式是消息驱动的”。本质上来讲,消息驱动组件促进了反应式的其他三项特点的实现。

提高系统的响应性

反应式是即时响应的,也就是系统能够动态适应不断变化的用户需求。对于一个反应式系统来说,以请求/ 响应的方式回应用户的需求并不少见。如果借助Actor 模型来支持反应式编程,开发人员能够实现非常高的吞吐量。

支持系统的弹性

借助消息传递和消息驱动架构提供的功能,我们也能支持弹性。当客户端Actor 发送一条消息给服务器Actor 接收者时,客户端不必处理服务器对象或Actor 可能抛出的异常。

请考虑一下典型的面向对象架构,在这种架构中,客户端发送一条消息或者调用接收者的一个方法,我们需要强迫客户端处理可能会出现的各种崩溃或抛出的异常。作为回应,客户端一般会重新抛出或者将异常传递给更高层的组件,希望其他人处理它。但是,客户端并不适合修复服务端的崩溃。

在Actor 模型中,尤其是在使用Akka 时,会建立一个用于监管的层级结构。当接收传入消息的过程中出现服务器崩溃或者抛出异常时,不是客户端来处理崩溃,而是由服务器Actor 的父Actor 或者负责服务器Actor 的对象来进行处理。

父Actor 能够更好地理解子Actor 可能会出现的崩溃,因此能够对其作出反应并重启该Actor。客户端只需要知道接收到请求的响应或者没有接收到响应时分别该如何处理就可以了。如果在一个可接受的时间范围内,它没有接收到响应,那么它可以向同一个Actor 发送相同的请求,希望得到再次处理。所以(如果正确构建的话)Actor 系统是非常有弹性的。

如下是一个样例,实际展现了Actor 的监管。在图7 中,Actor R 是一个监管者,它创建了四个工作者Actor。Actors A 和B 会发送消息给Actor R,请求它执行一些操作。Actor R 将工作委托给某一个可用的工作者Actor。

图7——Actor A 的消息会被R 委托给工作者Actor

在这个样例中,工作者Actor 遇到了问题并抛出了异常,如图8 所示。异常会被监管者Actor R 来处理。监管者Actor 会遵循一个定义良好的监管策略,以便于处理工作者的错误。监管者可以选择恢复出现简单问题的Actor 或者重启该工作者,也可能会将其停止掉,这依赖于问题的严重程度和恢复策略。

图8——工作者Actor 抛出了异常

尽管异常会由监管者处理,但是Actor A 还在期待收到响应信息。注意,Actor A 只是期待会有响应消息,而不是一直在等待该消息。

这种异步消息的交互引入了一些很有意思的动态性。Actor A 希望Actor R 能够像预期的那样,对它的消息做出反应。但是,无法保证Actor A 的消息会得到处理或者能够返回响应信息。这个过程中发生的任何问题都有可能破坏请求和响应的周期。例如,考虑这样一种情况,Actor A 和Actor R 运行在不同的节点上,Actor A 和Actor R 之间的消息发送要穿过一个网络连接。网络可能会被关闭,或者运行Actor R 的节点可能会出现故障。另外,还有可能要执行的任务失败了,比如在数据库操作时,因为网络故障或服务器停机造成了操作失败。

鉴于这里缺乏任何保证,在实现Actor A 时,有种常见的方式就是它预期会得到两种可能的结果。第一种结果是当它发送消息给Actor R 后,它最终接收到了一条响应消息。还有一种可能的结果就是Actor A 会预期得到一条替代消息,这条消息表明没有接收到预期的响应。如果按照这种方式的话,Actor A 会涉及到发送两条消息:第一条是发送给Actor R 的消息,第二条是在未来特定的时间点发送给自己的消息。

图9——Actor A 接收一条超时的消息

在这里所使用的基本策略就是同时存在A 计划和B 计划。A 计划指的是所有的事情都能正常运行。Actor A 发送一条消息给Actor R,预期的任务得到执行并且响应消息能够返回给Actor A。B 计划能够应对Actor R 无法处理请求消息的场景。

扩展系统的适应性

反应式系统需要具有适应性,它们可以根据需要扩展和收缩。真正的反应式设计没有竞争点或中心化的瓶颈,所以我们可以共享或复制组件,并在它们之间分配输入。它们通过提供相关的实时性能度量数据,实现预测和反应式伸缩算法。

Actor 模型对适应性的支持是通过动态响应用户活动的高峰和低谷来实现的,在有需要的时候,它会智能地提升性能,并在使用率较低的时候节省能源。消息驱动的特性能够带来更大程度的适应性。

适应性需要两个关键要素。第一个就是当负载增加或降低时,能够有一种机制扩展和收缩处理能力。第二个就是要有一种机制允许在处理能力发生变化的时候,系统对其作出适当的反应。

有很多方式来应对处理能力的扩展和收缩。通常来讲,处理能力的变化可以手动或自动进行。手动处理的样例场景就是为了准备客户季节性的流量高峰,提前增加处理能力。典型的例子就是黑色星期五(Black Friday)和剁手星期一(Cyber Monday)或者光棍节。自动扩展是很多云厂商所提供的很有用的特性,比如 Amazon AWS。

Actor 模型以及该模型的实现 Akka 并没有提供触发处理能力调整的机制,但它是一个理想的平台,能够借助它来构建当集群拓扑结构发生变化时,做出适当反应的系统。正如前文所述,在 Actor 级别,可以实现集群感知的 Actor,它们专门设计用来应对节点离开或加入集群的场景。在很多场景下,当我们设计和实现 Actor 系统使其更有弹性的时候,其实我们也为适应性打下了良好的基础。当你的系统能够处理分布式节点因为故障而离开集群,并且支持新节点取代故障节点的时候,其实在本质上来说节点因为故障离开或加入集群,这与为了应对用户的活动而调整可用的处理能力并没有什么差异。

消息至关重要

正如我前面讲到的,Actor 模型主要关注直接的异步消息。为了实现这一点,发送者有必要知道接收者的地址,这样才能将消息发送给接收者。

Actor 是无锁的结构,它们不会共享任何内容。如果三个发送者分别向一个接收者发送消息,接收者会在它的收件箱中对这些消息进行排队,每次只处理一条消息。因此,接收者不需要在内部使用锁来保护它的状态,以防止多线程对状态的同时操作。接收者不会将它的内部状态与其他的 Actor 共享。

Actor 模型还允许接收消息的 Actor 为下一条即将到达的消息做出调整。例如,假设有两个程序流序列。当第一个序列的发送者发送一条消息给接收者时,接收者对消息做出反应,然后将其自身转换为另外一种类型的消息监听者。现在,当第二个序列中的消息发送者发送消息给同一个 Actor 时,该 Actor 会在自己的 receive 代码块中使用不同的一组逻辑(当 Actor 改变状态时,它可以替换消息的接收逻辑。在 Akka 文档中,有一个这种类型的样例

用更少的资源做更多的事情

Actor 模型帮助我们解决的另外一个主要问题就是用更少的资源做更多的事情。各种规模的系统都能从中受益,从 Amazon 和 Netflix 所使用的大型网络到更小的架构均是如此。Actor 模型允许开发人员充分发挥每台服务器的最大能量,因此很有可能降低集群的规模。

基于 Actor 的服务可以有多少个 Actor 呢?五十个?一百个?都可以!基于 Actor 的系统非常灵活且具有适应性,它们可以支持大量的 Actor,甚至能到百万级别。

在典型的 N 层架构中,也可以称为“端口与适配器(ports-and-adapters)”或六边形架构,存在大量不必要的复杂性,甚至会有偶发的复杂性。Actor 模型最大的优势之一就是它能够消除很多的复杂性,它会将一组“控制器”适配器置于边界之上。控制器可以发送消息给领域模型,从而任务委托出去,领域模型进而发布事件。通过这种方式,Actor 模型能够在很大程度上降低网络复杂性,允许设计者用有限的资源和预算完成更多的事情。

使用领域驱动设计加速业务开发

领域驱动设计(DDD)的本质是在边界上下文(bounded context)建模一个通用(ubiquitous)语言。让我稍作解释:考虑将某个特定类型的服务建模为边界上下文。这个边界上下文是一个语义边界,该边界内的所有的内容(包括领域模型) 都有明确的定义,其中还包括一个团队成员所使用的语言,该语言能够帮助开发人员理解边界上下文中每个概念的含义。

接下来就是上下文映射(context-mapping)能够发挥作用的地方了,上下文映射会为每个边界上下文如何与其他的上下文对应、团队关系以及模型如何交互与集成等行为建模。使用上下文映射是非常重要的,因为边界上下文本身要比很多人在单体架构中所习惯的思维模式小得多。

对于任何企业和开发团队来说,响应快速变化的新业务方向都是很大的挑战。在处理这些不断演化的业务方向时,DDD 可以进行必要的知识消化(knowledge crunching)。Actor 和消息能够帮助开发人员快速实现领域模型以应对他们的需求,并且有助于对领域模型形成清晰的理解。

Actor 和 DDD:完美组合

Alan Kay 说过,“Actor 模型保留了对象理念更多的好特性”。Kay 还说过,“最大的理念是消息”。在创建通用语言的过程中,开发人员可以关注 Actor(将其作为对象或组件)、领域模型的元素以及它们之间的消息。

单个反应式服务无法构成完整的服务,反应式服务是来源于系统的。因此开发人员最终要试图完成的是构建整个系统,而不是单个服务。通过触发领域事件到其他的边界上下文或微服务,开发人员可以更容易地实现这一点。

为何 Actor 对业务更加有益?

Actor 可以和 DDD 非常理想地协作,因为它们都使用通用语言来表述核心业务领域。它们的设计都非常优雅,能够处理业务故障,不管网络出现了何种问题都能维护系统的弹性和快速响应。它们帮助开发人员扩展系统以满足并发的需求,当面临峰值负载时,进行适应性地扩展,并在流量降低的时候,进行收缩,从而最小化基础设施占用和硬件需求。这种模型非常适合当今高度分布式、多线程的环境,并且能够产生远远超出服务器能力空间的业务收益。

关于作者

Markus Eisele 是一位 Java Champion、前 Java EE 专家组成员、JavaLand 的创始人,在世界范围内的 Java 会议上是享有盛誉的讲师,在企业级 Java 领域颇为知名。他在 Lightbend 担任开发人员,读者可以在 Twitter 上 @myfear 联系到他。

Hugh McKee 是 Lightbend 的开发人员。在职业生涯中,他曾经长期构建演化缓慢的应用程序,这些应用无法高效地利用其基础设施,并且非常脆弱和易于出错。在开始构建反应式、异步、基于 Actor 的系统之后,一切都发生了变化。这种全新的构建应用程序的方式震撼了他。按照这种方式还有一个附加的好处,那就是构建应用系统会变得比以往更加有趣。现在,他主要帮助人们探索构建反应式、弹性、适应性和消息驱动应用程序的重要优势和乐趣。

查看英文原文: Building Reactive Systems Using Akka’s Actor Model and Domain-Driven Design

2018 年 3 月 22 日 18:1821559

评论

发布
暂无评论
发现更多内容

产品策略闭环是个什么环?

万事ONES

项目管理 研发管理 ONES 产品策略

我不服!这开源项目居然才888个星!?

why技术

Java

奇亚矿机分币系统搭建,Bzz云算力挖矿系统

13823153121

只记得文件类型如何用EasyRecovery实现恢复?

淋雨

数据恢复 EasyRecovery 文件恢复 照片恢复

和12岁小同志搞创客开发:如何选择合适的控制器?

不脱发的程序猿

DIY 创客开发 如何选择合适的控制器?

可视化及时把控营运状况,助力管理效率提升80%

一只数据鲸鱼

数据可视化 智慧城市 智慧园区 三维可视化 智慧楼宇

项目管理100问 | 研发团队如何实现无缝协作

万事ONES

项目管理 ONES Project 研发团队

☕【JVM 技术探索】Class字节码指令操作介绍(上)

浩宇天尚

Java JVM Class字节码 6月日更

WorkPlus Lite 企业级移动平台

WorkPlus Lite

ONES CTO 冯斌 | 大型团队敏捷项目管理实践与思考

万事ONES

项目管理 研发管理 团队协作 ONES 研发工具

为什么聪明的程序员会写出糟糕的代码

实力程序员

和12岁小同志搞创客开发:拿到一款控制器,要怎么分析?

不脱发的程序猿

DIY 创客开发 怎么分析控制器?

从单体系统到微服务

escray

学习 极客时间 朱赟的技术管理课 6月日更

相约厦门!HarmonyOS Connect伙伴峰会将于6月17日举办

科技汇

泰山版震撼来袭!阿里巴巴2021年Java程序员面试指导小册已开源

Java架构师迁哥

技术干货 | 如何实现对动态PPT的云端录制?

ZEGO即构

音视频 WebRTC RTC 即构 动态PPT录制

WorkPlus私有化「数智融合」移动平台

WorkPlus Lite

机器学习入门:多变量线性回归

华为云开发者社区

机器学习 多变量线性回归

Electron 开发音视频

anyRTC开发者

Java 音视频 WebRTC Electron RTC

GrowingIO 增长平台产研项目管理实践

GrowingIO技术专栏

项目管理 程序员 Jira growingio

【布道API】API设计应该了解的HTTP方法和特性

devpoint

RESTful HTTP协议 6月日更

Python接口自动化之request请求封装

行者AI

接口 测试 自动化测试 封装

开源,让程序员找回热血和激情,参与开源,为中国科技助力

陆陆通通

开源 鸿蒙 程序员

云小课 | 华为云KYON之私网NAT网关

华为云开发者社区

网关 华为云 KYON企业级云网络 私网NAT网关 重叠组网

【FlinkSQL】Flink SQL Query 语法(二)

Alex🐒

flink FlinkSQL flink1.13

从理论到实战只需七天!阿里P10撰写的Spring全家桶有多全面?

Java架构追梦

Java 阿里巴巴 架构 springboot SpringCloud

优秀的开发者每天都在做什么?

学神来啦

程序员 码农 编码 经验分享

Python——列表元素的增删改

在即

6月日更

助力碳中和,EMQ与SAP共同构建绿色IoT解决方案

EMQ映云科技

开源 5G 碳中和 SAP 碳达峰

「终!」☕️【Java技术之旅】带你进入String类的易错点和底层本质分析!

浩宇天尚

Java 字符串 字符串常量池 6月日更

Dokcer Compose部署Nebula Graph配置文件

阿呆

配置信息

React Native 核心原理及跨端选型思路

React Native 核心原理及跨端选型思路

使用Akka的Actor模型和领域驱动设计构建反应式系统-InfoQ