在 Concepts 部分获取 Dapr 构建块概述。

This is the multi-page printable view of this section. Click here to print.
在 Concepts 部分获取 Dapr 构建块概述。

通过服务调用,您的应用程序可以使用标准的 gRPC 或 HTTP 协议可靠且安全地与其他应用程序通信。
在许多基于微服务的应用程序中,多个服务需要相互通信的能力。这种服务间通信要求应用程序开发者处理以下问题:
Dapr 通过提供服务调用 API 来应对这些挑战,该 API 的行为类似于具有内置服务发现功能的反向代理,同时利用了内置的分布式追踪、指标、错误处理、加密等功能。
Dapr 使用边车架构。要使用 Dapr 调用应用程序:
invoke API。以下概述视频和演示 演示了 Dapr 服务调用的工作原理。
下图是 Dapr 服务调用在两个 Dapr 化应用程序之间工作方式的概述。

您还可以使用服务调用 API 调用非 Dapr HTTP 端点。例如,您可能只在部分整体应用程序中使用 Dapr,可能无法访问用于将现有应用程序迁移到使用 Dapr 的代码,或者只是需要调用外部 HTTP 服务。阅读“如何:使用 HTTP 调用非 Dapr 端点” 获取更多信息。
服务调用提供了多项功能,使您可以轻松地在应用程序之间调用方法或调用外部 HTTP 端点。
dapr-app-id 头即可。更多信息请参阅使用 HTTP 调用服务。通过 Dapr Sentry 服务,Dapr 应用程序之间的所有调用都可以使用托管平台上的相互(mTLS)身份验证来保证安全,包括自动证书轮换。
更多信息请阅读服务到服务安全文章。
在发生调用失败和瞬态错误的情况下,服务调用提供了一种弹性功能,可以执行带退避时间的自动重试。了解更多,请参阅弹性文章。
默认情况下,应用程序之间的所有调用都会被追踪,并收集指标以提供洞察和诊断。这在生产场景中尤为重要,提供服务间调用的调用图和指标。更多信息请阅读可观测性。
通过访问策略,应用程序可以控制:
例如,您可以限制包含人员信息的敏感应用程序被未经授权的应用程序访问。结合服务到服务安全通信,您可以提供软多租户部署。
更多信息请阅读服务调用的访问控制白名单文章。
您可以将应用程序作用域限定到命名空间,用于部署和安全目的,并可以调用部署在不同命名空间中的服务。更多信息请阅读跨命名空间的服务调用文章。
Dapr 使用 mDNS 协议提供服务调用的轮询负载均衡,例如在单机上或多个联网的物理机器上。
下图展示了这如何工作的示例。如果您有 1 个应用程序 ID 为 FrontEnd 的应用程序实例和 3 个应用程序 ID 为 Cart 的应用程序实例,当您从 FrontEnd 应用调用 Cart 应用时,Dapr 会在这 3 个实例之间轮询。这些实例可以在同一台机器上,也可以在不同的机器上。

注意:应用程序 ID 在每个_应用程序_中是唯一的,而不是应用程序实例。无论该应用程序存在多少个实例(由于扩展),它们都将共享相同的应用程序 ID。
Dapr 可以在各种托管平台上运行。为了使服务调用支持可交换的服务发现,Dapr 使用名称解析组件。例如,Kubernetes 名称解析组件使用 Kubernetes DNS 服务来解析集群中运行的其他应用程序的位置。
自托管机器可以使用 mDNS 名称解析组件。作为替代方案,您可以使用 SQLite 名称解析组件在单节点环境和本地开发场景中运行 Dapr。作为集群一部分的 Dapr 边车将其信息存储在本地机器上的 SQLite 数据库中。
Consul 名称解析组件特别适用于多机器部署,可用于任何托管环境,包括 Kubernetes、多个虚拟机或自托管。
您可以在 HTTP 服务调用中将数据作为流处理。当使用 Dapr 通过 HTTP 调用另一个服务并处理大型请求或响应体时,这可以提供性能和内存利用率方面的改进。
下图演示了数据流的六个步骤。

按照上述调用顺序,假设您有 Hello World 教程 中描述的应用程序,其中一个 Python 应用程序调用一个 Node.js 应用程序。在这种情况下,Python 应用程序是"服务 A",Node.js 应用程序是"服务 B"。
下图再次展示了在本地机器上显示 API 调用的序列 1-7:

nodeapp。Python 应用程序通过 POST http://localhost:3500/v1.0/invoke/nodeapp/method/neworder 来调用 Node.js 应用程序的 neworder 方法,该请求首先到达 Python 应用程序的本地 Dapr 边车。Dapr 文档包含多个快速入门,展示了在不同示例架构中利用服务调用构建块的方式。要直接了解服务调用 API 及其功能,我们建议从以下快速入门开始:
| 快速入门/教程 | 描述 |
|---|---|
| 服务调用快速入门 | 此快速入门让您直接与服务调用构建块交互。 |
| Hello world 教程 | 本教程展示如何同时使用服务调用和状态管理构建块,全部在本地机器上运行。 |
| Hello world Kubernetes 教程 | 本教程介绍如何在 Kubernetes 中使用 Dapr,涵盖服务调用和状态管理构建块。 |
想跳过快速入门吗?没问题。您可以直接在应用程序中试用服务调用构建块,与其他服务安全通信。安装 Dapr 后,您可以通过以下方式开始使用服务调用 API。
使用以下方式调用服务:
dapr-app-id 头即可开始使用。点击此处了解更多:使用 HTTP 调用服务。localhost:<dapr-http-port>,您就可以直接调用 API。您还可以在上面 HTTP 代理部分链接的"使用 HTTP 调用服务"文档中了解更多。要进行快速测试,请尝试使用 Dapr CLI 进行服务调用:
dapr invoke --method <method-name> 命令以及方法标志和感兴趣的方法。在 Dapr CLI中了解更多。本文演示了如何部署服务,每个服务都有一个唯一的 application ID,其他服务可以使用 HTTP 上的服务调用来发现它们并调用其端点。

Dapr 允许你为你的应用分配一个全局唯一的 ID。这个 ID 封装了你的应用的状态,无论它可能有多少个实例。
dapr run --app-id checkout --app-protocol http --dapr-http-port 3500 -- python3 checkout/app.py
dapr run --app-id order-processor --app-port 8001 --app-protocol http --dapr-http-port 3501 -- python3 order-processor/app.py
如果你的应用使用 TLS,你可以通过设置 --app-protocol https 来告诉 Dapr 通过 TLS 连接调用你的应用:
dapr run --app-id checkout --app-protocol https --dapr-http-port 3500 -- python3 checkout/app.py
dapr run --app-id order-processor --app-port 8001 --app-protocol https --dapr-http-port 3501 -- python3 order-processor/app.py
dapr run --app-id checkout --app-protocol http --dapr-http-port 3500 -- npm start
dapr run --app-id order-processor --app-port 5001 --app-protocol http --dapr-http-port 3501 -- npm start
如果你的应用使用 TLS,你可以通过设置 --app-protocol https 来告诉 Dapr 通过 TLS 连接调用你的应用:
dapr run --app-id checkout --dapr-http-port 3500 --app-protocol https -- npm start
dapr run --app-id order-processor --app-port 5001 --dapr-http-port 3501 --app-protocol https -- npm start
dapr run --app-id checkout --app-protocol http --dapr-http-port 3500 -- dotnet run
dapr run --app-id order-processor --app-port 7001 --app-protocol http --dapr-http-port 3501 -- dotnet run
如果你的应用使用 TLS,你可以通过设置 --app-protocol https 来告诉 Dapr 通过 TLS 连接调用你的应用:
dapr run --app-id checkout --dapr-http-port 3500 --app-protocol https -- dotnet run
dapr run --app-id order-processor --app-port 7001 --dapr-http-port 3501 --app-protocol https -- dotnet run
dapr run --app-id checkout --app-protocol http --dapr-http-port 3500 -- java -jar target/CheckoutService-0.0.1-SNAPSHOT.jar
dapr run --app-id order-processor --app-port 9001 --app-protocol http --dapr-http-port 3501 -- java -jar target/OrderProcessingService-0.0.1-SNAPSHOT.jar
如果你的应用使用 TLS,你可以通过设置 --app-protocol https 来告诉 Dapr 通过 TLS 连接调用你的应用:
dapr run --app-id checkout --dapr-http-port 3500 --app-protocol https -- java -jar target/CheckoutService-0.0.1-SNAPSHOT.jar
dapr run --app-id order-processor --app-port 9001 --dapr-http-port 3501 --app-protocol https -- java -jar target/OrderProcessingService-0.0.1-SNAPSHOT.jar
dapr run --app-id checkout --dapr-http-port 3500 -- go run .
dapr run --app-id order-processor --app-port 6006 --app-protocol http --dapr-http-port 3501 -- go run .
如果你的应用使用 TLS,你可以通过设置 --app-protocol https 来告诉 Dapr 通过 TLS 连接调用你的应用:
dapr run --app-id checkout --dapr-http-port 3500 --app-protocol https -- go run .
dapr run --app-id order-processor --app-port 6006 --dapr-http-port 3501 --app-protocol https -- go run .
在 Kubernetes 中,在你的 pod 上设置 dapr.io/app-id 注解:
apiVersion: apps/v1
kind: Deployment
metadata:
name: <language>-app
namespace: default
labels:
app: <language>-app
spec:
replicas: 1
selector:
matchLabels:
app: <language>-app
template:
metadata:
labels:
app: <language>-app
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "order-processor"
dapr.io/app-port: "6001"
...
如果你的应用使用 TLS 连接,你可以使用 app-protocol: "https" 注解(完整列表在这里)来告诉 Dapr 通过 TLS 调用你的应用。请注意,Dapr 不会验证应用提供的 TLS 证书。
要使用 Dapr 调用应用程序,你可以在任何 Dapr 实例上使用 invoke API。边车编程模型鼓励每个应用程序与自己的 Dapr 实例交互。Dapr 边车之间相互发现和通信。
以下是利用 Dapr SDK 进行服务调用的代码示例。
#dependencies
import random
from time import sleep
import logging
import requests
#code
logging.basicConfig(level = logging.INFO)
while True:
sleep(random.randrange(50, 5000) / 1000)
orderId = random.randint(1, 1000)
#Invoke a service
result = requests.post(
url='%s/orders' % (base_url),
data=json.dumps(order),
headers=headers
)
logging.basicConfig(level = logging.INFO)
logging.info('Order requested: ' + str(orderId))
logging.info('Result: ' + str(result))
//dependencies
import axios from "axios";
//code
const daprHost = "127.0.0.1";
var main = function() {
for(var i=0;i<10;i++) {
sleep(5000);
var orderId = Math.floor(Math.random() * (1000 - 1) + 1);
start(orderId).catch((e) => {
console.error(e);
process.exit(1);
});
}
}
//Invoke a service
const result = await axios.post('order-processor' , "orders/" + orderId , axiosConfig);
console.log("Order requested: " + orderId);
console.log("Result: " + result.config.data);
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
main();
//dependencies
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Threading;
//code
namespace EventService
{
class Program
{
static async Task Main(string[] args)
{
while(true) {
await Task.Delay(5000)
var random = new Random();
var orderId = random.Next(1,1000);
//Using Dapr SDK to invoke a method
var order = new Order(orderId.ToString());
var httpClient = DaprClient.CreateInvokeHttpClient();
var response = await httpClient.PostAsJsonAsync("http://order-processor/orders", order);
var result = await response.Content.ReadAsStringAsync();
Console.WriteLine("Order requested: " + orderId);
Console.WriteLine("Result: " + result);
}
}
}
}
//dependencies
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//code
@SpringBootApplication
public class CheckoutServiceApplication {
private static final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10))
.build();
public static void main(String[] args) throws InterruptedException, IOException {
while (true) {
TimeUnit.MILLISECONDS.sleep(5000);
Random random = new Random();
int orderId = random.nextInt(1000 - 1) + 1;
// Create a Map to represent the request body
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("orderId", orderId);
// Add other fields to the requestBody Map as needed
HttpRequest request = HttpRequest.newBuilder()
.POST(HttpRequest.BodyPublishers.ofString(new JSONObject(requestBody).toString()))
.uri(URI.create(dapr_url))
.header("Content-Type", "application/json")
.header("dapr-app-id", "order-processor")
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Order passed: " + orderId);
TimeUnit.MILLISECONDS.sleep(1000);
log.info("Order requested: " + orderId);
log.info("Result: " + response.body());
}
}
}
package main
import (
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"time"
)
func main() {
daprHttpPort := os.Getenv("DAPR_HTTP_PORT")
if daprHttpPort == "" {
daprHttpPort = "3500"
}
client := &http.Client{
Timeout: 15 * time.Second,
}
for i := 0; i < 10; i++ {
time.Sleep(5000)
orderId := rand.Intn(1000-1) + 1
url := fmt.Sprintf("http://localhost:%s/checkout/%v", daprHttpPort, orderId)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
panic(err)
}
// Adding target app id as part of the header
req.Header.Add("dapr-app-id", "order-processor")
// Invoking a service
resp, err := client.Do(req)
if err != nil {
log.Fatal(err.Error())
}
b, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(b))
}
}
要调用 ‘GET’ 端点:
curl http://localhost:3602/v1.0/invoke/checkout/method/checkout/100
为了尽可能避免更改 URL 路径,Dapr 提供了以下方式调用服务调用 API:
localhost:<dapr-http-port>。dapr-app-id 头来指定目标服务的 ID,或者通过 HTTP Basic Auth 传递 ID:http://dapr-app-id:<service-id>@localhost:3602/path。例如,以下命令:
curl http://localhost:3602/v1.0/invoke/checkout/method/checkout/100
等价于:
curl -H 'dapr-app-id: checkout' 'http://localhost:3602/checkout/100' -X POST
或者:
curl 'http://dapr-app-id:checkout@localhost:3602/checkout/100' -X POST
使用 CLI:
dapr invoke --app-id checkout --method checkout/100
你也可以在 URL 末尾附加查询字符串或片段,Dapr 会原样传递。这意味着如果你需要在服务调用中传递一些不属于负载或路径的额外参数,可以通过在 URL 末尾附加 ? 来实现,后跟用 = 分隔的键值对,用 & 分隔。例如:
curl 'http://dapr-app-id:checkout@localhost:3602/checkout/100?basket=1234&key=abc' -X POST
在支持命名空间的平台上运行时,你需要在 app ID 中包含目标应用的命名空间。例如,遵循 <app>.<namespace> 格式,使用 checkout.production。
使用此示例,带命名空间调用服务如下:
curl http://localhost:3602/v1.0/invoke/checkout.production/method/checkout/100 -X POST
有关命名空间的更多信息,请参见跨命名空间 API 规范。
上面的示例向你展示了如何直接调用在本地或 Kubernetes 中运行的不同服务。Dapr:
有关追踪和日志的更多信息,请参见可观测性文章。
本文介绍如何使用 Dapr 通过 gRPC 连接服务。
通过使用 Dapr 的 gRPC 代理能力,你可以使用现有的基于 proto 的 gRPC 服务,并让流量通过 Dapr 边车。这样做可以为开发者带来以下 Dapr 服务调用 优势:
Dapr 允许代理各种类型的 gRPC 调用,包括一元调用和基于流的调用。
以下示例取自 “hello world” grpc-go 示例。虽然此示例使用 Go,但相同的概念适用于 gRPC 支持的所有编程语言。
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
)
const (
port = ":50051"
)
// server 用于实现 helloworld.GreeterServer。
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello 实现 helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
此 Go 应用实现了 Greeter proto 服务,并公开了一个 SayHello 方法。
dapr run --app-id server --app-port 50051 -- go run main.go
使用 Dapr CLI,我们通过 --app-id 标志为应用分配一个唯一 ID server。
以下示例展示如何从 gRPC 客户端使用 Dapr 发现 Greeter 服务。
请注意,客户端不是直接在端口 50051 上调用目标服务,而是通过端口 50007 调用其本地 Dapr 边车,从而获得服务调用的所有能力,包括服务发现、追踪、mTLS 和重试。
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
"google.golang.org/grpc/metadata"
)
const (
address = "localhost:50007"
)
func main() {
// 建立与服务器的连接。
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
ctx = metadata.AppendToOutgoingContext(ctx, "dapr-app-id", "server")
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "Darth Tyrannus"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
}
以下代码行告诉 Dapr 发现并调用名为 server 的应用:
ctx = metadata.AppendToOutgoingContext(ctx, "dapr-app-id", "server")
所有支持 gRPC 的语言都允许添加元数据。以下是几个示例:
Metadata headers = new Metadata();
Metadata.Key<String> jwtKey = Metadata.Key.of("dapr-app-id", "server");
GreeterService.ServiceBlockingStub stub = GreeterService.newBlockingStub(channel);
stub = MetadataUtils.attachHeaders(stub, header);
stub.SayHello(new HelloRequest() { Name = "Darth Malak" });
var metadata = new Metadata
{
{ "dapr-app-id", "server" }
};
var call = client.SayHello(new HelloRequest { Name = "Darth Nihilus" }, metadata);
metadata = (('dapr-app-id', 'server'),)
response = stub.SayHello(request={ name: 'Darth Revan' }, metadata=metadata)
const metadata = new grpc.Metadata();
metadata.add('dapr-app-id', 'server');
client.sayHello({ name: "Darth Malgus" }, metadata)
metadata = { 'dapr-app-id' : 'server' }
response = service.sayHello({ 'name': 'Darth Bane' }, metadata)
grpc::ClientContext context;
context.AddMetadata("dapr-app-id", "server");
dapr run --app-id client --dapr-grpc-port 50007 -- go run main.go
如果你在本地运行 Dapr 并安装了 Zipkin,请在浏览器中打开 http://localhost:9411 查看客户端和服务器之间的追踪信息。
在你的部署上设置以下 Dapr 注解:
apiVersion: apps/v1
kind: Deployment
metadata:
name: grpc-app
namespace: default
labels:
app: grpc-app
spec:
replicas: 1
selector:
matchLabels:
app: grpc-app
template:
metadata:
labels:
app: grpc-app
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "server"
dapr.io/app-protocol: "grpc"
dapr.io/app-port: "50051"
...
dapr.io/app-protocol: "grpc" 注解告诉 Dapr 使用 gRPC 调用应用。
如果你的应用使用 TLS 连接,你可以通过 app-protocol: "grpcs" 注解告诉 Dapr 通过 TLS 调用你的应用(完整列表见这里)。请注意,Dapr 不验证应用提供的 TLS 证书。
在支持命名空间的平台上运行时,你需要在应用 ID 中包含目标应用的命名空间:myApp.production
例如,调用不同命名空间上的 gRPC 服务器:
ctx = metadata.AppendToOutgoingContext(ctx, "dapr-app-id", "server.production")
有关命名空间的更多信息,请参阅跨命名空间 API 规范。
上面的示例向你展示如何直接调用本地或 Kubernetes 上运行的不同服务。Dapr 输出指标、追踪和日志信息,使你能够可视化服务之间的调用图、记录错误,并可选择记录有效负载正文。
有关追踪和日志的更多信息,请参阅可观测性文章。
使用 Dapr 通过 gRPC 代理流式 RPC 调用时,必须设置一个额外的元数据选项 dapr-stream,其值为 true。
例如:
ctx = metadata.AppendToOutgoingContext(ctx, "dapr-app-id", "server")
ctx = metadata.AppendToOutgoingContext(ctx, "dapr-stream", "true")
Metadata headers = new Metadata();
Metadata.Key<String> jwtKey = Metadata.Key.of("dapr-app-id", "server");
Metadata.Key<String> jwtKey = Metadata.Key.of("dapr-stream", "true");
var metadata = new Metadata
{
{ "dapr-app-id", "server" },
{ "dapr-stream", "true" }
};
metadata = (('dapr-app-id', 'server'), ('dapr-stream', 'true'),)
const metadata = new grpc.Metadata();
metadata.add('dapr-app-id', 'server');
metadata.add('dapr-stream', 'true');
metadata = { 'dapr-app-id' : 'server' }
metadata = { 'dapr-stream' : 'true' }
grpc::ClientContext context;
context.AddMetadata("dapr-app-id", "server");
context.AddMetadata("dapr-stream", "true");
目前,通过 gRPC 进行服务调用时不支持弹性策略。
代理流式 gRPC 时,由于其长期存在的特性,弹性策略仅应用于"初始握手"。因此:
观看此视频,了解如何使用 Dapr 的 gRPC 代理能力:
本文演示如何使用 HTTP 通过 Dapr 调用非 Dapr 端点。
使用 Dapr 的服务调用 API,您可以与使用或不使用 Dapr 的端点进行通信。使用 Dapr 调用不使用 Dapr 的端点不仅能提供一致的 API,还能带来以下 Dapr 服务调用 优势:
有时您需要调用非 Dapr HTTP 端点。例如:
通过定义 HTTPEndpoint 资源,您可以声明式地定义与非 Dapr 端点交互的方式。然后使用服务调用 URL 来调用非 Dapr 端点。或者,您也可以直接将非 Dapr 的完全限定域名(FQDN)端点 URL 放入服务调用 URL 中。
使用服务调用时,Dapr 运行时遵循以下优先级顺序:
HTTPEndpoint 资源吗?http:// 或 https:// 前缀的 FQDN URL 吗?appID 吗?下图概述了 Dapr 的服务调用在调用非 Dapr 端点时的工作原理。

HTTPEndpoint 或 FQDN URL 发现服务 B 的位置,然后将消息转发给服务 B。在与 Dapr 应用程序或非 Dapr 应用程序通信时,有两种方法可以调用非 Dapr 端点。Dapr 应用程序可以通过提供以下内容之一来调用非 Dapr 端点:
命名的 HTTPEndpoint 资源,包括定义 HTTPEndpoint 资源类型。有关示例,请参阅 HTTPEndpoint 参考 指南。
localhost:3500/v1.0/invoke/<HTTPEndpoint-name>/method/<my-method>
例如,对于名为 “palpatine” 的 HTTPEndpoint 资源和名为 “Order66” 的方法,调用方式如下:
curl http://localhost:3500/v1.0/invoke/palpatine/method/order66
指向非 Dapr 端点的 FQDN URL。
localhost:3500/v1.0/invoke/<URL>/method/<my-method>
例如,对于名为 https://darthsidious.starwars 的 FQDN 资源,调用方式如下:
curl http://localhost:3500/v1.0/invoke/https://darthsidious.starwars/method/order66
调用使用 appID 的 Dapr 应用程序时,始终使用 AppID。有关更多信息,请阅读 操作指南:使用 HTTP 调用服务 指南。例如:
localhost:3500/v1.0/invoke/<appID>/method/<my-method>
curl http://localhost:3602/v1.0/invoke/orderprocessor/method/checkout
使用 HTTPEndpoint 资源 允许您根据远程端点的身份验证要求,使用根证书、客户端证书和私钥的任意组合。
apiVersion: dapr.io/v1alpha1
kind: HTTPEndpoint
metadata:
name: "external-http-endpoint-tls"
spec:
baseUrl: https://service-invocation-external:443
headers:
- name: "Accept-Language"
value: "en-US"
clientTLS:
rootCA:
secretKeyRef:
name: dapr-tls-client
key: ca.crt
apiVersion: dapr.io/v1alpha1
kind: HTTPEndpoint
metadata:
name: "external-http-endpoint-tls"
spec:
baseUrl: https://service-invocation-external:443
headers:
- name: "Accept-Language"
value: "en-US"
clientTLS:
certificate:
secretKeyRef:
name: dapr-tls-client
key: tls.crt
privateKey:
secretKeyRef:
name: dapr-tls-key
key: tls.key
SSE 支持与流服务器和 MCP 服务器进行实时通信。
HTTP 端点支持 服务器发送事件(SSE)。
要使用 SSE,请在 HTTPEndpoint 资源或服务调用请求中将 Accept 标头设置为 text/event-stream。
apiVersion: dapr.io/v1alpha1
kind: HTTPEndpoint
metadata:
name: "mcp-server"
spec:
baseUrl: https://my-mcp-server:443
headers:
- name: "Accept"
value: "test/event-stream"
观看此 视频,了解如何使用服务调用调用非 Dapr 端点。
在本文中,你将学习如何调用部署在不同命名空间中的服务。默认情况下,服务调用支持通过简单引用应用 ID(nodeapp)来调用同一命名空间内的服务:
localhost:3500/v1.0/invoke/nodeapp/method/neworder
服务调用也支持跨命名空间调用。在所有支持的托管平台上,Dapr 应用 ID 符合包含目标命名空间的有效 FQDN 格式。你可以同时指定:
nodeapp),以及production)。示例 1
调用 production 命名空间中 nodeapp 上的 neworder 方法:
localhost:3500/v1.0/invoke/nodeapp.production/method/neworder
使用服务调用调用命名空间中的应用程序时,需要用命名空间进行限定。这在 Kubernetes 集群中的跨命名空间调用中非常有用。
示例 2
调用 production 命名空间中 myapp 上的 ping 方法:
https://localhost:3500/v1.0/invoke/myapp.production/method/ping
示例 3
使用 curl 命令从外部 DNS 地址(此处为 api.demo.dapr.team)调用与示例 2 相同的 ping 方法,并提供 Dapr API 令牌进行身份验证:
MacOS/Linux:
curl -i -d '{ "message": "hello" }' \
-H "Content-type: application/json" \
-H "dapr-api-token: ${API_TOKEN}" \
https://api.demo.dapr.team/v1.0/invoke/myapp.production/method/ping
了解如何使用 Dapr Pub/sub:
发布订阅(pub/sub)使微服务能够使用消息进行通信,实现事件驱动架构。
中间消息代理将每条消息从发布者的输入通道复制到所有对该消息感兴趣的订阅者的输出通道。当你需要将微服务相互解耦时,这种模式特别有用。

Dapr 中的发布订阅 API:
服务使用的特定消息代理是可插拔的,在运行时配置为 Dapr 发布订阅组件。这消除了服务对消息代理的依赖,使服务更具可移植性和灵活性。
使用 Dapr 中的发布订阅时:
以下概述视频和演示 演示了 Dapr 发布订阅的工作原理。
在下图中,“shipping"服务和 “email” 服务都订阅了 “cart” 服务发布的主题。每个服务加载指向同一发布订阅消息代理组件的发布订阅组件配置文件;例如:Redis Streams、NATS Streaming、Azure Service Bus 或 GCP pub/sub。

在下图中,Dapr API 将来自发布 “cart” 服务的 “order” 主题发布到订阅服务 “shipping” 和 “email” 的 “order” 端点。

发布订阅 API 构建块为你的应用程序带来多项功能。
为了实现消息路由并在服务之间提供每个消息的额外上下文,Dapr 使用 CloudEvents 1.0 规范 作为其消息格式。任何使用 Dapr 发送到主题的应用程序消息都会自动包装在 Cloud Events 信封中,使用 Content-Type 头值 作为 datacontenttype 属性。
更多信息,请阅读 使用 CloudEvents 进行消息传递 或 发送不带 CloudEvents 的原始消息。
如果你的一个应用程序使用 Dapr 而另一个不使用,你可以为发布者或订阅者禁用 CloudEvent 包装。这允许在无法同时采用 Dapr 的应用程序中部分采用 Dapr 发布订阅。
更多信息,请阅读 如何在不使用 CloudEvents 的情况下使用发布订阅。
发布消息时,指定正在发送的数据的内容类型非常重要。除非指定,否则 Dapr 将假定为 text/plain。
Content-Type 头中设置内容类型原则上,Dapr 认为订阅者处理消息并返回非错误响应后,消息即成功传递。为了更精细的控制,Dapr 的发布订阅 API 还提供明确的状态(在响应负载中定义),订阅者使用这些状态向 Dapr 指示特定的处理指令(例如,RETRY 或 DROP)。
Dapr 应用程序可以通过支持相同功能的三种订阅类型订阅已发布的主题:声明式、流式和编程式。
| 订阅类型 | 描述 |
|---|---|
| 声明式 | 订阅在外部文件中定义。声明式方法从代码中移除 Dapr 依赖,允许现有应用程序订阅主题,而无需更改代码。 |
| 流式 | 订阅在用户代码中定义。流式订阅是动态的,意味着它们允许在运行时添加或移除订阅。它们不需要应用程序中的订阅端点(这是编程式和声明式订阅都需要的),使得在代码中配置变得简单。流式订阅也不需要应用程序配置边车来接收消息。使用流式订阅,由于消息被发送到消息处理程序代码,因此没有路由或批量订阅的概念。 |
| 编程式 | 订阅在用户代码中定义。编程式方法实现静态订阅,需要代码中的端点。 |
更多信息,请阅读 关于订阅类型的订阅。
要重新加载以编程方式或声明方式定义的主题订阅,需要重启 Dapr 边车。
通过启用 HotReload 特性门控 可以使 Dapr 边车动态重新加载已更改的声明式主题订阅,而无需重启。
主题订阅的热重载目前是预览功能。
重新加载订阅时,正在传输的消息不受影响。
Dapr 提供基于内容的路由模式。发布订阅路由 是此模式的实现,允许开发者使用表达式根据 CloudEvents 的内容将消息路由到应用程序中的不同 URI/路径和事件处理程序。如果没有路由匹配,则使用可选的默认路由。随着你的应用程序扩展以支持多个事件版本或特殊情况,这非常有用。
此功能适用于声明式和编程式订阅方法。
有关消息路由的更多信息,请阅读 Dapr 发布订阅 API 参考。
有时,消息由于多种可能的问题无法处理,例如生产者或消费者应用程序内部的错误条件或导致应用程序代码出现问题的意外状态更改。Dapr 允许开发者设置死信主题来处理无法传递到应用程序的消息。此功能在所有发布订阅组件上可用,可防止消费者应用程序无休止地重试失败的消息。更多信息,请阅读 关于死信主题.
Dapr 使开发者能够使用发件箱模式在事务性状态存储和任何消息代理之间实现单一事务。更多信息,请阅读 如何启用事务性发件箱消息传递。
Dapr 通过消费者组命名空间 在大规模场景下解决多租户问题。只需在组件元数据中包含 "{namespace}" 值,即可让具有相同 app-id 的多个命名空间的应用程序发布和订阅到同一消息代理。
Dapr 保证消息传递的至少一次语义。当应用程序使用发布订阅 API 向主题发布消息时,Dapr 确保消息至少一次传递到每个订阅者。
即使消息传递失败或应用程序崩溃,Dapr 也会重试重新传递消息直到成功传递。
所有 Dapr 发布订阅组件都支持至少一次保证。
Dapr 自动重试失败的订阅启动,以提高部署场景下的可靠性。这确保了你的发布订阅应用程序即使在面临临时连接或权限问题时也能保持弹性。
当 Dapr 遇到启动订阅的错误时,它会在日志中显示错误消息并继续尝试启动订阅。
Dapr 处理消费者组和竞争消费者模式的负担。在竞争消费者模式中,使用单个消费者组的多个应用程序实例竞争消息。当副本使用相同的 app-id 而没有明确的消费者组覆盖时,Dapr 强制执行竞争消费者模式。
当同一应用程序的多个实例(具有相同的 app-id)订阅主题时,Dapr 将每条消息传递到该应用程序的只有一个实例。此概念如下图所示。

同样,如果两个不同的应用程序(具有不同的 app-id)订阅同一主题,Dapr 将每条消息传递到每个应用程序的只有一个实例。
并非所有 Dapr 发布订阅组件都支持竞争消费者模式。目前,以下(非详尽)发布订阅组件支持此功能:
默认情况下,与发布订阅组件实例关联的所有主题消息都可用于使用该组件配置的每个应用程序。你可以使用 Dapr 主题限制来限制哪些应用程序可以发布或订阅主题。更多信息,请阅读:发布订阅主题限制。
Dapr 可以按消息设置超时消息,这意味着如果消息未从发布订阅组件中读取,则消息将被丢弃。此超时消息可防止未读消息堆积。如果消息在队列中停留的时间超过配置的 TTL,它将被标记为死信。更多信息,请阅读 发布订阅消息 TTL。
Dapr 支持在单个请求中发送和接收多条消息。编写需要发送或接收大量消息的应用程序时,使用批量操作可以减少请求总数来实现高吞吐量。更多信息,请阅读 发布订阅批量消息。
在 Kubernetes 上运行时,订阅者结合 StatefulSets 和 {podName} 标记可以拥有每个实例的粘性 consumerID。请参阅 如何使用 StatefulSets 水平扩展订阅者。
想测试 Dapr 发布订阅 API?请完成以下快速入门和教程来了解发布订阅的实际应用:
| 快速入门/教程 | 描述 |
|---|---|
| 发布订阅快速入门 | 使用发布订阅 API 发送和接收消息。 |
| 发布订阅教程 | 演示如何使用 Dapr 启用发布订阅应用程序。使用 Redis 作为发布订阅组件。 |
想跳过快速入门?没问题。你可以直接在应用程序中试用发布订阅构建块来发布消息和订阅主题。安装 Dapr 后,你可以从 发布订阅操作指南 开始使用发布订阅 API。
既然您已经了解了 Dapr 发布订阅构建块提供的能力,接下来了解如何在您的服务中使用它。下面的代码示例简单描述了一个包含两个服务的订单处理应用程序,每个服务都配置了 Dapr 边车:

Dapr 会自动使用 Content-Type 头部的值作为 datacontenttype 属性,将用户负载封装在一个符合 CloudEvents v1.0 标准的信封中。了解更多关于 CloudEvents 消息的信息。
下面的示例演示了您的应用程序如何发布和订阅名为 orders 的主题。
第一步是设置发布/订阅组件:
当您运行 dapr init 时,Dapr 会创建一个默认的 Redis pubsub.yaml 并在您的本地机器上运行一个 Redis 容器,位于:
%UserProfile%\.dapr\components\pubsub.yaml~/.dapr/components/pubsub.yaml通过 pubsub.yaml 组件,您可以轻松更换底层组件而无需更改应用程序代码。在本示例中,使用的是 RabbitMQ。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: order-pub-sub
spec:
type: pubsub.rabbitmq
version: v1
metadata:
- name: host
value: "amqp://localhost:5672"
- name: durable
value: "false"
- name: deletedWhenUnused
value: "false"
- name: autoAck
value: "false"
- name: reconnectWait
value: "0"
- name: concurrency
value: parallel
scopes:
- orderprocessing
- checkout
您可以通过创建一个包含该文件的组件目录(在本例中为 myComponents)并在 dapr run CLI 命令中使用 --resources-path 标志,用另一个发布订阅组件覆盖此文件。
要将其部署到 Kubernetes 集群中,请填写下面 YAML 中发布/订阅组件的 metadata 连接详情,将其保存为 pubsub.yaml,然后运行 kubectl apply -f pubsub.yaml。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: order-pub-sub
spec:
type: pubsub.rabbitmq
version: v1
metadata:
- name: connectionString
value: "amqp://localhost:5672"
- name: protocol
value: amqp
- name: hostname
value: localhost
- name: username
value: username
- name: password
value: password
- name: durable
value: "false"
- name: deletedWhenUnused
value: "false"
- name: autoAck
value: "false"
- name: reconnectWait
value: "0"
- name: concurrency
value: parallel
scopes:
- orderprocessing
- checkout
dapr run --app-id myapp --resources-path ./myComponents -- dotnet run
dapr run --app-id myapp --resources-path ./myComponents -- mvn spring-boot:run
dapr run --app-id myapp --resources-path ./myComponents -- python3 app.py
dapr run --app-id myapp --resources-path ./myComponents -- go run app.go
dapr run --app-id myapp --resources-path ./myComponents -- npm start
Dapr 提供了三种订阅主题的方法:
在声明式、流式和编程式订阅文档中了解更多信息。本示例演示声明式订阅。
创建一个名为 subscription.yaml 的文件并粘贴以下内容:
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: order-pub-sub
spec:
topic: orders
routes:
default: /checkout
pubsubname: order-pub-sub
scopes:
- orderprocessing
- checkout
上面的示例展示了对 orders 主题的事件订阅,使用的发布订阅组件是 order-pub-sub。
route 字段告诉 Dapr 将所有主题消息发送到应用程序中的 /checkout 端点。scopes 字段为 ID 为 orderprocessing 和 checkout 的应用程序启用此订阅。将 subscription.yaml 放在与 pubsub.yaml 组件相同的目录中。当 Dapr 启动时,它会与组件一起加载订阅。
HotReload 功能门控启用的。
为了防止重新处理或丢失未处理的消息,在热重载事件期间,Dapr 与您的应用程序之间正在传输的消息不受影响。以下是利用 Dapr SDK 订阅您在 subscription.yaml 中定义的主题的代码示例。
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Mvc;
using Dapr;
using Dapr.Client;
namespace CheckoutService.Controllers;
[ApiController]
public sealed class CheckoutServiceController : ControllerBase
{
//从 "order-pub-sub" 组件订阅名为 "orders" 的主题
[Topic("order-pub-sub", "orders")]
[HttpPost("checkout")]
public void GetCheckout([FromBody] int orderId)
{
Console.WriteLine("Subscriber received : " + orderId);
}
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和订阅者应用程序:
dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 --app-protocol https dotnet run
//dependencies
import io.dapr.Topic;
import io.dapr.client.domain.CloudEvent;
import org.springframework.web.bind.annotation.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
//code
@RestController
public class CheckoutServiceController {
private static final Logger log = LoggerFactory.getLogger(CheckoutServiceController.class);
//订阅一个主题
@Topic(name = "orders", pubsubName = "order-pub-sub")
@PostMapping(path = "/checkout")
public Mono<Void> getCheckout(@RequestBody(required = false) CloudEvent<String> cloudEvent) {
return Mono.fromRunnable(() -> {
try {
log.info("Subscriber received: " + cloudEvent.getData());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和订阅者应用程序:
dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 mvn spring-boot:run
#dependencies
from cloudevents.sdk.event import v1
from dapr.ext.grpc import App
import logging
import json
#code
app = App()
logging.basicConfig(level = logging.INFO)
#订阅一个主题
@app.subscribe(pubsub_name='order-pub-sub', topic='orders')
def mytopic(event: v1.Event) -> None:
data = json.loads(event.Data())
logging.info('Subscriber received: ' + str(data))
app.run(6002)
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和订阅者应用程序:
dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --app-protocol grpc -- python3 CheckoutService.py
//dependencies
import (
"log"
"net/http"
"context"
"github.com/dapr/go-sdk/service/common"
daprd "github.com/dapr/go-sdk/service/http"
)
//code
var sub = &common.Subscription{
PubsubName: "order-pub-sub",
Topic: "orders",
Route: "/checkout",
}
func main() {
s := daprd.NewService(":6002")
//订阅一个主题
if err := s.AddTopicEventHandler(sub, eventHandler); err != nil {
log.Fatalf("error adding topic subscription: %v", err)
}
if err := s.Start(); err != nil && err != http.ErrServerClosed {
log.Fatalf("error listenning: %v", err)
}
}
func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
log.Printf("Subscriber received: %s", e.Data)
return false, nil
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和订阅者应用程序:
dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 go run CheckoutService.go
//dependencies
import { DaprServer, CommunicationProtocolEnum } from '@dapr/dapr';
//code
const daprHost = "127.0.0.1";
const serverHost = "127.0.0.1";
const serverPort = "6002";
start().catch((e) => {
console.error(e);
process.exit(1);
});
async function start(orderId) {
const server = new DaprServer({
serverHost,
serverPort,
communicationProtocol: CommunicationProtocolEnum.HTTP,
clientOptions: {
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
},
});
//订阅一个主题
await server.pubsub.subscribe("order-pub-sub", "orders", async (orderId) => {
console.log(`Subscriber received: ${JSON.stringify(orderId)}`)
});
await server.start();
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和订阅者应用程序:
dapr run --app-id checkout --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 npm start
启动一个 app-id 为 orderprocessing 的 Dapr 实例:
dapr run --app-id orderprocessing --dapr-http-port 3601
然后向 orders 主题发布一条消息:
dapr publish --publish-app-id orderprocessing --pubsub order-pub-sub --topic orders --data '{"orderId": "100"}'
curl -X POST http://localhost:3601/v1.0/publish/order-pub-sub/orders -H "Content-Type: application/json" -d '{"orderId": "100"}'
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{"orderId": "100"}' -Uri 'http://localhost:3601/v1.0/publish/order-pub-sub/orders'
以下是利用 Dapr SDK 发布主题的代码示例。
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Dapr.Client;
using System.Threading;
const string PUBSUB_NAME = "order-pub-sub";
const string TOPIC_NAME = "orders";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
var random = new Random();
var client = app.Services.GetRequiredService<DaprClient>();
while(true) {
await Task.Delay(TimeSpan.FromSeconds(5));
var orderId = random.Next(1,1000);
var source = new CancellationTokenSource();
var cancellationToken = source.Token;
//使用 Dapr SDK 发布主题
await client.PublishEventAsync(PUBSUB_NAME, TOPIC_NAME, orderId, cancellationToken);
Console.WriteLine("Published data: " + orderId);
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和发布者应用程序:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 --app-protocol https dotnet run
//dependencies
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.Metadata;
import static java.util.Collections.singletonMap;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//code
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
public static void main(String[] args) throws InterruptedException{
String MESSAGE_TTL_IN_SECONDS = "1000";
String TOPIC_NAME = "orders";
String PUBSUB_NAME = "order-pub-sub";
while(true) {
TimeUnit.MILLISECONDS.sleep(5000);
Random random = new Random();
int orderId = random.nextInt(1000-1) + 1;
DaprClient client = new DaprClientBuilder().build();
//使用 Dapr SDK 发布主题
client.publishEvent(
PUBSUB_NAME,
TOPIC_NAME,
orderId,
singletonMap(Metadata.TTL_IN_SECONDS, MESSAGE_TTL_IN_SECONDS)).block();
log.info("Published data:" + orderId);
}
}
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和发布者应用程序:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
#dependencies
import random
from time import sleep
import requests
import logging
import json
from dapr.clients import DaprClient
#code
logging.basicConfig(level = logging.INFO)
while True:
sleep(random.randrange(50, 5000) / 1000)
orderId = random.randint(1, 1000)
PUBSUB_NAME = 'order-pub-sub'
TOPIC_NAME = 'orders'
with DaprClient() as client:
#使用 Dapr SDK 发布主题
result = client.publish_event(
pubsub_name=PUBSUB_NAME,
topic_name=TOPIC_NAME,
data=json.dumps(orderId),
data_content_type='application/json',
)
logging.info('Published data: ' + str(orderId))
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和发布者应用程序:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --app-protocol grpc python3 OrderProcessingService.py
//dependencies
import (
"context"
"log"
"math/rand"
"time"
"strconv"
dapr "github.com/dapr/go-sdk/client"
)
//code
var (
PUBSUB_NAME = "order-pub-sub"
TOPIC_NAME = "orders"
)
func main() {
for i := 0; i < 10; i++ {
time.Sleep(5000)
orderId := rand.Intn(1000-1) + 1
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
//使用 Dapr SDK 发布主题
if err := client.PublishEvent(ctx, PUBSUB_NAME, TOPIC_NAME, []byte(strconv.Itoa(orderId)));
err != nil {
panic(err)
}
log.Println("Published data: " + strconv.Itoa(orderId))
}
}
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和发布者应用程序:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run OrderProcessingService.go
//dependencies
import { DaprServer, DaprClient, CommunicationProtocolEnum } from '@dapr/dapr';
const daprHost = "127.0.0.1";
var main = function() {
for(var i=0;i<10;i++) {
sleep(5000);
var orderId = Math.floor(Math.random() * (1000 - 1) + 1);
start(orderId).catch((e) => {
console.error(e);
process.exit(1);
});
}
}
async function start(orderId) {
const PUBSUB_NAME = "order-pub-sub"
const TOPIC_NAME = "orders"
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP
});
console.log("Published data:" + orderId)
//使用 Dapr SDK 发布主题
await client.pubsub.publish(PUBSUB_NAME, TOPIC_NAME, orderId);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
main();
导航到包含上述代码的目录,然后运行以下命令来启动 Dapr 边车和发布者应用程序:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 npm start
为了告诉 Dapr 消息已成功处理,需要返回 200 OK 响应。如果 Dapr 收到除 200 以外的任何返回状态码,或者您的应用程序崩溃,Dapr 将按照至少一次语义尝试重新传递消息。
观看此演示视频以了解更多关于使用 Dapr 进行发布订阅消息传递的信息。
为启用消息路由并为每条消息提供额外的上下文,Dapr 使用 CloudEvents 1.0 规范 作为其消息格式。应用程序使用 Dapr 发送到主题的任何消息都会自动包装在 CloudEvents 信封中,并使用 Content-Type 头部的值 作为 datacontenttype 属性。
Dapr 使用 CloudEvents 为事件负载提供额外的上下文,从而启用以下功能:
你可以通过发布订阅选择三种方法之一来发布 CloudEvent:
向 Dapr 发送发布操作会自动将其包装在包含以下字段的 CloudEvent 信封中:
idsourcespecversiontypetraceparenttraceidtracestatetopicpubsubnametimedatacontenttype(可选)以下示例演示了 Dapr 为发布到 orders 主题的操作生成的 CloudEvent,其中包括:
traceid,对每条消息唯一data 和 CloudEvent 的字段,其中数据内容被序列化为 JSON{
"topic": "orders",
"pubsubname": "order_pub_sub",
"traceid": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01",
"tracestate": "",
"data": {
"orderId": 1
},
"id": "5929aaac-a5e2-4ca1-859c-edfe73f11565",
"specversion": "1.0",
"datacontenttype": "application/json; charset=utf-8",
"source": "checkout",
"type": "com.dapr.event.sent",
"time": "2020-09-23T06:23:21Z",
"traceparent": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01"
}
作为 v1.0 CloudEvent 的另一个示例,以下显示数据作为 XML 内容在序列化为 JSON 的 CloudEvent 消息中:
{
"topic": "orders",
"pubsubname": "order_pub_sub",
"traceid": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01",
"tracestate": "",
"data" : "<note><to></to><from>user2</from><message>Order</message></note>",
"id" : "id-1234-5678-9101",
"specversion" : "1.0",
"datacontenttype" : "text/xml",
"subject" : "Test XML Message",
"source" : "https://example.com/message",
"type" : "xml.message",
"time" : "2020-09-23T06:23:21Z"
}
Dapr 自动生成多个 CloudEvent 属性。你可以通过提供以下可选元数据键/值来替换这些生成的 CloudEvent 属性:
cloudevent.id:覆盖 idcloudevent.source:覆盖 sourcecloudevent.type:覆盖 typecloudevent.traceid:覆盖 traceidcloudevent.tracestate:覆盖 tracestatecloudevent.traceparent:覆盖 traceparent使用这些元数据属性替换 CloudEvents 属性的能力适用于所有发布订阅组件。
例如,要在代码中替换上述 CloudEvent 示例中的 source 和 id 值:
with DaprClient() as client:
order = {'orderId': i}
# Publish an event/message using Dapr PubSub
result = client.publish_event(
pubsub_name='order_pub_sub',
topic_name='orders',
publish_metadata={'cloudevent.id': 'd99b228f-6c73-4e78-8c4d-3f80a043d317', 'cloudevent.source': 'payment'}
)
# or
cloud_event = {
'specversion': '1.0',
'type': 'com.example.event',
'source': 'payment',
'id': 'd99b228f-6c73-4e78-8c4d-3f80a043d317',
'data': {'orderId': i},
'datacontenttype': 'application/json',
...
}
# Set the data content type to 'application/cloudevents+json'
result = client.publish_event(
pubsub_name='order_pub_sub',
topic_name='orders',
data=json.dumps(cloud_event),
data_content_type='application/cloudevents+json',
)
var order = new Order(i);
using var client = new DaprClientBuilder().Build();
// Override cloudevent metadata
var metadata = new Dictionary<string,string>() {
{ "cloudevent.source", "payment" },
{ "cloudevent.id", "d99b228f-6c73-4e78-8c4d-3f80a043d317" }
}
// Publish an event/message using Dapr PubSub
await client.PublishEventAsync("order_pub_sub", "orders", order, metadata);
Console.WriteLine("Published data: " + order);
await Task.Delay(TimeSpan.FromSeconds(1));
JSON 负载随后会反映新的 source 和 id 值:
{
"topic": "orders",
"pubsubname": "order_pub_sub",
"traceid": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01",
"tracestate": "",
"data": {
"orderId": 1
},
"id": "d99b228f-6c73-4e78-8c4d-3f80a043d317",
"specversion": "1.0",
"datacontenttype": "application/json; charset=utf-8",
"source": "payment",
"type": "com.dapr.event.sent",
"time": "2020-09-23T06:23:21Z",
"traceparent": "00-113ad9c4e42b27583ae98ba698d54255-e3743e35ff56f219-01"
}
traceid/traceparent 和 tracestate,但这样做可能会干扰事件追踪,并在追踪工具中报告不一致的结果。建议使用 Open Telemetry 进行分布式追踪。了解更多关于分布式追踪的信息。如果你想使用自己的 CloudEvent,请确保将 datacontenttype 指定为 application/cloudevents+json。
如果应用程序编写的 CloudEvent 不包含 CloudEvent 规范中的最小必填字段,则消息会被拒绝。如果以下字段缺失,Dapr 会将它们添加到 CloudEvent 中:
timetraceidtraceparenttracestatetopicpubsubnamesourcetypespecversion你可以在自定义 CloudEvent 中添加不属于官方 CloudEvent 规范的额外字段。Dapr 会原样传递这些字段。
向 orders 主题发布一个 CloudEvent:
dapr publish --publish-app-id orderprocessing --pubsub order-pub-sub --topic orders --data '{\"orderId\": \"100\"}'
向 orders 主题发布一个 CloudEvent:
curl -X POST http://localhost:3601/v1.0/publish/order-pub-sub/orders -H "Content-Type: application/cloudevents+json" -d '{"specversion" : "1.0", "type" : "com.dapr.cloudevent.sent", "source" : "testcloudeventspubsub", "subject" : "Cloud Events Test", "id" : "someCloudEventId", "time" : "2021-08-02T09:00:00Z", "datacontenttype" : "application/cloudevents+json", "data" : {"orderId": "100"}}'
向 orders 主题发布一个 CloudEvent:
Invoke-RestMethod -Method Post -ContentType 'application/cloudevents+json' -Body '{"specversion" : "1.0", "type" : "com.dapr.cloudevent.sent", "source" : "testcloudeventspubsub", "subject" : "Cloud Events Test", "id" : "someCloudEventId", "time" : "2021-08-02T09:00:00Z", "datacontenttype" : "application/cloudevents+json", "data" : {"orderId": "100"}}' -Uri 'http://localhost:3601/v1.0/publish/order-pub-sub/orders'
在二进制模式下,传输负载仅包含事件主体,而 CloudEvent 属性通过以 ce_ 前缀开头的传输元数据提供(HTTP 头、Kafka 头、NATS 头等)。这在你已经生成二进制模式事件或想要发送任意二进制数据而不将其包装在额外的 JSON 信封中时非常有用。
要将二进制 CloudEvent 发布到 Dapr(通过 HTTP/gRPC 发布 API 或直接发布到 Dapr 读取的代理):
将传输的原生 content-type 元数据(例如 HTTP Content-Type 头或 Kafka content-type 消息头)设置为表示二进制数据的 MIME 类型,即 application/octet-stream。
将必填的 CloudEvent 属性(ce_specversion、ce_type、ce_source、ce_id)添加为传输元数据。可选属性如 ce_subject、ce_time 或 ce_traceparent 也会被识别。
在消息主体中发送负载字节。
向 orders 主题发布二进制 CloudEvent:
curl -X POST http://localhost:3500/v1.0/publish/order-pub-sub/orders \
-H "Content-Type: application/octet-stream" \
-H "ce_specversion: 1.0" \
-H "ce_type: com.example.order.created" \
-H "ce_source: urn:example:/checkout" \
-H "ce_id: 2a8bbf52-1222-4c2c-85f0-8a8875c7bc10" \
-H "ce_subject: orders/100" \
--data-binary $'\x01\x02\x03\x04'
使用 Dapr 创建的 cloud events 时,信封包含一个 id 字段,应用程序可以将其用于消息去重。Dapr 不会自动处理去重。Dapr 支持使用原生支持消息去重的消息代理。
在向应用程序添加 Dapr 时,某些服务可能仍需要通过未封装在 CloudEvents 中的发布/订阅消息进行通信,原因可能是兼容性要求,或某些应用程序未使用 Dapr。这些被称为"原始"发布/订阅消息。Dapr 使应用程序能够发布和订阅原始事件,这些事件未包装在 CloudEvent 中,以实现兼容性并发送不可 JSON 序列化的数据。
Dapr 应用程序能够向发布/订阅主题发布不包含 CloudEvent 封装的原始事件,以实现与非 Dapr 应用程序的兼容。

要禁用 CloudEvent 包装,请在发布请求中设置 rawPayload 元数据为 true。这允许订阅者接收这些消息而无需解析 CloudEvent schema。
curl -X "POST" http://localhost:3500/v1.0/publish/pubsub/TOPIC_A?metadata.rawPayload=true -H "Content-Type: application/json" -d '{"order-number": "345"}'
using Dapr.Client;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers().AddDapr();
var app = builder.Build();
app.MapPost("/publish", async (DaprClient daprClient) =>
{
var message = new Message(
Guid.NewGuid().ToString(),
$"Hello at {DateTime.UtcNow}",
DateTime.UtcNow
);
await daprClient.PublishEventAsync(
"pubsub", // pubsub name
"messages", // topic name
message, // message data
new Dictionary<string, string>
{
{ "rawPayload", "true" },
{ "content-type", "application/json" }
}
);
return Results.Ok(message);
});
app.Run();
from dapr.clients import DaprClient
with DaprClient() as d:
req_data = {
'order-number': '345'
}
# Create a typed message with content type and body
resp = d.publish_event(
pubsub_name='pubsub',
topic_name='TOPIC_A',
data=json.dumps(req_data),
publish_metadata={'rawPayload': 'true'}
)
# Print the request
print(req_data, flush=True)
<?php
require_once __DIR__.'/vendor/autoload.php';
$app = \Dapr\App::create();
$app->run(function(\DI\FactoryInterface $factory) {
$publisher = $factory->make(\Dapr\PubSub\Publish::class, ['pubsub' => 'pubsub']);
$publisher->topic('TOPIC_A')->publish('data', ['rawPayload' => 'true']);
});
@RestController
@PathMapping("/publish")
public class PublishController {
@Inject
DaprClient client;
@PostMapping
public void sendRawMessage() {
Map<String, String> metadata = new HashMap<>();
metatada.put("content-type", "application/json");
metadata.put("rawPayload", "true");
Message message = new Message(UUID.random().toString(), "Hello from Dapr");
client.publishEvent(
"pubsub", // pubsub name
"messages", // topic name
message, // message data
metadata) // metadata
.block(); // wait for completion
}
}
Dapr 应用程序可以订阅来自发布/订阅主题的原始消息,即使这些消息不是作为 CloudEvents 发布的。然而,订阅的 Dapr 进程仍会在将这些原始消息传递给订阅应用程序之前将其包装在 CloudEvent 中。

以编程方式订阅时,添加 rawPayload 的额外元数据条目以允许订阅者接收未由 CloudEvent 包装的消息。对于 .NET,此元数据条目称为 isRawPayload。
使用原始负载时,消息始终使用 base64 编码,内容类型为 application/octet-stream。
using System.Text.Json;
using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/dapr/subscribe", () =>
{
var subscriptions = new[]
{
new
{
pubsubname = "pubsub",
topic = "messages",
route = "/messages",
metadata = new Dictionary<string, string>
{
{ "rawPayload", "true" },
{ "content-type", "application/json" }
}
}
};
return Results.Ok(subscriptions);
});
app.MapPost("/messages", async (HttpContext context) =>
{
using var reader = new StreamReader(context.Request.Body);
var json = await reader.ReadToEndAsync();
Console.WriteLine($"Raw message received: {json}");
return Results.Ok();
});
app.Run();
import flask
from flask import request, jsonify
from flask_cors import CORS
import json
import sys
app = flask.Flask(__name__)
CORS(app)
@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
subscriptions = [{'pubsubname': 'pubsub',
'topic': 'deathStarStatus',
'route': 'dsstatus',
'metadata': {
'rawPayload': 'true',
} }]
return jsonify(subscriptions)
@app.route('/dsstatus', methods=['POST'])
def ds_subscriber():
print(request.json, flush=True)
return json.dumps({'success':True}), 200, {'ContentType':'application/json'}
app.run()
<?php
require_once __DIR__.'/vendor/autoload.php';
$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions(['dapr.subscriptions' => [
new \Dapr\PubSub\Subscription(pubsubname: 'pubsub', topic: 'deathStarStatus', route: '/dsstatus', metadata: [ 'rawPayload' => 'true'] ),
]]));
$app->post('/dsstatus', function(
#[\Dapr\Attributes\FromBody]
\Dapr\PubSub\CloudEvent $cloudEvent,
\Psr\Log\LoggerInterface $logger
) {
$logger->alert('Received event: {event}', ['event' => $cloudEvent]);
return ['status' => 'SUCCESS'];
}
);
$app->start();
@RequestMapping("/consumer")
@RestController
public class MessageConsumerController {
@PostMapping
@ResponseStatus(HttpStatus.OK)
@Topic(pubsubName = "pubsub", name = "messages", metadata = "{\"rawPayload\":\"true\", \"content-type\": \"application/json\"}")
public void consume(@RequestBody Message message) {
System.out.println("Message received: " + message);
}
@PostMapping
@ResponseStatus(HttpStatus.OK)
@Topic(pubsubName = "pubsub", name = "another-topic", metadata = """
{"rawPayload": "true", "content-type": "application/json"}
""") // Using Java 15 text block
public void consumeAnother(@RequestBody Message message) {
System.out.println("Message received: " + message);
}
}
同样,您可以通过在订阅规范中添加 rawPayload 元数据条目来以声明方式订阅原始事件。
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: myevent-subscription
spec:
topic: deathStarStatus
routes:
default: /dsstatus
pubsubname: pubsub
metadata:
isRawPayload: "true"
scopes:
- app1
- app2
发布订阅路由是基于内容的路由的一种实现,这是一种使用 DSL 而非命令式应用程序代码的消息传递模式。通过发布订阅路由,你可以使用表达式根据内容将 CloudEvents 路由到应用程序中的不同 URI/路径和事件处理程序。如果没有路由匹配,则使用可选的默认路由。当应用程序扩展以支持多个事件版本或特殊情况时,这非常有用。
虽然路由可以通过代码实现,但将路由规则保持在应用程序外部可以提高可移植性。
此功能适用于声明式和编程式订阅方法,但不适用于流式订阅。
对于声明式订阅,使用 dapr.io/v2alpha1 作为 apiVersion。以下是使用路由的 subscriptions.yaml 示例:
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: myevent-subscription
spec:
pubsubname: pubsub
topic: inventory
routes:
rules:
- match: event.type == "widget"
path: /widgets
- match: event.type == "gadget"
path: /gadgets
default: /products
scopes:
- app1
- app2
在编程式方法中,返回 routes 结构而不是 route。JSON 结构与声明式 YAML 匹配:
import flask
from flask import request, jsonify
from flask_cors import CORS
import json
import sys
app = flask.Flask(__name__)
CORS(app)
@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
subscriptions = [
{
'pubsubname': 'pubsub',
'topic': 'inventory',
'routes': {
'rules': [
{
'match': 'event.type == "widget"',
'path': '/widgets'
},
{
'match': 'event.type == "gadget"',
'path': '/gadgets'
},
],
'default': '/products'
}
}]
return jsonify(subscriptions)
@app.route('/products', methods=['POST'])
def ds_subscriber():
print(request.json, flush=True)
return json.dumps({'success':True}), 200, {'ContentType':'application/json'}
app.run()
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json({ type: 'application/*+json' }));
const port = 3000
app.get('/dapr/subscribe', (req, res) => {
res.json([
{
pubsubname: "pubsub",
topic: "inventory",
routes: {
rules: [
{
match: 'event.type == "widget"',
path: '/widgets'
},
{
match: 'event.type == "gadget"',
path: '/gadgets'
},
],
default: '/products'
}
}
]);
})
app.post('/products', (req, res) => {
console.log(req.body);
res.sendStatus(200);
});
app.listen(port, () => console.log(`consumer app listening on port ${port}!`))
[Topic("pubsub", "inventory", "event.type ==\"widget\"", 1)]
[HttpPost("widgets")]
public async Task<ActionResult<Stock>> HandleWidget(Widget widget, [FromServices] DaprClient daprClient)
{
// Logic
return stock;
}
[Topic("pubsub", "inventory", "event.type ==\"gadget\"", 2)]
[HttpPost("gadgets")]
public async Task<ActionResult<Stock>> HandleGadget(Gadget gadget, [FromServices] DaprClient daprClient)
{
// Logic
return stock;
}
[Topic("pubsub", "inventory")]
[HttpPost("products")]
public async Task<ActionResult<Stock>> HandleProduct(Product product, [FromServices] DaprClient daprClient)
{
// Logic
return stock;
}
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
const appPort = 3000
type subscription struct {
PubsubName string `json:"pubsubname"`
Topic string `json:"topic"`
Metadata map[string]string `json:"metadata,omitempty"`
Routes routes `json:"routes"`
}
type routes struct {
Rules []rule `json:"rules,omitempty"`
Default string `json:"default,omitempty"`
}
type rule struct {
Match string `json:"match"`
Path string `json:"path"`
}
// This handles /dapr/subscribe
func configureSubscribeHandler(w http.ResponseWriter, _ *http.Request) {
t := []subscription{
{
PubsubName: "pubsub",
Topic: "inventory",
Routes: routes{
Rules: []rule{
{
Match: `event.type == "widget"`,
Path: "/widgets",
},
{
Match: `event.type == "gadget"`,
Path: "/gadgets",
},
},
Default: "/products",
},
},
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(t)
}
func main() {
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/dapr/subscribe", configureSubscribeHandler).Methods("GET")
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", appPort), router))
}
<?php
require_once __DIR__.'/vendor/autoload.php';
$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions(['dapr.subscriptions' => [
new \Dapr\PubSub\Subscription(pubsubname: 'pubsub', topic: 'inventory', routes: (
rules: => [
('match': 'event.type == "widget"', path: '/widgets'),
('match': 'event.type == "gadget"', path: '/gadgets'),
]
default: '/products')),
]]));
$app->post('/products', function(
#[\Dapr\Attributes\FromBody]
\Dapr\PubSub\CloudEvent $cloudEvent,
\Psr\Log\LoggerInterface $logger
) {
$logger->alert('Received event: {event}', ['event' => $cloudEvent]);
return ['status' => 'SUCCESS'];
}
);
$app->start();
在这些示例中,根据 event.type 的不同,应用程序将在以下路径被调用:
/widgets/gadgets/products表达式以通用表达式语言 (CEL)编写,其中 event 表示云事件。可以引用 CloudEvents 核心规范中的任何属性。
匹配"重要"消息:
has(event.data.important) && event.data.important == true
匹配大于 $10,000 的存款:
event.type == "deposit" && int(event.data.amount) > 10000
匹配消息的多个版本:
event.type == "mymessage.v1"
event.type == "mymessage.v2"
作为参考,以下属性来自 CloudEvents 规范。
根据术语 data 的定义,CloudEvents _可能_包含有关发生的领域特定信息。如果存在,此信息将封装在 data 中。
datacontenttype 属性指定的媒体格式(例如 application/json),并在存在相应属性时遵循 dataschema 格式。以下属性在所有 CloudEvents 中是必需的:
Stringsource + id 对于每个不同的事件都是唯一的。如果重复事件被重新发送(例如由于网络错误),它可能具有相同的 id。消费者可以假定具有相同 source 和 id 的事件是重复的。类型: URI-reference
描述: 标识事件发生的上下文。通常这包括以下信息:
URI 中编码的数据的确切语法和语义由事件生产者定义。
生产者_必须_确保 source + id 对于每个不同的事件都是唯一的。
应用程序可以:
source,从而更容易生成唯一 ID 并防止其他生产者具有相同的 source。source 标识符。一个源可能包含多个生产者。在这种情况下,生产者_必须_协作以确保 source + id 对于每个不同的事件都是唯一的。
约束:
示例:
类型: String
描述: 事件使用的 CloudEvents 规范版本。这能够解释上下文。合规的事件生产者在引用此版本的规范时_必须_使用值 1.0。
目前,此属性仅包含"主"和"次"版本号。这允许对规范进行补丁更改而不更改序列化中此属性的值。
注意:对于"候选版本"版本,可能会使用后缀进行测试目的。
约束:
Stringtype 的版本等信息。有关更多信息,请参阅 Primer 中的 CloudEvents 版本控制。以下属性在 CloudEvents 中是可选的。有关 OPTIONAL 定义的更多信息,请参阅符号约定部分。
类型: String,根据 RFC 2046
描述: data 值的内容类型。此属性使 data 能够承载任何类型的内容,其中格式和编码可能与所选事件格式的不同。
例如,使用 JSON 信封格式渲染的事件可能在 data 中承载 XML 负载。通过将此属性设置为 "application/xml" 来通知消费者。
对于不同 datacontenttype 值如何渲染 data 内容的规则在事件格式规范中定义。例如,JSON 事件格式在第 3.1 节中定义了关系。
对于某些二进制模式协议绑定,此字段直接映射到相应协议的 content-type 元数据属性。你可以在相应协议中找到二进制模式和 content-type 元数据映射的规范规则。
在某些事件格式中,你可以省略 datacontenttype 属性。例如,如果 JSON 格式事件没有 datacontenttype 属性,则暗示 data 是符合 "application/json" 媒体类型的 JSON 值。换句话说:没有 datacontenttype 的 JSON 格式事件与具有 datacontenttype="application/json" 的事件完全等效。
将没有 datacontenttype 属性的事件消息转换为不同格式或协议绑定时,应将目标 datacontenttype 显式设置为源的隐式 datacontenttype。
约束:
有关媒体类型示例,请参阅 IANA 媒体类型
URIdata 遵循的模式。对模式的不兼容更改应通过不同的 URI 反映。有关更多信息,请参阅 Primer 中的 CloudEvents 版本控制。类型: String
描述: 这在事件生产者(由 source 标识)的上下文中描述事件主题。在发布-订阅场景中,订阅者通常会订阅由 source 发出的事件。如果 source 上下文具有内部子结构,则单独的 source 标识符可能不足以作为任何特定事件的限定符。
在上下文元数据中标识事件主题(而不是仅在 data 负载中)对于通用订阅过滤场景很有帮助,在这些场景中,中间件无法解释 data 内容。在上述示例中,订阅者可能只对名称以 ‘.jpg’ 或 ‘.jpeg’ 结尾的 blob 感兴趣。使用 subject 属性,你可以为该事件子集构建简单而高效的字符串后缀过滤器。
约束:
示例:
订阅者可能注册对新 blob 在 blob 存储容器中创建时的兴趣。在这种情况下:
source 标识订阅范围(存储容器)type 标识"blob 已创建"事件id 唯一标识事件实例,以区分同名的 blob 的单独创建事件。新创建的 blob 的名称承载在 subject 中:
source: https://example.com/storage/tenant/containersubject: mynewfile.jpgTimestampsource 的所有生产者在这方面_必须_保持一致。换句话说,它们要么都使用事件的实际时间,要么都使用相同的算法来确定使用的值。观看此视频,了解如何使用发布订阅进行消息路由:
Dapr 应用程序可以通过三种订阅类型订阅已发布的主题,这些类型支持相同的功能:声明式、流式和编程式。
| 订阅类型 | 描述 |
|---|---|
| 声明式 | 订阅在外部文件中定义。声明式方法从代码中移除了 Dapr 依赖,允许现有应用程序订阅主题,而无需更改代码。 |
| 流式 | 订阅在应用程序代码中定义。流式订阅是动态的,这意味着它们允许在运行时添加或删除订阅。它们不需要在应用程序中设置订阅端点(编程式和声明式订阅都需要),使其易于在代码中配置。流式订阅也不需要将应用程序配置为通过边车来接收消息。 |
| 编程式 | 订阅在应用程序代码中定义。编程式方法实现静态订阅并要求代码中有一个端点。 |
下面的示例演示了 checkout 应用程序和 orderprocessing 应用程序之间通过 orders 主题进行的发布订阅消息传递。这些示例演示了同一个 Dapr 发布订阅组件首先以声明方式使用,然后以编程方式使用。
HotReload 功能门控启用的。
为了防止重新处理或丢失未处理的消息,Dapr 与应用程序之间正在传输的消息在热重载事件期间不受影响。您可以使用外部组件文件以声明方式订阅主题。此示例使用名为 subscription.yaml 的 YAML 组件文件:
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: order
spec:
topic: orders
routes:
default: /orders
pubsubname: pubsub
scopes:
- orderprocessing
这里名为 order 的订阅:
pubsub 的发布订阅组件订阅名为 orders 的主题。route 字段以将所有主题消息发送到应用程序中的 /orders 端点。scopes 字段以将此订阅的范围限制为仅由 ID 为 orderprocessing 的应用程序访问。运行 Dapr 时,设置 YAML 组件文件路径以将 Dapr 指向该组件。
dapr run --app-id myapp --resources-path ./myComponents -- dotnet run
dapr run --app-id myapp --resources-path ./myComponents -- mvn spring-boot:run
dapr run --app-id myapp --resources-path ./myComponents -- python3 app.py
dapr run --app-id myapp --resources-path ./myComponents -- npm start
dapr run --app-id myapp --resources-path ./myComponents -- go run app.go
在 Kubernetes 中,将组件应用到集群:
kubectl apply -f subscription.yaml
在应用程序代码中,订阅 Dapr 发布订阅组件中指定的主题。
//Subscribe to a topic
[HttpPost("orders")]
public void getCheckout([FromBody] int orderId)
{
Console.WriteLine("Subscriber received : " + orderId);
}
import io.dapr.client.domain.CloudEvent;
//Subscribe to a topic
@PostMapping(path = "/orders")
public Mono<Void> getCheckout(@RequestBody(required = false) CloudEvent<String> cloudEvent) {
return Mono.fromRunnable(() -> {
try {
log.info("Subscriber received: " + cloudEvent.getData());
}
});
}
from cloudevents.sdk.event import v1
#Subscribe to a topic
@app.route('/orders', methods=['POST'])
def checkout(event: v1.Event) -> None:
data = json.loads(event.Data())
logging.info('Subscriber received: ' + str(data))
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json({ type: 'application/*+json' }));
// listen to the declarative route
app.post('/orders', (req, res) => {
console.log(req.body);
res.sendStatus(200);
});
//Subscribe to a topic
var sub = &common.Subscription{
PubsubName: "pubsub",
Topic: "orders",
Route: "/orders",
}
func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
log.Printf("Subscriber received: %s", e.Data)
return false, nil
}
/orders 端点与订阅中定义的 route 匹配,这是 Dapr 将所有主题消息发送到的地方。
流式订阅是在应用程序代码中定义的订阅,可以在运行时动态停止和启动。 消息由应用程序从 Dapr 拉取。这意味着不需要端点来订阅主题,并且可以在边车上根本没有配置任何应用程序的情况下进行订阅。 可以同时订阅任意数量的发布订阅和主题。 当消息发送到给定的消息处理程序代码时,没有路由或批量订阅的概念。
下面的示例展示了流式订阅主题的不同方式。
您可以使用 DaprPublishSubscribeClient 上的 SubscribeAsync 方法来配置用于从流中拉取消息的消息处理程序。
using System.Text;
using Dapr.Messaging.PublishSubscribe;
using Dapr.Messaging.PublishSubscribe.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprPubSubClient();
var app = builder.Build();
var messagingClient = app.Services.GetRequiredService<DaprPublishSubscribeClient>();
//Create a dynamic streaming subscription and subscribe with a timeout of 30 seconds and 10 seconds for message handling
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var subscription = await messagingClient.SubscribeAsync("pubsub", "myTopic",
new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(10), TopicResponseAction.Retry)),
HandleMessageAsync, cancellationTokenSource.Token);
await Task.Delay(TimeSpan.FromMinutes(1));
//When you're done with the subscription, simply dispose of it
await subscription.DisposeAsync();
return;
//Process each message returned from the subscription
Task<TopicResponseAction> HandleMessageAsync(TopicMessage message, CancellationToken cancellationToken = default)
{
try
{
//Do something with the message
Console.WriteLine(Encoding.UTF8.GetString(message.Data.Span));
return Task.FromResult(TopicResponseAction.Success);
}
catch
{
return Task.FromResult(TopicResponseAction.Retry);
}
}
您可以使用 subscribe 方法,该方法返回一个 Subscription 对象,并允许您通过调用 next_message 方法从流中拉取消息。这在等待消息时运行并可能阻塞主线程。
import time
from dapr.clients import DaprClient
from dapr.clients.grpc.subscription import StreamInactiveError
counter = 0
def process_message(message):
global counter
counter += 1
# Process the message here
print(f'Processing message: {message.data()} from {message.topic()}...')
return 'success'
def main():
with DaprClient() as client:
global counter
subscription = client.subscribe(
pubsub_name='pubsub', topic='orders', dead_letter_topic='orders_dead'
)
try:
while counter < 5:
try:
message = subscription.next_message()
except StreamInactiveError as e:
print('Stream is inactive. Retrying...')
time.sleep(1)
continue
if message is None:
print('No message received within timeout period.')
continue
# Process the message
response_status = process_message(message)
if response_status == 'success':
subscription.respond_success(message)
elif response_status == 'retry':
subscription.respond_retry(message)
elif response_status == 'drop':
subscription.respond_drop(message)
finally:
print("Closing subscription...")
subscription.close()
if __name__ == '__main__':
main()
您也可以使用 subscribe_with_handler 方法,该方法接受一个回调函数,该函数对从流接收到的每条消息执行。这在单独的线程中运行,因此不会阻塞主线程。
import time
from dapr.clients import DaprClient
from dapr.clients.grpc._response import TopicEventResponse
counter = 0
def process_message(message):
# Process the message here
global counter
counter += 1
print(f'Processing message: {message.data()} from {message.topic()}...')
return TopicEventResponse('success')
def main():
with (DaprClient() as client):
# This will start a new thread that will listen for messages
# and process them in the `process_message` function
close_fn = client.subscribe_with_handler(
pubsub_name='pubsub', topic='orders', handler_fn=process_message,
dead_letter_topic='orders_dead'
)
while counter < 5:
time.sleep(1)
print("Closing subscription...")
close_fn()
if __name__ == '__main__':
main()
package main
import (
"context"
"log"
"github.com/dapr/go-sdk/client"
)
func main() {
cl, err := client.NewClient()
if err != nil {
log.Fatal(err)
}
sub, err := cl.Subscribe(context.Background(), client.SubscriptionOptions{
PubsubName: "pubsub",
Topic: "orders",
})
if err != nil {
panic(err)
}
// Close must always be called.
defer sub.Close()
for {
msg, err := sub.Receive()
if err != nil {
panic(err)
}
// Process the event
// We _MUST_ always signal the result of processing the message, else the
// message will not be considered as processed and will be redelivered or
// dead lettered.
// msg.Retry()
// msg.Drop()
if err := msg.Success(); err != nil {
panic(err)
}
}
}
或
package main
import (
"context"
"log"
"github.com/dapr/go-sdk/client"
"github.com/dapr/go-sdk/service/common"
)
func main() {
cl, err := client.NewClient()
if err != nil {
log.Fatal(err)
}
stop, err := cl.SubscribeWithHandler(context.Background(),
client.SubscriptionOptions{
PubsubName: "pubsub",
Topic: "orders",
},
eventHandler,
)
if err != nil {
panic(err)
}
// Stop must always be called.
defer stop()
<-make(chan struct{})
}
func eventHandler(e *common.TopicEvent) common.SubscriptionResponseStatus {
// Process message here
// common.SubscriptionResponseStatusRetry
// common.SubscriptionResponseStatusDrop
common.SubscriptionResponseStatusDrop, status);
}
return common.SubscriptionResponseStatusSuccess
}
观看此视频了解流式订阅的概述:
与声明式方法的 route YAML 结构不同,动态编程式方法在代码中返回 routes JSON 结构。
注意: 编程式订阅仅在应用程序启动期间读取一次。您不能动态添加新的编程式订阅,只能在编译时添加新的订阅。
/dapr/subscribe 的自动 HTTP 调用以减少日志噪音。在 dapr run 中使用 --disable-init-endpoints subscribe 标志,或在 Kubernetes 中使用 dapr.io/disable-init-endpoints: "subscribe" 注解。了解有关禁用初始化端点的更多信息。在下面的示例中,您在应用程序代码中定义上面声明式 YAML 订阅中找到的值。
[Topic("pubsub", "orders")]
[HttpPost("/orders")]
public async Task<ActionResult<Order>>Checkout(Order order, [FromServices] DaprClient daprClient)
{
// Logic
return order;
}
或
// Dapr subscription in [Topic] routes orders topic to this route
app.MapPost("/orders", [Topic("pubsub", "orders")] (Order order) => {
Console.WriteLine("Subscriber received : " + order);
return Results.Ok(order);
});
上面定义的处理程序还需要映射到 dapr/subscribe 端点。这是在定义端点时的应用程序启动代码中完成的。
app.UseEndpoints(endpoints =>
{
endpoints.MapSubscribeHandler();
});
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Topic(name = "orders", pubsubName = "pubsub")
@PostMapping(path = "/orders")
public Mono<Void> handleMessage(@RequestBody(required = false) CloudEvent<String> cloudEvent) {
return Mono.fromRunnable(() -> {
try {
System.out.println("Subscriber received: " + cloudEvent.getData());
System.out.println("Subscriber received: " + OBJECT_MAPPER.writeValueAsString(cloudEvent));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
subscriptions = [
{
'pubsubname': 'pubsub',
'topic': 'orders',
'routes': {
'rules': [
{
'match': 'event.type == "order"',
'path': '/orders'
},
],
'default': '/orders'
}
}]
return jsonify(subscriptions)
@app.route('/orders', methods=['POST'])
def ds_subscriber():
print(request.json, flush=True)
return json.dumps({'success':True}), 200, {'ContentType':'application/json'}
app.run()
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json({ type: 'application/*+json' }));
const port = 3000
app.get('/dapr/subscribe', (req, res) => {
res.json([
{
pubsubname: "pubsub",
topic: "orders",
routes: {
rules: [
{
match: 'event.type == "order"',
path: '/orders'
},
],
default: '/products'
}
}
]);
})
app.post('/orders', (req, res) => {
console.log(req.body);
res.sendStatus(200);
});
app.listen(port, () => console.log(`consumer app listening on port ${port}!`))
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
const appPort = 3000
type subscription struct {
PubsubName string `json:"pubsubname"`
Topic string `json:"topic"`
Metadata map[string]string `json:"metadata,omitempty"`
Routes routes `json:"routes"`
}
type routes struct {
Rules []rule `json:"rules,omitempty"`
Default string `json:"default,omitempty"`
}
type rule struct {
Match string `json:"match"`
Path string `json:"path"`
}
// This handles /dapr/subscribe
func configureSubscribeHandler(w http.ResponseWriter, _ *http.Request) {
t := []subscription{
{
PubsubName: "pubsub",
Topic: "orders",
Routes: routes{
Rules: []rule{
{
Match: `event.type == "order"`,
Path: "/orders",
},
},
Default: "/orders",
},
},
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(t)
}
func main() {
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/dapr/subscribe", configureSubscribeHandler).Methods("GET")
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", appPort), router))
}
应用程序有时可能因各种原因无法处理消息。例如,在检索处理消息所需的数据时可能存在瞬时问题,或者应用程序业务逻辑失败并返回错误。死信主题用于转发无法投递到订阅应用程序的消息。这减轻了应用程序的压力,使其无需处理这些失败的消息,允许开发者编写代码从死信主题读取消息,然后修复消息并重新发送,或完全放弃该消息。
死信主题通常与重试弹性策略以及死信订阅一起使用,死信订阅负责处理从死信主题转发来的消息所需的逻辑。
当设置了死信主题时,任何未能投递到已配置主题的应用程序的消息都会被放到死信主题上,以便转发给处理这些消息的订阅。这可能是同一个应用程序,也可能是完全不同的应用程序。
Dapr 为其所有发布/订阅组件启用死信主题,即使底层系统本身不支持此功能。例如,AWS SNS 组件 具有死信队列,RabbitMQ 具有死信主题。你需要确保正确配置此类组件。
下图是死信主题如何工作的示例。首先,从 orders 主题上的发布者发送一条消息。Dapr 代表订阅应用程序接收该消息,但是 orders 主题消息未能投递到应用程序上的 /checkout 端点,即使经过重试也是如此。由于投递失败,该消息被转发到 poisonMessages 主题,该主题将其传递到 /failedMessages 端点进行处理,在本例中是在同一应用程序上。failedMessages 处理代码可以丢弃消息或重新发送新消息。

以下 YAML 显示如何为从 orders 主题消费的消息配置名为 poisonMessages 的死信主题的订阅。此订阅的作用域限定为具有 checkout ID 的应用程序。
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: order
spec:
topic: orders
routes:
default: /checkout
pubsubname: pubsub
deadLetterTopic: poisonMessages
scopes:
- checkout
var deadLetterTopic = "poisonMessages"
sub, err := cl.Subscribe(context.Background(), client.SubscriptionOptions{
PubsubName: "pubsub",
Topic: "orders",
DeadLetterTopic: &deadLetterTopic,
})
从 /subscribe 端点返回的 JSON 显示如何为从 orders 主题消费的消息配置名为 poisonMessages 的死信主题。
app.get('/dapr/subscribe', (_req, res) => {
res.json([
{
pubsubname: "pubsub",
topic: "orders",
route: "/checkout",
deadLetterTopic: "poisonMessages"
}
]);
});
默认情况下,当设置了死信主题时,任何失败的消息都会立即进入死信主题。因此,建议在订阅中使用死信主题时始终设置重试策略。要在将消息发送到死信主题之前启用重试,请将重试弹性策略 应用于发布/订阅组件。
此示例显示如何为 pubsub 发布/订阅组件设置名为 pubsubRetry 的恒定重试策略,最大投递尝试次数为 10 次,每 5 秒应用一次。
apiVersion: dapr.io/v1alpha1
kind: Resiliency
metadata:
name: myresiliency
spec:
policies:
retries:
pubsubRetry:
policy: constant
duration: 5s
maxRetries: 10
targets:
components:
pubsub:
inbound:
retry: pubsubRetry
请记住,现在要配置一个订阅来处理死信主题。例如,你可以创建另一个声明式订阅,以在同一或不同的应用程序上接收这些消息。下面的示例显示 checkout 应用程序订阅 poisonMessages 主题,并通过另一个订阅将这些消息发送到 /failedmessages 端点进行处理。
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: deadlettertopics
spec:
topic: poisonMessages
routes:
rules:
- match:
path: /failedMessages
pubsubname: pubsub
scopes:
- checkout
你已经设置了 Dapr 的发布订阅 API 构建块,你的应用程序正在使用中心化的消息代理顺畅地发布和订阅主题。如果你想为应用程序执行简单的 A/B 测试、蓝绿部署,甚至金丝雀部署,该怎么办呢?即使使用 Dapr,这也可能变得很困难。
Dapr 通过其发布订阅命名空间消费者组构造解决了大规模的多租户问题。
假设你有一个 Kubernetes 集群,两个应用程序(App1 和 App2)部署在同一个命名空间(namespace-a)中。App2 发布到名为 order 的主题,而 App1 订阅名为 order 的主题。这将创建两个消费者组,以你的应用程序名称命名(App1 和 App2)。

为了在使用中心化消息代理的同时执行简单的测试和部署,你创建了另一个命名空间,其中包含两个具有相同 app-id 的应用程序 App1 和 App2。
Dapr 使用单个应用程序的 app-id 创建消费者组,因此消费者组名称将保持为 App1 和 App2。

为了避免这种情况,你需要根据运行的命名空间,在代码中添加一些内容来更改 app-id。这种变通方法很繁琐,是一个显著的痛点。
Dapr 不仅允许你通过 UUID 和 Pod 名称的 consumerID 更改消费者组的行为,还提供了一个存在于发布/订阅组件元数据中的命名空间构造。例如,使用 Redis 作为消息代理:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: consumerID
value: "{namespace}"
通过将 consumerID 配置为 {namespace} 值,你将能够从不同的命名空间使用相同的 app-id 和相同的主题。

在上图中,你有两个命名空间,每个命名空间都有具有相同 app-id 的应用程序,发布和订阅到同一个中心化消息代理 orders。然而这一次,Dapr 创建了以其运行的命名空间为前缀的消费者组名称。
无需更改代码/app-id,命名空间消费者组允许你:
app-id只需在组件元数据中包含 "{namespace}" 消费者组构造即可。你不需要在元数据中编码命名空间。Dapr 理解它运行的命名空间,并为你完成命名空间值,就像运行时注入的动态元数据值一样。
与 Pod 为临时性的 Deployments 不同,StatefulSets 允许在 Kubernetes 上部署有状态应用程序,同时为每个 Pod 保持一个稳定的标识。
以下是使用 Dapr 的 StatefulSet 示例:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: python-subscriber
spec:
selector:
matchLabels:
app: python-subscriber # has to match .spec.template.metadata.labels
serviceName: "python-subscriber"
replicas: 3
template:
metadata:
labels:
app: python-subscriber # has to match .spec.selector.matchLabels
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "python-subscriber"
dapr.io/app-port: "5001"
spec:
containers:
- name: python-subscriber
image: ghcr.io/dapr/samples/pubsub-python-subscriber:latest
ports:
- containerPort: 5001
imagePullPolicy: Always
通过 Dapr 订阅发布订阅主题时,应用程序可以定义 consumerID,该 ID 决定了订阅者在队列或主题中的位置。借助 StatefulSets 的 Pod 稳定标识特性,每个 Pod 可以拥有唯一的 consumerID,从而实现订阅者应用程序的每个水平扩展。Dapr 会跟踪每个 Pod 的名称,该名称可在声明组件时使用 {podName} 标记。
在扩展给定主题的订阅者数量时,每个 Dapr 组件都有独特的设置来决定其行为。通常,多个消费者有两种选择:
Kafka 通过 consumerID 隔离每个订阅者,并在主题中维护各自的位置。当实例重启时,它会重用相同的 consumerID 并从其最后已知位置继续,不会跳过消息。下面的组件演示了 Kafka 组件如何被多个 Pod 使用:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.kafka
version: v1
metadata:
- name: brokers
value: my-cluster-kafka-bootstrap.kafka.svc.cluster.local:9092
- name: consumerID
value: "{podName}"
- name: authRequired
value: "false"
MQTT3 协议具有共享主题功能,允许多个订阅者"竞争"主题中的消息,即一条消息仅由其中一个处理。例如:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mqtt-pubsub
spec:
type: pubsub.mqtt3
version: v1
metadata:
- name: consumerID
value: "{podName}"
- name: cleanSession
value: "true"
- name: url
value: "tcp://admin:public@localhost:1883"
- name: qos
value: 1
- name: retain
value: "false"
命名空间或组件范围 可用于将组件访问限制到特定应用程序。添加到组件的这些应用程序范围仅允许具有特定 ID 的应用程序使用该组件。
除了此常规组件范围外,发布订阅组件还可以限制以下内容:
这称为发布订阅主题范围。
发布订阅范围是为每个发布订阅组件定义的。您可能有一个名为 pubsub 的发布订阅组件,它具有一组范围,而另一个 pubsub2 具有不同的范围。
要使用此主题范围,可以为发布订阅组件设置三个元数据属性:
spec.metadata.publishingScopespublishingScopes 中未指定任何内容(默认行为),则所有应用程序都可以向所有主题发布app1=;app2=topic2)app1=topic1;app2=topic2,topic3;app3= 将允许 app1 向 topic1 发布,而不允许向其他主题发布;app2 仅向 topic2 和 topic3 发布;app3 不向任何主题发布spec.metadata.subscriptionScopessubscriptionScopes 中未指定任何内容(默认行为),则所有应用程序都可以订阅所有主题app1=topic1;app2=topic2,topic3 将允许 app1 仅订阅 topic1,app2 订阅 topic2 和 topic3spec.metadata.allowedTopicsallowedTopics(默认行为),则所有主题均有效。如果存在 subscriptionScopes 和 publishingScopes,它们仍然生效。publishingScopes 或 subscriptionScopes 可与 allowedTopics 结合使用以添加精细限制spec.metadata.protectedTopicspublishingScopes 或 subscriptionScopes 显式授予发布或订阅权限才能发布/订阅该主题。这些元数据属性可用于所有发布订阅组件。以下示例使用 Redis 作为发布订阅组件。
如果您有包含敏感信息的主题,并且只允许您的应用程序的一个子集发布或订阅这些主题,那么限制哪些应用程序可以发布/订阅主题会很有用。
它还可以用于所有主题,以始终拥有哪些应用程序作为发布者/订阅者使用哪些主题的"真实来源"。
以下是三个应用程序和三个主题的示例:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: publishingScopes
value: "app1=topic1;app2=topic2,topic3;app3="
- name: subscriptionScopes
value: "app2=;app3=topic1"
下表显示了允许哪些应用程序向主题发布:
| topic1 | topic2 | topic3 | |
|---|---|---|---|
| app1 | ✅ | ||
| app2 | ✅ | ✅ | |
| app3 |
下表显示了允许哪些应用程序订阅主题:
| topic1 | topic2 | topic3 | |
|---|---|---|---|
| app1 | ✅ | ✅ | ✅ |
| app2 | |||
| app3 | ✅ |
注意:如果未列出应用程序(例如 subscriptionScopes 中的 app1),则允许它订阅所有主题。由于未使用
allowedTopics且 app1 没有任何订阅范围,因此它也可以使用上面未列出的其他主题。
如果 Dapr 应用程序向主题发送消息,则会创建该主题。在某些情况下,应该控制此主题创建。例如:
在这些情况下,可以使用 allowedTopics。
以下是三个允许主题的示例:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: allowedTopics
value: "topic1,topic2,topic3"
所有应用程序都可以使用这些主题,但只能使用这些主题,不允许使用其他主题。
allowedTopics 和范围有时您希望结合这两种范围,从而只有一组固定的允许主题并指定对特定应用程序的范围。
以下是三个应用程序和两个主题的示例:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: allowedTopics
value: "A,B"
- name: publishingScopes
value: "app1=A"
- name: subscriptionScopes
value: "app1=;app2=A"
注意:第三个应用程序未列出,因为如果应用程序未在范围内指定,则允许它使用所有主题。
下表显示了允许哪个应用程序向主题发布:
| A | B | C | |
|---|---|---|---|
| app1 | ✅ | ||
| app2 | ✅ | ✅ | |
| app3 | ✅ | ✅ |
下表显示了允许哪个应用程序订阅主题:
| A | B | C | |
|---|---|---|---|
| app1 | |||
| app2 | ✅ | ||
| app3 | ✅ | ✅ |
如果您的主题涉及敏感数据,则必须在 publishingScopes 和 subscriptionScopes 中显式列出每个新应用程序,以确保它无法从该主题读取或向其写入。或者,您可以将主题指定为"受保护"(使用 protectedTopics)并仅授予真正需要它的特定应用程序的访问权限。
以下是三个应用程序和三个主题的示例,其中两个是受保护的:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: protectedTopics
value: "A,B"
- name: publishingScopes
value: "app1=A,B;app2=B"
- name: subscriptionScopes
value: "app1=A,B;app2=B"
在上面的示例中,主题 A 和 B 被标记为受保护。因此,即使 app3 未在 publishingScopes 或 subscriptionScopes 下列出,它也无法与这些主题交互。
下表显示了允许哪个应用程序向主题发布:
| A | B | C | |
|---|---|---|---|
| app1 | ✅ | ✅ | |
| app2 | ✅ | ||
| app3 | ✅ |
下表显示了允许哪个应用程序订阅主题:
| A | B | C | |
|---|---|---|---|
| app1 | ✅ | ✅ | |
| app2 | ✅ | ||
| app3 | ✅ |
Dapr 支持每条消息设置生存时间(TTL)。这意味着应用程序可以为每条消息设置生存时间,订阅者在消息过期后将不会收到这些消息。
所有 Dapr 发布订阅组件都兼容消息 TTL,因为 Dapr 在运行时内部处理 TTL 逻辑。只需在发布消息时设置 ttlInSeconds 元数据即可。
对于某些组件(如 Kafka),可以通过 retention.ms 在主题级别配置生存时间,详见 文档。通过 Dapr 的消息 TTL,使用 Kafka 的应用程序现在可以除了按主题设置外,还可以为每条消息设置生存时间。
当发布订阅组件原生支持消息生存时间时,Dapr 直接转发 TTL 配置,不添加任何额外逻辑,保持可预测的行为。这在组件以不同方式处理过期消息时很有帮助。例如,使用 Azure Service Bus 时,过期消息会被存储在死信队列中,而不会被简单删除。
Azure Service Bus 支持实体级别生存时间。这意味着消息具有默认生存时间,但也可以在发布时设置为更短的时间范围。Dapr 会传播消息的 TTL 元数据,并让 Azure Service Bus 直接处理过期。
如果消息被不使用 Dapr 的订阅者消费,过期消息不会被自动丢弃,因为过期处理是由 Dapr 运行时在 Dapr 边车接收消息时执行的。但是,订阅者可以通过添加逻辑来处理云事件中的 expiration 属性来以编程方式丢弃过期消息,该属性遵循 RFC3339 格式。
当非 Dapr 订阅者使用原生处理消息 TTL 的组件(如 Azure Service Bus)时,它们不会收到过期的消息。这种情况下,不需要额外逻辑。
消息 TTL 可以作为发布请求的一部分在元数据中设置:
[%] (tab "Python SDK" %%) ```python from dapr.clients import DaprClient with DaprClient() as d: req_data = { 'order-number': '345' } # Create a typed message with content type and body resp = d.publish_event( pubsub_name='pubsub', topic='TOPIC_A', data=json.dumps(req_data), publish_metadata={'ttlInSeconds': '120'} ) # Print the request print(req_data, flush=True) ```
curl -X "POST" http://localhost:3500/v1.0/publish/pubsub/TOPIC_A?metadata.ttlInSeconds=120 -H "Content-Type: application/json" -d '{"order-number": "345"}'
<?php
require_once __DIR__.'/vendor/autoload.php';
$app = \Dapr\App::create();
$app->run(function(\DI\FactoryInterface $factory) {
$publisher = $factory->make(\Dapr\PubSub\Publish::class, ['pubsub' => 'pubsub']);
$publisher->topic('TOPIC_A')->publish('data', ['ttlInSeconds' => '120']);
});
[%] (/tab “PHP SDK” %%) [%] (/tabpane >)
有关发布订阅 API 的参考,请参阅本指南。
借助批量发布和订阅 API,您可以在单个请求中发布和订阅多条消息。在编写需要发送或接收大量消息的应用程序时,使用批量操作可以通过减少 Dapr 边车、应用程序与底层发布/订阅代理之间的总体请求数量,从而实现高吞吐量。
批量发布 API 允许您在单个请求中向主题发布多条消息。它是非事务性的,也就是说,在单个批量请求中,部分消息可能成功,部分消息可能失败。如果有任何消息发布失败,批量发布操作会返回失败消息列表。
批量发布操作也不保证消息的任何顺序。
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.BulkPublishResponse;
import io.dapr.client.domain.BulkPublishResponseFailedEntry;
import java.util.ArrayList;
import java.util.List;
class BulkPublisher {
private static final String PUBSUB_NAME = "my-pubsub-name";
private static final String TOPIC_NAME = "topic-a";
public void publishMessages() {
try (DaprClient client = (new DaprClientBuilder()).build()) {
// 创建要发布的消息列表
List<String> messages = new ArrayList<>();
for (int i = 0; i < 10; i++) {
String message = String.format("This is message #%d", i);
messages.add(message);
}
// 使用批量发布 API 发布消息列表
BulkPublishResponse<String> res = client.publishEvents(PUBSUB_NAME, TOPIC_NAME, "text/plain", messages).block();
}
}
}
import { DaprClient } from "@dapr/dapr";
const pubSubName = "my-pubsub-name";
const topic = "topic-a";
async function start() {
const client = new DaprClient();
// 向主题发布多条消息。
await client.pubsub.publishBulk(pubSubName, topic, ["message 1", "message 2", "message 3"]);
// 使用显式的批量发布消息向主题发布多条消息。
const bulkPublishMessages = [
{
entryID: "entry-1",
contentType: "application/json",
event: { hello: "foo message 1" },
},
{
entryID: "entry-2",
contentType: "application/cloudevents+json",
event: {
specversion: "1.0",
source: "/some/source",
type: "example",
id: "1234",
data: "foo message 2",
datacontenttype: "text/plain"
},
},
{
entryID: "entry-3",
contentType: "text/plain",
event: "foo message 3",
},
];
await client.pubsub.publishBulk(pubSubName, topic, bulkPublishMessages);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
using System;
using System.Collections.Generic;
using Dapr.Client;
const string PubsubName = "my-pubsub-name";
const string TopicName = "topic-a";
IReadOnlyList<object> BulkPublishData = new List<object>() {
new { Id = "17", Amount = 10m },
new { Id = "18", Amount = 20m },
new { Id = "19", Amount = 30m }
};
using var client = new DaprClientBuilder().Build();
var res = await client.BulkPublishEventAsync(PubsubName, TopicName, BulkPublishData);
if (res == null) {
throw new Exception("null response from dapr");
}
if (res.FailedEntries.Count > 0)
{
Console.WriteLine("Some events failed to be published!");
foreach (var failedEntry in res.FailedEntries)
{
Console.WriteLine("EntryId: " + failedEntry.Entry.EntryId + " Error message: " +
failedEntry.ErrorMessage);
}
}
else
{
Console.WriteLine("Published all events!");
}
import requests
import json
base_url = "http://localhost:3500/v1.0/publish/bulk/{}/{}"
pubsub_name = "my-pubsub-name"
topic_name = "topic-a"
payload = [
{
"entryId": "ae6bf7c6-4af2-11ed-b878-0242ac120002",
"event": "first text message",
"contentType": "text/plain"
},
{
"entryId": "b1f40bd6-4af2-11ed-b878-0242ac120002",
"event": {
"message": "second JSON message"
},
"contentType": "application/json"
}
]
response = requests.post(base_url.format(pubsub_name, topic_name), json=payload)
print(response.status_code)
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
const (
pubsubName = "my-pubsub-name"
topicName = "topic-a"
baseUrl = "http://localhost:3500/v1.0/publish/bulk/%s/%s"
)
func main() {
url := fmt.Sprintf(baseUrl, pubsubName, topicName)
method := "POST"
payload := strings.NewReader(`[
{
"entryId": "ae6bf7c6-4af2-11ed-b878-0242ac120002",
"event": "first text message",
"contentType": "text/plain"
},
{
"entryId": "b1f40bd6-4af2-11ed-b878-0242ac120002",
"event": {
"message": "second JSON message"
},
"contentType": "application/json"
}
]`)
client := &http.Client {}
req, _ := http.NewRequest(method, url, payload)
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
// ...
}
curl -X POST http://localhost:3500/v1.0/publish/bulk/my-pubsub-name/topic-a \
-H 'Content-Type: application/json' \
-d '[
{
"entryId": "ae6bf7c6-4af2-11ed-b878-0242ac120002",
"event": "first text message",
"contentType": "text/plain"
},
{
"entryId": "b1f40bd6-4af2-11ed-b878-0242ac120002",
"event": {
"message": "second JSON message"
},
"contentType": "application/json"
},
]'
Invoke-RestMethod -Method Post -ContentType 'application/json' -Uri 'http://localhost:3500/v1.0/publish/bulk/my-pubsub-name/topic-a' `
-Body '[
{
"entryId": "ae6bf7c6-4af2-11ed-b878-0242ac120002",
"event": "first text message",
"contentType": "text/plain"
},
{
"entryId": "b1f40bd6-4af2-11ed-b878-0242ac120002",
"event": {
"message": "second JSON message"
},
"contentType": "application/json"
},
]'
批量订阅 API 允许您在单个请求中从主题订阅多条消息。 正如我们从如何操作:发布和订阅主题中了解到的,有三种方式订阅主题:
要批量订阅主题,我们只需要使用 bulkSubscribe 规范属性,如下所示:
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: order-pub-sub
spec:
topic: orders
routes:
default: /checkout
pubsubname: order-pub-sub
bulkSubscribe:
enabled: true
maxMessagesCount: 100
maxAwaitDurationMs: 40
scopes:
- orderprocessing
- checkout
在上面的示例中,bulkSubscribe 是_可选的_。如果您使用 bulkSubscribe,则:
enabled 是必需的,用于在此主题上启用或禁用批量订阅maxMessagesCount)。
对于不支持批量订阅的组件,maxMessagesCount 的默认值为 100,即应用程序与 Dapr 之间的默认批量事件。请参阅组件如何处理批量消息的发布和订阅。
如果组件支持批量订阅,则可以在该组件的文档中找到此参数的默认值。maxAwaitDurationMs)。
对于不支持批量订阅的组件,maxAwaitDurationMs 的默认值为 1000,即应用程序与 Dapr 之间的默认批量事件。请参阅组件如何处理批量消息的发布和订阅。
如果组件支持批量订阅,则可以在该组件的文档中找到此参数的默认值。应用程序会收到与批量消息中每个条目(单独的消息)关联的 EntryId。应用程序必须使用此 EntryId 来传达该特定条目的状态。如果应用程序未能通知 EntryId 状态,则视为 RETRY。
需要发送一个 JSON 编码的负载正文,其中包含每个条目的处理状态:
{
"statuses":
[
{
"entryId": "<entryId1>",
"status": "<status>"
},
{
"entryId": "<entryId2>",
"status": "<status>"
}
]
}
可能的状态值:
| 状态 | 描述 |
|---|---|
SUCCESS | 消息处理成功 |
RETRY | 消息将由 Dapr 重试 |
DROP | 记录警告并丢弃消息 |
有关响应的更多见解,请参阅批量订阅的预期 HTTP 响应。
以下代码示例演示如何使用批量订阅。
import io.dapr.Topic;
import io.dapr.client.domain.BulkSubscribeAppResponse;
import io.dapr.client.domain.BulkSubscribeAppResponseEntry;
import io.dapr.client.domain.BulkSubscribeAppResponseStatus;
import io.dapr.client.domain.BulkSubscribeMessage;
import io.dapr.client.domain.BulkSubscribeMessageEntry;
import io.dapr.client.domain.CloudEvent;
import io.dapr.springboot.annotations.BulkSubscribe;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import reactor.core.publisher.Mono;
class BulkSubscriber {
@BulkSubscribe()
// @BulkSubscribe(maxMessagesCount = 100, maxAwaitDurationMs = 40)
@Topic(name = "topicbulk", pubsubName = "orderPubSub")
@PostMapping(path = "/topicbulk")
public Mono<BulkSubscribeAppResponse> handleBulkMessage(
@RequestBody(required = false) BulkSubscribeMessage<CloudEvent<String>> bulkMessage) {
return Mono.fromCallable(() -> {
List<BulkSubscribeAppResponseEntry> entries = new ArrayList<BulkSubscribeAppResponseEntry>();
for (BulkSubscribeMessageEntry<?> entry : bulkMessage.getEntries()) {
try {
CloudEvent<?> cloudEvent = (CloudEvent<?>) entry.getEvent();
System.out.printf("Bulk Subscriber got: %s\n", cloudEvent.getData());
entries.add(new BulkSubscribeAppResponseEntry(entry.getEntryId(), BulkSubscribeAppResponseStatus.SUCCESS));
} catch (Exception e) {
e.printStackTrace();
entries.add(new BulkSubscribeAppResponseEntry(entry.getEntryId(), BulkSubscribeAppResponseStatus.RETRY));
}
}
return new BulkSubscribeAppResponse(entries);
});
}
}
import { DaprServer } from "@dapr/dapr";
const pubSubName = "orderPubSub";
const topic = "topicbulk";
const daprHost = process.env.DAPR_HOST || "127.0.0.1";
const daprPort = process.env.DAPR_HTTP_PORT || "3502";
const serverHost = process.env.SERVER_HOST || "127.0.0.1";
const serverPort = process.env.APP_PORT || "5001";
async function start() {
const server = new DaprServer({
serverHost,
serverPort,
clientOptions: {
daprHost,
daprPort,
},
});
// 使用默认配置向主题发布多条消息。
await client.pubsub.bulkSubscribeWithDefaultConfig(pubSubName, topic, (data) => console.log("Subscriber received: " + JSON.stringify(data)));
// 使用特定的 maxMessagesCount 和 maxAwaitDurationMs 向主题发布多条消息。
await client.pubsub.bulkSubscribeWithConfig(pubSubName, topic, (data) => console.log("Subscriber received: " + JSON.stringify(data)), 100, 40);
}
using Microsoft.AspNetCore.Mvc;
using Dapr.AspNetCore;
using Dapr;
namespace DemoApp.Controllers;
[ApiController]
[Route("[controller]")]
public class BulkMessageController : ControllerBase
{
private readonly ILogger<BulkMessageController> logger;
public BulkMessageController(ILogger<BulkMessageController> logger)
{
this.logger = logger;
}
[BulkSubscribe("messages", 10, 10)]
[Topic("pubsub", "messages")]
public ActionResult<BulkSubscribeAppResponse> HandleBulkMessages([FromBody] BulkSubscribeMessage<BulkMessageModel<BulkMessageModel>> bulkMessages)
{
List<BulkSubscribeAppResponseEntry> responseEntries = new List<BulkSubscribeAppResponseEntry>();
logger.LogInformation($"Received {bulkMessages.Entries.Count()} messages");
foreach (var message in bulkMessages.Entries)
{
try
{
logger.LogInformation($"Received a message with data '{message.Event.Data.MessageData}'");
responseEntries.Add(new BulkSubscribeAppResponseEntry(message.EntryId, BulkSubscribeAppResponseStatus.SUCCESS));
}
catch (Exception e)
{
logger.LogError(e.Message);
responseEntries.Add(new BulkSubscribeAppResponseEntry(message.EntryId, BulkSubscribeAppResponseStatus.RETRY));
}
}
return new BulkSubscribeAppResponse(responseEntries);
}
public class BulkMessageModel
{
public string MessageData { get; set; }
}
}
目前,您只能在 Python 中使用 HTTP 客户端进行批量订阅。
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/dapr/subscribe', methods=['GET'])
def subscribe():
# 定义批量订阅配置
subscriptions = [{
"pubsubname": "pubsub",
"topic": "TOPIC_A",
"route": "/checkout",
"bulkSubscribe": {
"enabled": True,
"maxMessagesCount": 3,
"maxAwaitDurationMs": 40
}
}]
print('Dapr pub/sub is subscribed to: ' + json.dumps(subscriptions))
return jsonify(subscriptions)
# 定义处理传入消息的端点
@app.route('/checkout', methods=['POST'])
def checkout():
messages = request.json
print(messages)
for message in messages:
print(f"Received message: {message}")
return json.dumps({'success': True}), 200, {'ContentType': 'application/json'}
if __name__ == '__main__':
app.run(port=5000)
对于事件发布/订阅,涉及两种类型的网络传输。
这些是可以进行优化的机会。当优化时,会发出批量请求,从而减少总体调用次数,因此提高吞吐量并提供更好的延迟。
在启用批量发布和/或批量订阅时,应用程序与 Dapr 边车之间的通信(上面的第 1 点)对所有组件都进行了优化。
从 Dapr 边车到发布/订阅代理的优化取决于许多因素,例如:
目前,以下组件已更新以支持此级别的优化:
| 组件 | 批量发布 | 批量订阅 |
|---|---|---|
| Kafka | 是 | 是 |
| Azure Servicebus | 是 | 是 |
| Azure Eventhubs | 是 | 是 |
观看以下关于批量发布/订阅的演示和演示文稿。
Dapr workflow 使开发者能够以可靠的方式编写业务逻辑和集成。 由于 Dapr workflows 是有状态的,因此支持长时间运行和容错的应用程序,非常适合编排微服务。 Dapr workflow 可与其他 Dapr 构建块无缝协作,例如服务调用、发布订阅、状态管理和绑定。
持久化、有弹性的 Dapr Workflow 能力:

Dapr Workflow 可以执行的一些示例场景包括:
使用 Dapr Workflow,你可以编写活动,然后在工作流中编排这些活动。 工作流活动具有以下特点:
除了活动之外,你还可以编写工作流来调度其他工作流作为子工作流。 子工作流有其自己的实例 ID、历史记录和状态,独立于启动它的父工作流,但终止父工作流会终止其创建的所有子工作流除外。 子工作流还支持自动重试策略。
多应用程序工作流使你能够编排跨多个应用程序的复杂业务流程。 这允许工作流在不同应用程序中调用活动或启动子工作流,在保持 Dapr 工作流引擎的安全、可靠性和持久性保证的同时分配工作流执行。
与 Dapr actors 一样,你可以为任意时间范围安排类似提醒的持久延迟。
当你使用工作流代码创建应用程序并使用 Dapr 运行它时,你可以调用驻留在该应用程序中的特定工作流。 每个独立的工作流可以:
Dapr Workflow 简化了微服务架构中复杂的有状态协调需求。 以下部分描述了可以从 Dapr Workflow 中受益的几种应用程序模式。
详细了解不同类型的工作流模式
Dapr Workflow authoring SDK 是特定语言的 SDK,包含用于实现工作流逻辑的类型和函数。 工作流逻辑存在于你的应用程序中,并通过 gRPC 流由 Dapr 边车中运行的 Dapr Workflow 引擎编排。
你可以使用以下 SDK 编写工作流。
| 语言堆栈 | 包 |
|---|---|
| Python | dapr-ext-workflow |
| JavaScript | DaprWorkflowClient |
| .NET | Dapr.Workflow |
| Java | io.dapr.workflows |
| Go | workflow |
想测试工作流吗?请完成以下快速入门和教程来查看工作流的实际应用:
| 快速入门/教程 | 描述 |
|---|---|
| Workflow 快速入门 | 运行一个包含四个工作流活动的工作流应用程序,了解 Dapr Workflow 的实际应用 |
| Workflow Python SDK 示例 | 了解如何使用 Python dapr-ext-workflow 包创建 Dapr Workflow 并调用它。 |
| Workflow JavaScript SDK 示例 | 了解如何使用 JavaScript SDK 创建 Dapr Workflow 并调用它。 |
| Workflow .NET SDK 示例 | 了解如何使用 ASP.NET Core Web API 创建 Dapr Workflow 并调用它。 |
| Workflow Java SDK 示例 | 了解如何使用 Java io.dapr.workflows 包创建 Dapr Workflow 并调用它。 |
| Workflow Go SDK 示例 | 了解如何使用 Go workflow 包创建 Dapr Workflow 并调用它。 |
想跳过快速入门吗?没问题。你可以直接在你的应用程序中试用工作流构建块。 安装 Dapr后,你可以开始使用工作流,从如何编写工作流开始。
Dapr 通过 HTTP API 和 CLI 提供全面的工作流管理能力。
启动工作流
dapr workflow run MyWorkflow --app-id myapp --input '{"key": "value"}'
监控工作流
# 列出给定应用程序的活跃工作流
dapr workflow list --app-id myapp --filter-status RUNNING
# 查看执行历史
dapr workflow history <instance-id> --app-id myapp
控制工作流
# 暂停、恢复或终止
dapr workflow suspend <instance-id> --app-id myapp
dapr workflow resume <instance-id> --app-id myapp
dapr workflow terminate <instance-id> --app-id myapp
维护操作
# 清除已完成的工作流
dapr workflow purge --app-id myapp --all-older-than 720h
详细说明请参阅如何:管理工作流。
现在您已经对工作流构建块有了整体了解,让我们深入探讨 Dapr 工作流引擎和 SDK 提供的功能和概念。 Dapr 工作流公开了几个核心功能和概念,这些在所有支持的语言中都是通用的。
Dapr 工作流是您编写的函数,用于定义要按特定顺序执行的一系列任务。 Dapr 工作流引擎负责任务的调度和执行,包括管理失败和重试。 如果托管工作流的应用程序扩展到多台机器上,工作流引擎会在多台机器之间对工作流及其任务的执行进行负载均衡。
工作流可以调度多种不同类型的任务,包括:
您可以使用 CLI 查询工作流实例:
# 查找所有运行中的工作流
dapr workflow list --app-id myapp --filter-status RUNNING
# 按名称查找工作流
dapr workflow list --app-id myapp --filter-name OrderProcessing
# 查找最近的工作流(过去 2 小时)
dapr workflow list --app-id myapp --filter-max-age 2h
# 获取详细的 JSON 输出
dapr workflow list --app-id myapp --output json
查看完整的执行历史:
dapr workflow history wf-12345 --app-id myapp --output json
这将显示所有事件、活动和状态转换。
dapr workflow raise-event wf-12345/ApprovalReceived \
--app-id myapp \
--input '{"approved": true, "comments": "Approved by manager"}'
# 暂停以进行手动干预
dapr workflow suspend wf-12345 \
--app-id myapp \
--reason "Awaiting customer response"
# 准备就绪后恢复
dapr workflow resume wf-12345 \
--app-id myapp \
--reason "Customer responded"
每个您定义的工作流都有一个类型名称,而工作流的每次单独执行都需要一个唯一的_实例 ID_。工作流实例 ID 可以由您的应用程序代码生成,这在工作流对应于文档或作业等业务实体时非常有用;也可以是自动生成的 UUID。工作流的实例 ID 可用于调试,也可用于使用 Workflow API 管理工作流。
任何给定时刻只能存在一个具有给定 ID 的工作流实例。但是,如果某个工作流实例完成或失败,其 ID 可以被新的工作流实例重用。但请注意,新的工作流实例将有效地替换配置的状态存储中的旧实例。
Dapr 工作流使用一种称为事件溯源的技术来维护其执行状态。工作流引擎不存储工作流的当前状态快照,而是管理一个仅追加的历史事件日志,记录描述工作流所执行的各种步骤。当使用工作流 SDK 时,这些历史事件会在工作流"等待"调度任务的结果时自动存储。
当工作流"等待"调度任务时,它会从内存中卸载,直到任务完成。一旦任务完成,工作流引擎会调度工作流函数再次运行。第二次工作流函数执行称为_重放_。
当工作流函数被重放时,它会从头开始运行。但是,当它遇到已经完成的任务时,工作流引擎不会再次调度该任务,而是:
这种"重放"行为会持续进行,直到工作流函数完成或以错误失败。
使用这种重放技术,工作流能够从任何"等待"点恢复执行,就好像它从未从内存中卸载一样。甚至可以恢复前次运行中的局部变量值,而工作流引擎无需知道它们存储了什么数据。这种恢复状态的能力使 Dapr 工作流具有_持久性_和_容错性_。
如工作流重放章节所述,工作流维护其所有操作的只写事件溯源历史日志。为了避免资源失控使用,工作流必须限制其调度的操作数量。例如,确保您的工作流不会:
如果工作流可能需要调度大量任务,您可以使用以下两种技术来编写工作流:
使用 continue-as-new API:
每个工作流 SDK 都暴露了一个 continue-as-new API,工作流可以调用它来使用新的输入和历史重新启动自身。continue-as-new API 特别适合实现"永生工作流",如监控代理,否则将使用类似 while (true) 的构造来实现。使用 continue-as-new 是保持工作流历史记录规模较小的好方法。
continue-as-new API 会截断现有历史记录,用新的历史记录替换它。
使用子工作流: 每个工作流 SDK 都暴露了用于创建子工作流的 API。子工作流的行为与任何其他工作流一样,只是它由父工作流调度。子工作流具有:
如果工作流需要调度数千个或更多任务,建议将这些任务分布在子工作流中,以避免单个工作流的历史记录规模过大。
由于工作流是长期运行且持久的,更新工作流代码必须非常小心。如工作流确定性限制章节所述,工作流代码必须具有确定性。如果系统中存在任何未完成的工作流实例,则对工作流代码的更新必须保持这种确定性。否则,对工作流代码的更新可能导致这些工作流下次执行时出现运行时故障。
工作流活动是工作流中的基本工作单元,也是业务流程中被编排的任务。例如,您可能创建一个工作流来处理订单。任务可能涉及检查库存、向客户收费和创建发货。每个任务都是一个单独的活动。这些活动可以串行执行、并行执行或两者结合执行。
与工作流不同,活动对您可以在其中执行的工作类型没有限制。活动经常用于发出网络调用或运行 CPU 密集型操作。活动也可以将数据返回给工作流。
Dapr 工作流引擎保证每个被调用的活动作为工作流执行的一部分至少执行一次。由于活动只保证至少一次执行,建议尽可能将活动逻辑实现为幂等的。
除了活动,工作流还可以将其他工作流调度为_子工作流_。子工作流有自己的实例 ID、历史记录和状态,独立于启动它的父工作流。
子工作流有许多好处:
子工作流的返回值就是其输出。如果子工作流因异常失败,则该异常会像活动任务因异常失败一样被暴露到父工作流。子工作流也支持自动重试策略。
终止父工作流会终止由该工作流实例创建的所有子工作流。详见终止工作流 API。
Dapr 工作流允许您为任意时间范围安排类似提醒的持久化延迟,包括分钟、天甚至数年。这些_持久化定时器_可以由工作流调度以实现简单延迟或为其他异步任务设置临时超时。更具体地说,持久化定时器可以设置为在特定日期触发或在指定持续时间后触发。持久化定时器的最大持续时间没有限制,它们在内部由内部 actor 提醒器支持。例如,跟踪某项服务 30 天免费订阅的工作流可以使用在创建工作流 30 天后触发的持久化定时器来实现。工作流在等待持久化定时器触发时可以安全地从内存中卸载。
工作流支持针对活动和子工作流的持久化重试策略。工作流重试策略与 Dapr 弹性策略 在以下方面有所不同:
重试在内部使用持久化定时器实现。这意味着工作流在等待重试触发时可以安全地从内存中卸载,从而节省系统资源。这也意味着重试之间的延迟可以任意长,包括分钟、小时甚至数天。
可以同时使用工作流重试策略和 Dapr 弹性策略。例如,如果工作流活动使用 Dapr 客户端调用服务,则 Dapr 客户端使用配置好的弹性策略。详见快速入门:服务间弹性以获取更多信息和示例。但是,如果活动本身因任何原因失败,包括耗尽弹性策略的重试次数,则工作流的弹性策略会介入。
由于工作流重试策略是在代码中配置的,确切的开发者体验可能因工作流 SDK 版本而异。一般来说,工作流重试策略可以使用以下参数进行配置:
| 参数 | 描述 |
|---|---|
| 最大尝试次数 | 执行活动或子工作流的最大次数。如果设置为 0,则不会进行任何尝试。 |
| 首次重试间隔 | 等待第一次重试的时间量。 |
| 退避系数 | 用于确定退避增加速率的系数。例如,系数为 2 会使每次后续重试的等待时间翻倍。 |
| 最大重试间隔 | 每次后续重试之前等待的最大时间量。如果设置为 0,则不会发生重试。 |
| 重试超时 | 重试的全局超时时间,无论配置的最大尝试次数如何。此超时到期后,将不再尝试执行活动。 |
有时工作流需要等待由外部系统触发的事件。例如,审批工作流可能要求人工在工作流处理订单时明确批准订单请求(如果总成本超过某个阈值)。另一个例子是琐事游戏编排工作流,在等待所有参与者提交他们对琐事问题的答案时暂停。这些中途执行的输入称为_外部事件_。
外部事件具有_名称_和_有效载荷_,并被传递到单个工作流实例。工作流可以创建"等待外部事件"任务来订阅外部事件,并_等待_这些任务以阻塞执行直到收到事件。然后工作流可以读取这些事件的有效载荷,并决定下一步采取什么行动。外部事件可以串行或并行处理。外部事件可以由其他工作流或工作流代码触发。
工作流也可以等待多个相同名称的外部事件信号,在这种情况下,它们会以先进先出(FIFO)的方式被分派到相应的工作流任务。如果工作流收到外部事件信号但尚未创建"等待外部事件"任务,该事件将被保存到工作流的历史记录中,并在工作流请求该事件后立即被消费。
了解更多关于与外部系统交互的信息。
工作流状态可以从状态存储中清除,清除其所有历史记录并移除与特定工作流实例相关的所有元数据。清除功能用于已运行到 COMPLETED、FAILED 或 TERMINATED 状态的工作流。
在 workflow API 参考指南 中了解更多。
工作流代码是长期运行的,必须在更新期间保持确定性。有关修补和命名工作流版本控制的详细信息,请参阅工作流版本控制。
为了利用工作流重放技术,您的工作流代码需要具有确定性。为了使您的工作流代码具有确定性,您可能需要解决一些限制。
生成随机数、随机 UUID 或获取当前日期的 API 是_非确定性的_。要解决此限制,您可以:
例如,不要这样做:
// 不要这样做!
DateTime currentTime = DateTime.UtcNow;
Guid newIdentifier = Guid.NewGuid();
string randomString = GetRandomString();
// 不要这样做!
Instant currentTime = Instant.now();
UUID newIdentifier = UUID.randomUUID();
String randomString = getRandomString();
// 不要这样做!
const currentTime = new Date();
const newIdentifier = uuidv4();
const randomString = getRandomString();
// 不要这样做!
const currentTime = time.Now()
这样做:
// 这样做!!
DateTime currentTime = context.CurrentUtcDateTime;
Guid newIdentifier = context.NewGuid();
string randomString = await context.CallActivityAsync<string>(nameof("GetRandomString")); //使用 "nameof" 以防止指定应用程序中不存在的活动名称
// 这样做!!
Instant currentTime = context.getCurrentInstant();
Guid newIdentifier = context.newGuid();
String randomString = context.callActivity(GetRandomString.class.getName(), String.class).await();
// 这样做!!
const currentTime = context.getCurrentUtcDateTime();
const randomString = yield context.callActivity(getRandomString);
const currentTime = ctx.CurrentUTCDateTime()
外部数据包括任何不存储在工作流状态中的数据。工作流不得与全局变量、环境变量、文件系统交互,或进行网络调用。
相反,工作流应该使用工作流输入、活动任务和外部事件处理来_间接_地与外部状态交互。
例如,不要这样做:
// 不要这样做!
string configuration = Environment.GetEnvironmentVariable("MY_CONFIGURATION")!;
string data = await new HttpClient().GetStringAsync("https://example.com/api/data");
// 不要这样做!
String configuration = System.getenv("MY_CONFIGURATION");
HttpRequest request = HttpRequest.newBuilder().uri(new URI("https://postman-echo.com/post")).GET().build();
HttpResponse<String> response = HttpClient.newBuilder().build().send(request, HttpResponse.BodyHandlers.ofString());
// 不要这样做!
// 访问环境变量 (Node.js)
const configuration = process.env.MY_CONFIGURATION;
fetch('https://postman-echo.com/get')
.then(response => response.text())
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error:', error);
});
// 不要这样做!
resp, err := http.Get("http://example.com/api/data")
这样做:
// 这样做!!
string configuration = workflowInput.Configuration; // 假设的工作流输入参数
string data = await context.CallActivityAsync<string>(nameof("MakeHttpCall"), "https://example.com/api/data");
// 这样做!!
String configuration = ctx.getInput(InputType.class).getConfiguration(); // 假设的工作流输入参数
String data = ctx.callActivity(MakeHttpCall.class, "https://example.com/api/data", String.class).await();
// 这样做!!
const configuration = workflowInput.getConfiguration(); // 假设的工作流输入参数
const data = yield ctx.callActivity(makeHttpCall, "https://example.com/api/data");
// 这样做!!
err := ctx.CallActivity(MakeHttpCallActivity, workflow.ActivityInput("https://example.com/api/data")).Await(&output)
每种语言 SDK 的实现都要求所有工作流函数操作在与调度函数的同一线程(goroutine 等)上操作。工作流函数必须永远不要:
违反此规则可能导致未定义的行为。任何后台处理都应委托给活动任务,活动任务可以串行或并发调度。
例如,不要这样做:
// 不要这样做!
Task t = Task.Run(() => context.CallActivityAsync("DoSomething"));
await context.CreateTimer(5000).ConfigureAwait(false);
// 不要这样做!
new Thread(() -> {
ctx.callActivity(DoSomethingActivity.class.getName()).await();
}).start();
ctx.createTimer(Duration.ofSeconds(5)).await();
不要将 JavaScript 工作流声明为 async。Node.js 运行时不能保证异步函数是确定性的。
// 不要这样做!
go func() {
err := ctx.CallActivity(DoSomething).Await(nil)
}()
err := ctx.CreateTimer(time.Second).Await(nil)
这样做:
// 这样做!!
Task t = context.CallActivityAsync(nameof("DoSomething"));
await context.CreateTimer(5000).ConfigureAwait(true);
// 这样做!!
ctx.callActivity(DoSomethingActivity.class.getName()).await();
ctx.createTimer(Duration.ofSeconds(5)).await();
由于 Node.js 运行时不能保证异步函数是确定性的,请始终将 JavaScript 工作流声明为同步生成器函数。
// 这样做!
task := ctx.CallActivity(DoSomething)
task.Await(nil)
确保您对工作流代码的更新保持其确定性。以下是一些可能破坏工作流确定性的代码更新示例:
要解决这些约束,请使用版本控制指南中描述的工作流版本控制概念来修补和引入新的命名工作流版本,以确定性地将更改合并到您的工作流中。
在许多场景中,需要在工作流活跃运行时引入对工作流代码的更改。在这些更改具有非确定性的情况下,需要采用版本控制策略,以便现有工作流可以继续使用原始代码执行,而新工作流则使用更新后的版本。
工作流可以使用两种互补的方法进行版本控制,这两种方法旨在结合使用。它们是:
在任一版本控制策略中,工作流都可能达到 stalled(停滞)状态。这通常发生在滚动部署期间,此时新旧版本的工作流代码同时运行。
当工作流在新副本上启动,但后续尝试在旧副本上重放时,版本化的工作流将变为停滞状态。
工作流可能会在整个滚动部署期间保持停滞状态,直到只有新副本可用为止。一旦停滞的工作流被调度到新副本上,它将继续执行。
如果工作流从未继续,则表示导致停滞的条件仍然存在,需要解决。请参阅下面每种版本控制方法中导致停滞的具体原因。
为了最好地理解版本控制如何工作以及如何有效地使用它,首先了解它解决了什么以及为什么需要版本控制会很有帮助。工作流必须是确定性的,这意味着每次运行时,它们都会产生与上次完全相同的输出。
这是一个关键概念,因为 Dapr 工作流实现使用事件溯源方法来持久化工作流状态。当在工作流中执行活动和子工作流时,Dapr 会维护一个历史记录,记录这些边界执行的输入和输出。当每个活动完成时,我们会持久化更新后的事件历史,然后从顶部重新调用工作流(这称为"重放")。这一次,当它遇到这些活动或子工作流之一时,它会替换已实现的输出并跳过重新执行,然后重复。
因此,至关重要的是,在工作流运行期间不得随意引入代码,因为当引擎重放工作流时,您的更改可能会导致工作流不再与其历史记录匹配并失败。
修补是我们的两种版本控制方法中的第一种,它允许您在保持重放之间确定性的同时引入对工作流的更改。它的工作原理是允许您通过 if 语句插入与命名标识符关联的分支逻辑。工作流历史记录会在重放工作流时跟踪这些命名标识符的观察位置,从而确保在重放之间遵循一致的逻辑路径。
这使您能够仅对需要调整的特定位置对工作流代码进行有针对性的更改。
要添加补丁,请选择一个稳定的唯一名称来标识该补丁。这可以是描述性的内容,如 use-sms,它描述了补丁更改的内容,也可以是本质不同的内容,如当前时间戳或日期。您使用的具体值并不重要,只要不使用已部署到生产环境中的标识符即可。您将通过添加一个 if 语句来插入补丁,该语句评估工作流是否已应用此补丁——如果是,则使用补丁的代码;如果不是,则采用原始代码路径。
补丁标识符的唯一性要求仅扩展到此工作流;不会评估工作流之间的补丁,因此可以在多个工作流中使用重复的标识符而不会产生冲突。
以下示例演示了这一点,通过检查此工作流实例是否已应用 use-sms 补丁。如果是,则执行 SendSMS 活动;否则,它回退到原始路径并使用 SendEmail 活动。
public sealed class MyWorkflow : Workflow<string, object?>
{
public overrride async Task<object?> RunAsync(WorkflowContext context, string input)
{
// ...
if (context.IsPatched("use-sms"))
{
// 修补后的代码
await context.CallActivityAsync(nameof(SendSMS), input);
}
else {
// 原始代码
await context.CallActivityAsync(nameof(SendEmail), input);
}
return null;
}
}
import "github.com/dapr/durabletask-go/workflow"
func Workflow(ctx *workflow.WorkflowContext) error {
// ...
if ctx.IsPatched("use-sms") {
if err := ctx.CallActivity("SendSMS", ctx.GetInput()).Await(); err != nil {
return err
}
} else {
if err := ctx.CallActivity("SendEmail", ctx.GetInput()).Await(); err != nil {
return err
}
}
// ...
}
from dapr.ext.workflow import DaprWorkflowContext, WorkflowRuntime
wfr = WorkflowRuntime()
@wfr.workflow
def my_workflow(ctx: DaprWorkflowContext, wf_input: str):
# ...
if ctx.is_patched("use-sms"):
yield ctx.call_activity(send_sms)
else:
yield ctx.call_activity(send_email)
# ...
使用这种方法,新的工作流实例将采用修补后的代码路径,但在引入补丁时正在运行的现有工作流将继续使用原始代码路径。补丁检查在首次评估时会记录在工作流实例历史记录中,这意味着可以在工作流的不同位置安全地使用相同的标识符。
同样,至关重要的是,一旦修补后的工作流在生产环境中运行,您就不应再重新使用该标识符。部署后,应假设引擎将以确定性方式处理工作流,但使用以前使用的标识符添加新补丁会破坏该契约:如果您的新代码插入位置早于正在运行的工作流实例评估到的位置,当它重放时,它将采用您的补丁分支,但您的更改可能不再与现有工作流历史记录匹配,并导致工作流无限期停滞(至少直到您删除新补丁或更新它以使用不同的唯一标识符)。
应用于工作流的补丁列表存储在工作流的历史记录中,因此重要的是要注意,您使用的补丁越多,工作流状态历史记录就越大。这将越来越对工作流性能产生负面影响,因为每次重放都需要检索它,并且随着状态的增长,这会增加检索和解析开销。
除了上述重新使用标识符外,使用补丁时还有其他几个原因导致工作流停滞:
您可以使用 Dapr CLI 中的工作流命令 来检查停滞的工作流。例如,以下是当工作流停滞时 dapr workflow list 命令的显示结果:
> dapr workflow list
NAME ID STATUS AGE
workflow <ID> STALLED 9m39s
以下是当工作流停滞时 dapr workflow history 命令的显示结果:
> dapr workflow history <ID> -k -o wide
PLAY TYPE NAME TIMESTAMP ELAPSED STATUS DETAILS ROUTER EXECUTION ID ATTRS
0 ExecutionStarted workflow <TIMESTAMP> Age:15.8h RUNNING workflowStart workflows <EXEC_ID> input=2026-01-22T14:44:02.728101
1 OrchestratorStarted <TIMESTAMP> 25.3ms RUNNING replay versionName=workflow_v1
1 ExecutionStalled <TIMESTAMP> 8.9m STALLED reason=VERSION_NAME_MISMATCH;description=Version not available: workflow_v1
以下代表一些额外的建议,应考虑这些建议以消除因使用补丁而导致工作流停滞或失败的可能性:
else 分支,将使应用未来的补丁更容易。虽然替换现有代码时通常无法避免这种情况,但如果您只是向工作流添加新逻辑,这当然是可以管理的。else 分支中包装现有的补丁。否则,正在运行的工作流将无法访问原始代码,从而破坏确定性保证。命名工作流版本控制代表我们的第二种方法,有助于以确定性安全的方式对工作流进行版本控制。在此方法中,您通过复制现有工作流并为其分配一个新的"版本化"名称来引入新的工作流版本。因为您可以从头开始重构工作流逻辑以删除任何补丁并引入您想要的任何其他更改,所以这种方法提供了与以前工作流版本的干净断开。
虽然实现此目的的具体细节取决于您使用的语言 SDK,但通常,您将复制最新的工作流,为其分配一个反映较新版本的唯一名称,并向您的 SDK 注册它。将为每个工作流构建一个注册表,以便在运行时,Dapr 将使用工作流名称进行调用,SDK 将路由到旧工作流(如果正在运行)或较新版本。
请注意,在所有 SDK 中,运行时不会在版本之间顺序迁移工作流。相反,当 SDK 收到运行新工作流的请求时,它选择运行最新版本,而不仅仅是"下一个"版本,因此无需处理版本之间的任何补偿逻辑。
语言 SDK 可能会公开一种在使用相同工作流名称时注册版本的方法,但这因 SDK 而异,因此请参阅特定 SDK 的文档以获取更多信息。例如,某些可能使用一种机制,您可以在其中显式提供 is_latest 标志来指示哪个版本是最新版本。
当 SDK 收到运行未注册版本的工作流的请求时,工作流将停滞。这可能在应用程序滚动部署期间自然发生,但也可能在某个版本被删除而某些工作流实例仍在使用它时发生。建议是,除非您已独立确认没有未完成的(正在运行或休眠的)工作流实例正在针对该版本运行,否则不应删除较旧的命名工作流版本。
.NET 使用源生成器自动识别和注册您的工作流版本,因此无需手动注册工作流(活动需要注册)。默认情况下,.NET 使用内置的可配置数字版本控制策略,其中版本作为名称的后缀提供。有关如何更改版本控制策略或配置以及如何在应用程序中设置版本控制,请参阅 .NET SDK 文档。
由于 .NET 应用程序不允许存在多个具有相同名称的类型,因此这不是此 SDK 中的选项。
要创建新的命名工作流版本,请复制现有工作流并修改类的名称以使用相同的前缀,但更改后缀中的版本标识符以反映较新的版本。重新构建应用程序,SDK 将自动处理其余部分。
给定以下工作流定义:
import "github.com/dapr/durabletask-go/workflow"
func WorkflowV1(ctx *workflow.WorkflowContext) error {
// ...
return nil
}
func WorkflowV2(ctx *workflow.WorkflowContext) error {
// ...
return nil
}
这是您注册这两个工作流的方式:
import "github.com/dapr/durabletask-go/workflow"
registry := workflow.NewRegistry()
// 这是以前的工作流版本,因此 `isLatest` 为 false
registry.AddVersionedWorkflow("Workflow", false, WorkflowV1)
// 这是最新的工作流版本,因此 `isLatest` 为 true
registry.AddVersionedWorkflow("Workflow", true, WorkflowV2)
这是相同的示例,但显式设置版本名称:
import "github.com/dapr/durabletask-go/workflow"
registry := workflow.NewRegistry()
// 这是以前的工作流版本,因此 `isLatest` 为 false
registry.AddVersionedWorkflow("Workflow", "WorkflowV1", false, WorkflowV1)
// 这是最新的工作流版本,因此 `isLatest` 为 true
registry.AddVersionedWorkflow("Workflow", "WorkflowV2", true, WorkflowV2)
注意:AddVersionedOrchestrator 和 AddVersionedOrchestratorN 的布尔参数指示工作流是否是最新版本。
这是使用不同版本的工作流:
from dapr.ext.workflow import DaprWorkflowContext, WorkflowRuntime
wfr = WorkflowRuntime()
@wfr.versioned_workflow(name="workflow")
def workflow_v1(ctx: DaprWorkflowContext, wf_input: str):
# ...
@wfr.versioned_workflow(name="workflow", is_latest=True)
def workflow_v2(ctx: DaprWorkflowContext, wf_input: str):
# ...
这是相同的示例,但显式设置版本名称:
from dapr.ext.workflow import DaprWorkflowContext, WorkflowRuntime
wfr = WorkflowRuntime()
@wfr.versioned_workflow(name="workflow", version_name="workflow_v1")
def workflow_v1(ctx: DaprWorkflowContext, wf_input: str):
# ...
@wfr.versioned_workflow(name="workflow", version_name="workflow_v2", is_latest=True)
def workflow_v2(ctx: DaprWorkflowContext, wf_input: str):
# ...
由于命名工作流与补丁完全兼容,因此该方法是对工作流的更改最初通过向现有工作流逻辑应用补丁来进行的。最终,在应用了几个补丁后,您将遇到以下问题之一:
此时,建议您引入工作流的命名版本。复制现有工作流,更改其名称以反映您在 SDK 中使用的任何版本控制策略,并重构以删除所有补丁,仅保留预期的"最新"逻辑。应用您的新更改(此处无需补丁——这是一个新的工作流)并部署它。
在这里,根据需要再次应用补丁以解决未来的更改,并在必要时引入另一个命名工作流版本并继续。无限重复此过程。
建议您保留旧的工作流版本,直到您完全确信没有任何正在运行的(包括使用长期运行计时器延迟的)使用任何旧工作流版本的内容。
工作流版本控制不解决更改工作流本身的输入和输出类型的问题。作为一般指导,建议要么返回序列化值(如字符串),使输出的使用者能够随着时间的推移反序列化不同的输出,要么采用包含可选属性的复杂类型以包含新的预期输出值。
Dapr 工作流简化了微服务架构中复杂的有状态协调需求。以下各节介绍了几种可以从 Dapr 工作流中受益的应用程序模式。
在任务链模式中,工作流中的多个步骤按顺序运行,一个步骤的输出可以作为下一步的输入传递。任务链工作流通常涉及创建需要对某些数据执行的操作序列,例如过滤、转换和规约。

在某些情况下,工作流的步骤可能需要跨多个微服务进行编排。为了提高可靠性和可扩展性,您可能还会使用队列来触发各个步骤。
虽然该模式很简单,但在实现中隐藏了许多复杂性。例如:
Dapr 工作流通过允许您使用您选择的编程语言将任务链模式简洁地实现为一个简单的函数来解决这些复杂性,如以下示例所示。
import dapr.ext.workflow as wf
def task_chain_workflow(ctx: wf.DaprWorkflowContext, wf_input: int):
try:
result1 = yield ctx.call_activity(step1, input=wf_input)
result2 = yield ctx.call_activity(step2, input=result1)
result3 = yield ctx.call_activity(step3, input=result2)
except Exception as e:
yield ctx.call_activity(error_handler, input=str(e))
raise
return [result1, result2, result3]
def step1(ctx, activity_input):
print(f'Step 1: Received input: {activity_input}.')
# Do some work
return activity_input + 1
def step2(ctx, activity_input):
print(f'Step 2: Received input: {activity_input}.')
# Do some work
return activity_input * 2
def step3(ctx, activity_input):
print(f'Step 3: Received input: {activity_input}.')
# Do some work
return activity_input ^ 2
def error_handler(ctx, error):
print(f'Executing error handler: {error}.')
# Apply some compensating work
Note 工作流重试策略将在 Python SDK 的未来版本中提供。
import { DaprWorkflowClient, WorkflowActivityContext, WorkflowContext, WorkflowRuntime, TWorkflow } from "@dapr/dapr";
async function start() {
// Update the gRPC client and worker to use a local address and port
const daprHost = "localhost";
const daprPort = "50001";
const workflowClient = new DaprWorkflowClient({
daprHost,
daprPort,
});
const workflowRuntime = new WorkflowRuntime({
daprHost,
daprPort,
});
const hello = async (_: WorkflowActivityContext, name: string) => {
return `Hello ${name}!`;
};
const sequence: TWorkflow = async function* (ctx: WorkflowContext): any {
const cities: string[] = [];
const result1 = yield ctx.callActivity(hello, "Tokyo");
cities.push(result1);
const result2 = yield ctx.callActivity(hello, "Seattle");
cities.push(result2);
const result3 = yield ctx.callActivity(hello, "London");
cities.push(result3);
return cities;
};
workflowRuntime.registerWorkflow(sequence).registerActivity(hello);
// Wrap the worker startup in a try-catch block to handle any errors during startup
try {
await workflowRuntime.start();
console.log("Workflow runtime started successfully");
} catch (error) {
console.error("Error starting workflow runtime:", error);
}
// Schedule a new orchestration
try {
const id = await workflowClient.scheduleNewWorkflow(sequence);
console.log(`Orchestration scheduled with ID: ${id}`);
// Wait for orchestration completion
const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30);
console.log(`Orchestration completed! Result: ${state?.serializedOutput}`);
} catch (error) {
console.error("Error scheduling or waiting for orchestration:", error);
}
await workflowRuntime.stop();
await workflowClient.stop();
// stop the dapr side car
process.exit(0);
}
start().catch((e) => {
console.error(e);
process.exit(1);
# Apply custom compensation logic
});
// Expotential backoff retry policy that survives long outages
var retryOptions = new WorkflowTaskOptions
{
RetryPolicy = new WorkflowRetryPolicy(
firstRetryInterval: TimeSpan.FromMinutes(1),
backoffCoefficient: 2.0,
maxRetryInterval: TimeSpan.FromHours(1),
maxNumberOfAttempts: 10),
};
try
{
var result1 = await context.CallActivityAsync<string>("Step1", wfInput, retryOptions);
var result2 = await context.CallActivityAsync<byte[]>("Step2", result1, retryOptions);
var result3 = await context.CallActivityAsync<long[]>("Step3", result2, retryOptions);
return string.Join(", ", result4);
}
catch (TaskFailedException) // Task failures are surfaced as TaskFailedException
{
// Retries expired - apply custom compensation logic
await context.CallActivityAsync<long[]>("MyCompensation", options: retryOptions);
throw;
}
Note 在上面的示例中,
"Step1"、"Step2"、"Step3"和"MyCompensation"表示工作流活动,这些是实际实现工作流步骤的代码中的函数。为简洁起见,这些活动的实现未包含在此示例中。
public class ChainWorkflow extends Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
StringBuilder sb = new StringBuilder();
String wfInput = ctx.getInput(String.class);
String result1 = ctx.callActivity("Step1", wfInput, String.class).await();
String result2 = ctx.callActivity("Step2", result1, String.class).await();
String result3 = ctx.callActivity("Step3", result2, String.class).await();
String result = sb.append(result1).append(',').append(result2).append(',').append(result3).toString();
ctx.complete(result);
};
}
}
class Step1 implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(Step1.class);
logger.info("Starting Activity: " + ctx.getName());
// Do some work
return null;
}
}
class Step2 implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(Step2.class);
logger.info("Starting Activity: " + ctx.getName());
// Do some work
return null;
}
}
class Step3 implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(Step3.class);
logger.info("Starting Activity: " + ctx.getName());
// Do some work
return null;
}
}
func TaskChainWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
var result1 int
if err := ctx.CallActivity(Step1, workflow.ActivityInput(input)).Await(&result1); err != nil {
return nil, err
}
var result2 int
if err := ctx.CallActivity(Step2, workflow.ActivityInput(input)).Await(&result2); err != nil {
return nil, err
}
var result3 int
if err := ctx.CallActivity(Step3, workflow.ActivityInput(input)).Await(&result3); err != nil {
return nil, err
}
return []int{result1, result2, result3}, nil
}
func Step1(ctx workflow.ActivityContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
fmt.Printf("Step 1: Received input: %s", input)
return input + 1, nil
}
func Step2(ctx workflow.ActivityContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
fmt.Printf("Step 2: Received input: %s", input)
return input * 2, nil
}
func Step3(ctx workflow.ActivityContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
fmt.Printf("Step 3: Received input: %s", input)
return int(math.Pow(float64(input), 2)), nil
}
如您所见,工作流被表示为您选择的编程语言中的一系列简单语句。这使得组织中的任何工程师都可以快速理解端到端流程,而无需一定理解端到端系统架构。
在幕后,Dapr 工作流运行时:
在扇出/扇入设计模式中,您跨多个工作器同时执行多个任务,等待它们完成,然后对结果执行某种聚合。

除了前面模式中提到的挑战之外,手动实现扇出/扇入模式时还有几个重要问题需要考虑:
Dapr 工作流提供了一种将扇出/扇入模式表示为简单函数的方法,如以下示例所示:
# Start the workflow
dapr workflow run DataProcessingWorkflow \
--app-id processor \
--input '{"items": ["item1", "item2", "item3"]}'
# Monitor parallel execution
dapr workflow history <instance-id> --app-id processor --output json
import time
from typing import List
import dapr.ext.workflow as wf
def batch_processing_workflow(ctx: wf.DaprWorkflowContext, wf_input: int):
# get a batch of N work items to process in parallel
work_batch = yield ctx.call_activity(get_work_batch, input=wf_input)
# schedule N parallel tasks to process the work items and wait for all to complete
parallel_tasks = [ctx.call_activity(process_work_item, input=work_item) for work_item in work_batch]
outputs = yield wf.when_all(parallel_tasks)
# aggregate the results and send them to another activity
total = sum(outputs)
yield ctx.call_activity(process_results, input=total)
def get_work_batch(ctx, batch_size: int) -> List[int]:
return [i + 1 for i in range(batch_size)]
def process_work_item(ctx, work_item: int) -> int:
print(f'Processing work item: {work_item}.')
time.sleep(5)
result = work_item * 2
print(f'Work item {work_item} processed. Result: {result}.')
return result
def process_results(ctx, final_result: int):
print(f'Final result: {final_result}.')
import {
Task,
DaprWorkflowClient,
WorkflowActivityContext,
WorkflowContext,
WorkflowRuntime,
TWorkflow,
} from "@dapr/dapr";
// Wrap the entire code in an immediately-invoked async function
async function start() {
// Update the gRPC client and worker to use a local address and port
const daprHost = "localhost";
const daprPort = "50001";
const workflowClient = new DaprWorkflowClient({
daprHost,
daprPort,
});
const workflowRuntime = new WorkflowRuntime({
daprHost,
daprPort,
});
function getRandomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
async function getWorkItemsActivity(_: WorkflowActivityContext): Promise<string[]> {
const count: number = getRandomInt(2, 10);
console.log(`generating ${count} work items...`);
const workItems: string[] = Array.from({ length: count }, (_, i) => `work item ${i}`);
return workItems;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function processWorkItemActivity(context: WorkflowActivityContext, item: string): Promise<number> {
console.log(`processing work item: ${item}`);
// Simulate some work that takes a variable amount of time
const sleepTime = Math.random() * 5000;
await sleep(sleepTime);
// Return a result for the given work item, which is also a random number in this case
// For more information about random numbers in workflow please check
// https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-code-constraints?tabpane=csharp#random-numbers
return Math.floor(Math.random() * 11);
}
const workflow: TWorkflow = async function* (ctx: WorkflowContext): any {
const tasks: Task<any>[] = [];
const workItems = yield ctx.callActivity(getWorkItemsActivity);
for (const workItem of workItems) {
tasks.push(ctx.callActivity(processWorkItemActivity, workItem));
}
const results: number[] = yield ctx.whenAll(tasks);
const sum: number = results.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
return sum;
};
workflowRuntime.registerWorkflow(workflow);
workflowRuntime.registerActivity(getWorkItemsActivity);
workflowRuntime.registerActivity(processWorkItemActivity);
// Wrap the worker startup in a try-catch block to handle any errors during startup
try {
await workflowRuntime.start();
console.log("Worker started successfully");
} catch (error) {
console.error("Error starting worker:", error);
}
// Schedule a new orchestration
try {
const id = await workflowClient.scheduleNewWorkflow(workflow);
console.log(`Orchestration scheduled with ID: ${id}`);
// Wait for orchestration completion
const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30);
console.log(`Orchestration completed! Result: ${state?.serializedOutput}`);
} catch (error) {
console.error("Error scheduling or waiting for orchestration:", error);
}
// stop worker and client
await workflowRuntime.stop();
await workflowClient.stop();
// stop the dapr side car
process.exit(0);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
// Get a list of N work items to process in parallel.
object[] workBatch = await context.CallActivityAsync<object[]>("GetWorkBatch", null);
// Schedule the parallel tasks, but don't wait for them to complete yet.
var parallelTasks = new List<Task<int>>(workBatch.Length);
for (int i = 0; i < workBatch.Length; i++)
{
Task<int> task = context.CallActivityAsync<int>("ProcessWorkItem", workBatch[i]);
parallelTasks.Add(task);
}
// Everything is scheduled. Wait here until all parallel tasks have completed.
await Task.WhenAll(parallelTasks);
// Aggregate all N outputs and publish the result.
int sum = parallelTasks.Sum(t => t.Result);
await context.CallActivityAsync("PostResults", sum);
public class FaninoutWorkflow extends Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
// Get a list of N work items to process in parallel.
Object[] workBatch = ctx.callActivity("GetWorkBatch", Object[].class).await();
// Schedule the parallel tasks, but don't wait for them to complete yet.
List<Task<Integer>> tasks = Arrays.stream(workBatch)
.map(workItem -> ctx.callActivity("ProcessWorkItem", workItem, int.class))
.collect(Collectors.toList());
// Everything is scheduled. Wait here until all parallel tasks have completed.
List<Integer> results = ctx.allOf(tasks).await();
// Aggregate all N outputs and publish the result.
int sum = results.stream().mapToInt(Integer::intValue).sum();
ctx.complete(sum);
};
}
}
func BatchProcessingWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return 0, err
}
var workBatch []int
if err := ctx.CallActivity(GetWorkBatch, workflow.ActivityInput(input)).Await(&workBatch); err != nil {
return 0, err
}
parallelTasks := workflow.NewTaskSlice(len(workBatch))
for i, workItem := range workBatch {
parallelTasks[i] = ctx.CallActivity(ProcessWorkItem, workflow.ActivityInput(workItem))
}
var outputs int
for _, task := range parallelTasks {
var output int
err := task.Await(&output)
if err == nil {
outputs += output
} else {
return 0, err
}
}
if err := ctx.CallActivity(ProcessResults, workflow.ActivityInput(outputs)).Await(nil); err != nil {
return 0, err
}
return 0, nil
}
func GetWorkBatch(ctx workflow.ActivityContext) (any, error) {
var batchSize int
if err := ctx.GetInput(&batchSize); err != nil {
return 0, err
}
batch := make([]int, batchSize)
for i := 0; i < batchSize; i++ {
batch[i] = i
}
return batch, nil
}
func ProcessWorkItem(ctx workflow.ActivityContext) (any, error) {
var workItem int
if err := ctx.GetInput(&workItem); err != nil {
return 0, err
}
fmt.Printf("Processing work item: %d\n", workItem)
time.Sleep(time.Second * 5)
result := workItem * 2
fmt.Printf("Work item %d processed. Result: %d\n", workItem, result)
return result, nil
}
func ProcessResults(ctx workflow.ActivityContext) (any, error) {
var finalResult int
if err := ctx.GetInput(&finalResult); err != nil {
return 0, err
}
fmt.Printf("Final result: %d\n", finalResult)
return finalResult, nil
}
此示例的关键要点是:
此外,工作流的执行是持久的。如果一个工作流启动 100 个并行任务执行,并且在进程崩溃之前只完成了 40 个,工作流会自动重新启动,并且只安排剩余的 60 个任务。
您可以进一步使用简单的特定语言构造来限制并发度。下面的示例代码说明了如何将扇出度限制为仅 5 个并发活动执行:
//Revisiting the earlier example...
// Get a list of N work items to process in parallel.
object[] workBatch = await context.CallActivityAsync<object[]>("GetWorkBatch", null);
const int MaxParallelism = 5;
var results = new List<int>();
var inFlightTasks = new HashSet<Task<int>>();
foreach(var workItem in workBatch)
{
if (inFlightTasks.Count >= MaxParallelism)
{
var finishedTask = await Task.WhenAny(inFlightTasks);
results.Add(finishedTask.Result);
inFlightTasks.Remove(finishedTask);
}
inFlightTasks.Add(context.CallActivityAsync<int>("ProcessWorkItem", workItem));
}
results.AddRange(await Task.WhenAll(inFlightTasks));
var sum = results.Sum(t => t);
await context.CallActivityAsync("PostResults", sum);
您可以通过使用 WorkflowContext 上的以下扩展方法来并行处理工作流活动,同时对并发性设置上限:
//Revisiting the earlier example...
// Get a list of work items to process
var workBatch = await context.CallActivityAsync<object[]>("GetWorkBatch", null);
// Process deterministically in parallel with an upper cap of 5 activities at a time
var results = await context.ProcessInParallelAsync(workBatch, workItem => context.CallActivityAsync<int>("ProcessWorkItem", workItem), maxConcurrency: 5);
var sum = results.Sum(t => t);
await context.CallActivityAsync("PostResults", sum);
以这种方式限制并发度对于限制对共享资源的争用非常有用。例如,如果活动需要调用具有自己并发限制的外部资源(如数据库或外部 API),确保不超过指定数量的活动同时调用该资源会很有用。
异步 HTTP API 通常使用异步请求-回复模式来实现。传统实现此模式涉及以下内容:
端到端流程如下图所示。

实现异步请求-回复模式的挑战在于它涉及使用多个 API 和状态存储。它还涉及正确实现协议,以便客户端知道如何自动轮询状态并了解操作何时完成。
Dapr 工作流 HTTP API 开箱即用地支持异步请求-回复模式,无需您编写任何代码或进行任何状态管理。
以下 curl 命令说明了工作流 API 如何支持此模式。
curl -X POST http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 -d '{"Name":"Paperclips","Quantity":1,"TotalCost":9.95}'
前面的命令将产生以下响应 JSON:
{"instanceID":"12345678"}
然后,HTTP 客户端可以使用工作流实例 ID 构造状态查询 URL,并重复轮询它,直到在有效负载中看到"COMPLETE"、“FAILURE"或"TERMINATED"状态。
curl http://localhost:3500/v1.0/workflows/dapr/12345678
以下是正在进行中的工作流状态可能看起来像的示例。
{
"instanceID": "12345678",
"workflowName": "OrderProcessingWorkflow",
"createdAt": "2023-05-03T23:22:11.143069826Z",
"lastUpdatedAt": "2023-05-03T23:22:22.460025267Z",
"runtimeStatus": "RUNNING",
"properties": {
"dapr.workflow.custom_status": "",
"dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":9.95}"
}
}
从上一个示例中可以看出,工作流的运行时状态是 RUNNING,这让客户端知道应该继续轮询。
如果工作流已完成,状态可能如下所示。
{
"instanceID": "12345678",
"workflowName": "OrderProcessingWorkflow",
"createdAt": "2023-05-03T23:30:11.381146313Z",
"lastUpdatedAt": "2023-05-03T23:30:52.923870615Z",
"runtimeStatus": "COMPLETED",
"properties": {
"dapr.workflow.custom_status": "",
"dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":9.95}",
"dapr.workflow.output": "{\"Processed\":true}"
}
}
从上一个示例中可以看出,工作流的运行时状态现在是 COMPLETED,这意味着客户端可以停止轮询更新。
监视器模式是一个循环过程,通常:
下图大致说明了此模式。

根据业务需求,可能只有一个监视器,也可能有多个监视器,每个业务实体一个(例如,股票)。此外,根据情况,休眠的时间可能需要更改。这些要求使得使用基于 cron 的调度系统变得不切实际。
Dapr 工作流通过允许您实现_永久工作流_来原生支持此模式。无需编写无限 while 循环(这是一种反模式),Dapr 工作流公开了一个 continue-as-new API,工作流作者可以使用它从头开始使用新输入重新启动工作流函数。
from dataclasses import dataclass
from datetime import timedelta
import random
import dapr.ext.workflow as wf
@dataclass
class JobStatus:
job_id: str
is_healthy: bool
def status_monitor_workflow(ctx: wf.DaprWorkflowContext, job: JobStatus):
# poll a status endpoint associated with this job
status = yield ctx.call_activity(check_status, input=job)
if not ctx.is_replaying:
print(f"Job '{job.job_id}' is {status}.")
if status == "healthy":
job.is_healthy = True
next_sleep_interval = 60 # check less frequently when healthy
else:
if job.is_healthy:
job.is_healthy = False
ctx.call_activity(send_alert, input=f"Job '{job.job_id}' is unhealthy!")
next_sleep_interval = 5 # check more frequently when unhealthy
yield ctx.create_timer(fire_at=ctx.current_utc_datetime + timedelta(minutes=next_sleep_interval))
# restart from the beginning with a new JobStatus input
ctx.continue_as_new(job)
def check_status(ctx, _) -> str:
return random.choice(["healthy", "unhealthy"])
def send_alert(ctx, message: str):
print(f'*** Alert: {message}')
const statusMonitorWorkflow: TWorkflow = async function* (ctx: WorkflowContext): any {
let duration;
const status = yield ctx.callActivity(checkStatusActivity);
if (status === "healthy") {
// Check less frequently when in a healthy state
// set duration to 1 hour
duration = 60 * 60;
} else {
yield ctx.callActivity(alertActivity, "job unhealthy");
// Check more frequently when in an unhealthy state
// set duration to 5 minutes
duration = 5 * 60;
}
// Put the workflow to sleep until the determined time
ctx.createTimer(duration);
// Restart from the beginning with the updated state
ctx.continueAsNew();
};
public override async Task<object> RunAsync(WorkflowContext context, MyEntityState myEntityState)
{
TimeSpan nextSleepInterval;
var status = await context.CallActivityAsync<string>("GetStatus");
if (status == "healthy")
{
myEntityState.IsHealthy = true;
// Check less frequently when in a healthy state
nextSleepInterval = TimeSpan.FromMinutes(60);
}
else
{
if (myEntityState.IsHealthy)
{
myEntityState.IsHealthy = false;
await context.CallActivityAsync("SendAlert", myEntityState);
}
// Check more frequently when in an unhealthy state
nextSleepInterval = TimeSpan.FromMinutes(5);
}
// Put the workflow to sleep until the determined time
await context.CreateTimer(nextSleepInterval);
// Restart from the beginning with the updated state
context.ContinueAsNew(myEntityState);
return null;
}
此示例假设您有一个预定义的
MyEntityState类,其中包含布尔IsHealthy属性。
public class MonitorWorkflow extends Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
Duration nextSleepInterval;
var status = ctx.callActivity(DemoWorkflowStatusActivity.class.getName(), DemoStatusActivityOutput.class).await();
var isHealthy = status.getIsHealthy();
if (isHealthy) {
// Check less frequently when in a healthy state
nextSleepInterval = Duration.ofMinutes(60);
} else {
ctx.callActivity(DemoWorkflowAlertActivity.class.getName()).await();
// Check more frequently when in an unhealthy state
nextSleepInterval = Duration.ofMinutes(5);
}
// Put the workflow to sleep until the determined time
try {
ctx.createTimer(nextSleepInterval);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// Restart from the beginning with the updated state
ctx.continueAsNew();
}
}
}
type JobStatus struct {
JobID string `json:"job_id"`
IsHealthy bool `json:"is_healthy"`
}
func StatusMonitorWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var sleepInterval time.Duration
var job JobStatus
if err := ctx.GetInput(&job); err != nil {
return "", err
}
var status string
if err := ctx.CallActivity(CheckStatus, workflow.ActivityInput(job)).Await(&status); err != nil {
return "", err
}
if status == "healthy" {
job.IsHealthy = true
sleepInterval = time.Minutes * 60
} else {
if job.IsHealthy {
job.IsHealthy = false
err := ctx.CallActivity(SendAlert, workflow.ActivityInput(fmt.Sprintf("Job '%s' is unhealthy!", job.JobID))).Await(nil)
if err != nil {
return "", err
}
}
sleepInterval = time.Minutes * 5
}
if err := ctx.CreateTimer(sleepInterval).Await(nil); err != nil {
return "", err
}
ctx.ContinueAsNew(job, false)
return "", nil
}
func CheckStatus(ctx workflow.ActivityContext) (any, error) {
statuses := []string{"healthy", "unhealthy"}
return statuses[rand.Intn(1)], nil
}
func SendAlert(ctx workflow.ActivityContext) (any, error) {
var message string
if err := ctx.GetInput(&message); err != nil {
return "", err
}
fmt.Printf("*** Alert: %s", message)
return "", nil
}
实现监视器模式的工作流可以永远循环,也可以通过不调用 continue-as-new 来优雅地终止自己。
在某些情况下,工作流可能需要暂停并等待外部系统执行某些操作。例如,工作流可能需要暂停并等待收到付款。在这种情况下,支付系统可能会在收到付款时向发布/订阅主题发布事件,并且该主题上的侦听器可以使用引发事件工作流 API向工作流引发事件。
另一个非常常见的场景是工作流需要暂停并等待人员,例如在批准采购订单时。Dapr 工作流通过外部事件功能支持此事件模式。
以下是涉及人员的采购订单的工作流示例:
下图说明了此流程。

以下示例代码显示了如何使用 Dapr 工作流实现此模式。
from dataclasses import dataclass
from datetime import timedelta
import dapr.ext.workflow as wf
@dataclass
class Order:
cost: float
product: str
quantity: int
def __str__(self):
return f'{self.product} ({self.quantity})'
@dataclass
class Approval:
approver: str
@staticmethod
def from_dict(dict):
return Approval(**dict)
def purchase_order_workflow(ctx: wf.DaprWorkflowContext, order: Order):
# Orders under $1000 are auto-approved
if order.cost < 1000:
return "Auto-approved"
# Orders of $1000 or more require manager approval
yield ctx.call_activity(send_approval_request, input=order)
# Approvals must be received within 24 hours or they will be canceled.
approval_event = ctx.wait_for_external_event("approval_received")
timeout_event = ctx.create_timer(timedelta(hours=24))
winner = yield wf.when_any([approval_event, timeout_event])
if winner == timeout_event:
return "Cancelled"
# The order was approved
yield ctx.call_activity(place_order, input=order)
approval_details = Approval.from_dict(approval_event.get_result())
return f"Approved by '{approval_details.approver}'"
def send_approval_request(_, order: Order) -> None:
print(f'*** Sending approval request for order: {order}')
def place_order(_, order: Order) -> None:
print(f'*** Placing order: {order}')
import {
Task,
DaprWorkflowClient,
WorkflowActivityContext,
WorkflowContext,
WorkflowRuntime,
TWorkflow,
} from "@dapr/dapr";
import * as readlineSync from "readline-sync";
// Wrap the entire code in an immediately-invoked async function
async function start() {
class Order {
cost: number;
product: string;
quantity: number;
constructor(cost: number, product: string, quantity: number) {
this.cost = cost;
this.product = product;
this.quantity = quantity;
}
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Update the gRPC client and worker to use a local address and port
const daprHost = "localhost";
const daprPort = "50001";
const workflowClient = new DaprWorkflowClient({
daprHost,
daprPort,
});
const workflowRuntime = new WorkflowRuntime({
daprHost,
daprPort,
});
// Activity function that sends an approval request to the manager
const sendApprovalRequest = async (_: WorkflowActivityContext, order: Order) => {
// Simulate some work that takes an amount of time
await sleep(3000);
console.log(`Sending approval request for order: ${order.product}`);
};
// Activity function that places an order
const placeOrder = async (_: WorkflowActivityContext, order: Order) => {
console.log(`Placing order: ${order.product}`);
};
// Orchestrator function that represents a purchase order workflow
const purchaseOrderWorkflow: TWorkflow = async function* (ctx: WorkflowContext, order: Order): any {
// Orders under $1000 are auto-approved
if (order.cost < 1000) {
return "Auto-approved";
}
// Orders of $1000 or more require manager approval
yield ctx.callActivity(sendApprovalRequest, order);
// Approvals must be received within 24 hours or they will be cancled.
const tasks: Task<any>[] = [];
const approvalEvent = ctx.waitForExternalEvent("approval_received");
const timeoutEvent = ctx.createTimer(24 * 60 * 60);
tasks.push(approvalEvent);
tasks.push(timeoutEvent);
const winner = ctx.whenAny(tasks);
if (winner == timeoutEvent) {
return "Cancelled";
}
yield ctx.callActivity(placeOrder, order);
const approvalDetails = approvalEvent.getResult();
return `Approved by ${approvalDetails.approver}`;
};
workflowRuntime
.registerWorkflow(purchaseOrderWorkflow)
.registerActivity(sendApprovalRequest)
.registerActivity(placeOrder);
// Wrap the worker startup in a try-catch block to handle any errors during startup
try {
await workflowRuntime.start();
console.log("Worker started successfully");
} catch (error) {
console.error("Error starting worker:", error);
}
// Schedule a new orchestration
try {
const cost = readlineSync.questionInt("Cost of your order:");
const approver = readlineSync.question("Approver of your order:");
const timeout = readlineSync.questionInt("Timeout for your order in seconds:");
const order = new Order(cost, "MyProduct", 1);
const id = await workflowClient.scheduleNewWorkflow(purchaseOrderWorkflow, order);
console.log(`Orchestration scheduled with ID: ${id}`);
// prompt for approval asynchronously
promptForApproval(approver, workflowClient, id);
// Wait for orchestration completion
const state = await workflowClient.waitForWorkflowCompletion(id, undefined, timeout + 2);
console.log(`Orchestration completed! Result: ${state?.serializedOutput}`);
} catch (error) {
console.error("Error scheduling or waiting for orchestration:", error);
}
// stop worker and client
await workflowRuntime.stop();
await workflowClient.stop();
// stop the dapr side car
process.exit(0);
}
async function promptForApproval(approver: string, workflowClient: DaprWorkflowClient, id: string) {
if (readlineSync.keyInYN("Press [Y] to approve the order... Y/yes, N/no")) {
const approvalEvent = { approver: approver };
await workflowClient.raiseEvent(id, "approval_received", approvalEvent);
} else {
return "Order rejected";
}
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order)
{
// ...(other steps)...
// Require orders over a certain threshold to be approved
if (order.TotalCost > OrderApprovalThreshold)
{
try
{
// Request human approval for this order
await context.CallActivityAsync(nameof(RequestApprovalActivity), order);
// Pause and wait for a human to approve the order
ApprovalResult approvalResult = await context.WaitForExternalEventAsync<ApprovalResult>(
eventName: "ManagerApproval",
timeout: TimeSpan.FromDays(3));
if (approvalResult == ApprovalResult.Rejected)
{
// The order was rejected, end the workflow here
return new OrderResult(Processed: false);
}
}
catch (TaskCanceledException)
{
// An approval timeout results in automatic order cancellation
return new OrderResult(Processed: false);
}
}
// ...(other steps)...
// End the workflow with a success result
return new OrderResult(Processed: true);
}
Note 在上面的示例中,
RequestApprovalActivity是要调用的工作流活动的名称,而ApprovalResult是由工作流应用定义的枚举。为简洁起见,这些定义未包含在示例代码中。
public class ExternalSystemInteractionWorkflow extends Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
// ...other steps...
Integer orderCost = ctx.getInput(int.class);
// Require orders over a certain threshold to be approved
if (orderCost > ORDER_APPROVAL_THRESHOLD) {
try {
// Request human approval for this order
ctx.callActivity("RequestApprovalActivity", orderCost, Void.class).await();
// Pause and wait for a human to approve the order
boolean approved = ctx.waitForExternalEvent("ManagerApproval", Duration.ofDays(3), boolean.class).await();
if (!approved) {
// The order was rejected, end the workflow here
ctx.complete("Process reject");
}
} catch (TaskCanceledException e) {
// An approval timeout results in automatic order cancellation
ctx.complete("Process cancel");
}
}
// ...other steps...
// End the workflow with a success result
ctx.complete("Process approved");
};
}
}
type Order struct {
Cost float64 `json:"cost"`
Product string `json:"product"`
Quantity int `json:"quantity"`
}
type Approval struct {
Approver string `json:"approver"`
}
func PurchaseOrderWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var order Order
if err := ctx.GetInput(&order); err != nil {
return "", err
}
// Orders under $1000 are auto-approved
if order.Cost < 1000 {
return "Auto-approved", nil
}
// Orders of $1000 or more require manager approval
if err := ctx.CallActivity(SendApprovalRequest, workflow.ActivityInput(order)).Await(nil); err != nil {
return "", err
}
// Approvals must be received within 24 hours or they will be cancelled
var approval Approval
if err := ctx.WaitForExternalEvent("approval_received", time.Hour*24).Await(&approval); err != nil {
// Assuming that a timeout has taken place - in any case; an error.
return "error/cancelled", err
}
// The order was approved
if err := ctx.CallActivity(PlaceOrder, workflow.ActivityInput(order)).Await(nil); err != nil {
return "", err
}
return fmt.Sprintf("Approved by %s", approval.Approver), nil
}
func SendApprovalRequest(ctx workflow.ActivityContext) (any, error) {
var order Order
if err := ctx.GetInput(&order); err != nil {
return "", err
}
fmt.Printf("*** Sending approval request for order: %v\n", order)
return "", nil
}
func PlaceOrder(ctx workflow.ActivityContext) (any, error) {
var order Order
if err := ctx.GetInput(&order); err != nil {
return "", err
}
fmt.Printf("*** Placing order: %v", order)
return "", nil
}
向等待的工作流实例传递事件以恢复工作流执行的代码在工作流外部。可以使用引发事件工作流管理 API 将工作流事件传递给等待的工作流实例,如以下示例所示:
from dapr.clients import DaprClient
from dataclasses import asdict
with DaprClient() as d:
d.raise_workflow_event(
instance_id=instance_id,
workflow_component="dapr",
event_name="approval_received",
event_data=asdict(Approval("Jane Doe")))
import { DaprClient } from "@dapr/dapr";
public async raiseEvent(workflowInstanceId: string, eventName: string, eventPayload?: any) {
this._innerClient.raiseOrchestrationEvent(workflowInstanceId, eventName, eventPayload);
}
// Raise the workflow event to the waiting workflow
await daprClient.RaiseWorkflowEventAsync(
instanceId: orderId,
workflowComponent: "dapr",
eventName: "ManagerApproval",
eventData: ApprovalResult.Approved);
System.out.println("**SendExternalMessage: RestartEvent**");
client.raiseEvent(restartingInstanceId, "RestartEvent", "RestartEventPayload");
func raiseEvent() {
daprClient, err := client.NewClient()
if err != nil {
log.Fatalf("failed to initialize the client")
}
err = daprClient.RaiseEventWorkflow(context.Background(), &client.RaiseEventWorkflowRequest{
InstanceID: "instance_id",
WorkflowComponent: "dapr",
EventName: "approval_received",
EventData: Approval{
Approver: "Jane Doe",
},
})
if err != nil {
log.Fatalf("failed to raise event on workflow")
}
log.Println("raised an event on specified workflow")
}
外部事件不必直接由人员触发。它们也可以由其他系统触发。例如,工作流可能需要暂停并等待收到付款。在这种情况下,支付系统可能会在收到付款时向发布/订阅主题发布事件,并且该主题上的侦听器可以使用引发事件工作流 API 向工作流引发事件。
补偿模式(也称为 Saga 模式)提供了一种在工作流中途失败时回滚或撤销已执行操作的机制。此模式对于跨越多个微服务且传统数据库事务不可行的长时间运行的工作流特别重要。
在分布式微服务架构中,您通常需要跨多个服务协调操作。当这些操作无法包含在单个事务中时,补偿模式通过为工作流中的每个步骤定义补偿操作来提供一种保持一致性的方法。
补偿模式解决了几个关键挑战:
补偿模式的常见用例包括:
Dapr 工作流提供对补偿模式的支持,允许您为每个步骤注册补偿活动,并在需要时以相反的顺序执行它们。
以下是电子商务流程的工作流示例:
下图说明了此流程。

public class PaymentProcessingWorkflow implements Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
ctx.getLogger().info("Starting Workflow: " + ctx.getName());
var orderId = ctx.getInput(String.class);
List<String> compensations = new ArrayList<>();
try {
// Step 1: Reserve inventory
String reservationId = ctx.callActivity(ReserveInventoryActivity.class.getName(), orderId, String.class).await();
ctx.getLogger().info("Inventory reserved: {}", reservationId);
compensations.add("ReleaseInventory");
// Step 2: Process payment
String paymentId = ctx.callActivity(ProcessPaymentActivity.class.getName(), orderId, String.class).await();
ctx.getLogger().info("Payment processed: {}", paymentId);
compensations.add("RefundPayment");
// Step 3: Ship order
String shipmentId = ctx.callActivity(ShipOrderActivity.class.getName(), orderId, String.class).await();
ctx.getLogger().info("Order shipped: {}", shipmentId);
compensations.add("CancelShipment");
} catch (TaskFailedException e) {
ctx.getLogger().error("Activity failed: {}", e.getMessage());
// Execute compensations in reverse order
Collections.reverse(compensations);
for (String compensation : compensations) {
try {
switch (compensation) {
case "CancelShipment":
String shipmentCancelResult = ctx.callActivity(
CancelShipmentActivity.class.getName(),
orderId,
String.class).await();
ctx.getLogger().info("Shipment cancellation completed: {}", shipmentCancelResult);
break;
case "RefundPayment":
String refundResult = ctx.callActivity(
RefundPaymentActivity.class.getName(),
orderId,
String.class).await();
ctx.getLogger().info("Payment refund completed: {}", refundResult);
break;
case "ReleaseInventory":
String releaseResult = ctx.callActivity(
ReleaseInventoryActivity.class.getName(),
orderId,
String.class).await();
ctx.getLogger().info("Inventory release completed: {}", releaseResult);
break;
}
} catch (TaskFailedException ex) {
ctx.getLogger().error("Compensation activity failed: {}", ex.getMessage());
}
}
ctx.complete("Order processing failed, compensation applied");
}
// Step 4: Send confirmation
ctx.callActivity(SendConfirmationActivity.class.getName(), orderId, Void.class).await();
ctx.getLogger().info("Confirmation sent for order: {}", orderId);
ctx.complete("Order processed successfully: " + orderId);
};
}
}
// Example activities
class ReserveInventoryActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String orderId = ctx.getInput(String.class);
// Logic to reserve inventory
String reservationId = "reservation_" + orderId;
System.out.println("Reserved inventory for order: " + orderId);
return reservationId;
}
}
class ReleaseInventoryActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String reservationId = ctx.getInput(String.class);
// Logic to release inventory reservation
System.out.println("Released inventory reservation: " + reservationId);
return "Released: " + reservationId;
}
}
class ProcessPaymentActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String orderId = ctx.getInput(String.class);
// Logic to process payment
String paymentId = "payment_" + orderId;
System.out.println("Processed payment for order: " + orderId);
return paymentId;
}
}
class RefundPaymentActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String paymentId = ctx.getInput(String.class);
// Logic to refund payment
System.out.println("Refunded payment: " + paymentId);
return "Refunded: " + paymentId;
}
}
class ShipOrderActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String orderId = ctx.getInput(String.class);
// Logic to ship order
String shipmentId = "shipment_" + orderId;
System.out.println("Shipped order: " + orderId);
return shipmentId;
}
}
class CancelShipmentActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String shipmentId = ctx.getInput(String.class);
// Logic to cancel shipment
System.out.println("Canceled shipment: " + shipmentId);
return "Canceled: " + shipmentId;
}
}
class SendConfirmationActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String orderId = ctx.getInput(String.class);
// Logic to send confirmation
System.out.println("Sent confirmation for order: " + orderId);
return null;
}
}
使用 Dapr 工作流补偿模式的主要好处包括:
补偿模式确保您的分布式工作流可以保持一致性并从故障中优雅地恢复,使其成为构建可靠微服务架构的重要工具。
Dapr 工作流允许开发者使用多种编程语言的普通代码来定义工作流。工作流引擎运行在 Dapr 边车内部,协调作为应用程序一部分部署的工作流代码。Dapr 工作流构建在 Dapr Actor 之上,为工作流执行提供持久性和可扩展性。
本文介绍:
有关如何在应用程序中编写 Dapr 工作流的更多信息,请参阅如何:编写工作流。
Dapr 工作流引擎内部由 Dapr 的 actor 运行时驱动。下图展示了 Kubernetes 模式下的 Dapr 工作流架构:

要使用 Dapr 工作流构建块,您需要使用 Dapr Workflow SDK 在应用程序中编写工作流代码,SDK 内部使用 gRPC 流连接到边车。这会注册工作流以及任何工作流活动,或工作流可以调度的任务。
引擎直接嵌入到边车中,并使用 durabletask-go 框架库实现。该框架允许您交换不同的存储提供程序,包括为 Dapr 创建的存储提供程序,它在幕后利用内部 actor。由于 Dapr 工作流使用 actor,您可以将工作流状态存储在状态存储中。
当工作流应用程序启动时,它使用工作流编写 SDK 向 Dapr 边车发送 gRPC 请求,并获取工作流工作项的流,遵循服务器流式 RPC 模式。这些工作项可以是任何内容,从"启动新的 X 工作流"(其中 X 是工作流的类型)到"代表工作流 X 调度具有输入 Z 的活动 Y"。
工作流应用程序执行适当的工作流代码,然后向边车发送 gRPC 请求,其中包含执行结果。

所有交互都通过单个 gRPC 通道进行,并由应用程序发起,这意味着应用程序不需要打开任何入站端口。 这些交互的细节由特定语言的 Dapr 工作流编写 SDK 内部处理。
如果您熟悉 Dapr actor,您可能会注意到,与应用程序定义的 actor 相比,工作流的边车交互方式有一些差异。
| Actor | 工作流 |
|---|---|
| 应用程序创建的 actor 可以使用 HTTP 或 gRPC 与边车交互。 | 工作流仅使用 gRPC。由于工作流 gRPC 协议的复杂性,实现工作流时_必须_使用 SDK。 |
| Actor 操作从边车推送到应用程序代码。这要求应用程序监听特定的_应用端口_。 | 对于工作流,操作由应用程序使用流式协议从边车_拉取_。应用程序不需要监听任何端口即可运行工作流。 |
| Actor 向边车显式注册自己。 | 工作流不向边车注册自己。嵌入式引擎不跟踪工作流类型。此职责委托给工作流应用程序及其 SDK。 |
工作流引擎使用的 durabletask-go 核心使用 Open Telemetry SDK 编写分布式跟踪。
这些跟踪由 Dapr 边车自动捕获,并导出到配置的 Open Telemetry 提供程序,例如 Zipkin。
引擎管理的每个工作流实例表示为一个或多个 span。 有一个表示完整工作流执行的父 span,以及各种任务的子 span,包括活动任务执行和持久化计时器的 span。
工作流活动代码当前_不能_访问跟踪上下文。
当工作流客户端连接到边车时,会注册两种类型的 actor 以支持工作流引擎:
dapr.internal.{namespace}.{appID}.workflowdapr.internal.{namespace}.{appID}.activity{namespace} 值是 Dapr 命名空间,如果未配置命名空间,则默认为 default。
{appID} 值是应用程序的 ID。
例如,如果您有一个名为"wfapp"的工作流应用程序,则工作流 actor 的类型为 dapr.internal.default.wfapp.workflow,活动 actor 的类型为 dapr.internal.default.wfapp.activity。
下图演示了工作流 actor 在 Kubernetes 场景中如何运行:

与用户定义的 actor 一样,工作流 actor 通过 actor 放置服务提供的哈希查找表分布在集群中。 它们还维护自己的状态并使用提醒。 但是,与应用程序代码中的 actor 不同,这些工作流 actor 嵌入到 Dapr 边车中。 应用程序代码完全不知道这些 actor 的存在。
工作流 actor 类型仅在应用程序使用 Dapr Workflow SDK 注册工作流后才注册。 如果应用程序从未注册工作流,则永远不会注册内部工作流 actor。
工作流使用两种不同类型的 actor:工作流 actor 和活动 actor。 工作流 actor 负责管理应用程序中运行的所有工作流的状态和位置。 为每个被调度的工作流实例激活一个新的工作流 actor 实例。 工作流 actor 的 ID 是工作流的 ID。 此工作流 actor 在工作流进行时存储工作流的状态,并通过 actor 查找表确定工作流代码在哪个节点上执行。
由于工作流基于 actor,所有工作流和活动工作在实现工作流的应用程序的所有副本之间随机分布。 工作流启动的位置与每个工作项执行的位置之间没有局部性或关系。
每个工作流 actor 使用配置的 actor 状态存储中的以下键保存其状态:
| 键 | 描述 |
|---|---|
inbox-NNNNNN | 工作流的收件箱实际上是驱动工作流执行的_消息_的 FIFO 队列。示例消息包括工作流创建消息、活动任务完成消息等。每条消息作为单独的键存储在状态存储中,名称为 inbox-NNNNNN,其中 NNNNNN 是一个 6 位数字,表示消息的顺序。一旦工作流消耗了相应的消息,这些状态键就会被删除。 |
history-NNNNNN | 工作流的历史是表示工作流执行历史的事件的有序列表。历史中的每个键保存单个历史事件的数据。像仅追加日志一样,工作流历史事件只添加,从不删除(除非工作流执行"继续作为新"操作,这会清除所有历史记录并使用新输入重新启动工作流)。 |
customStatus | 包含用户定义的工作流状态值。每个工作流 actor 实例恰好有一个 customStatus 键。 |
metadata | 包含有关工作流的元信息,作为 JSON blob,包括收件箱长度、历史记录长度以及表示工作流生成的 64 位整数(用于实例 ID 被重用的情况)。长度信息用于确定在加载或保存工作流状态更新时需要读取或写入哪些键。 |
工作流 actor 状态在工作流完成后仍保留在状态存储中。
创建大量工作流可能导致无限制的存储使用。 要解决此问题,可以使用工作流的 ID 清除工作流或直接删除工作流数据库存储中的条目。
下图说明了工作流 actor 的典型生命周期。

总结:
活动 actor 负责管理所有工作流活动调用的状态和位置。
为工作流调度的每个活动任务激活一个新的活动 actor 实例。
活动 actor 的 ID 是工作流的 ID 加上序列号(序列号从 0 开始)以及"生成"(在使用 continue as new 重新运行的实例期间递增)的组合。
例如,如果工作流的 ID 为 876bf371,并且是工作流调度的第三个活动,其 ID 将为 876bf371::2::1,其中 2 是序列号,1 是生成。
如果活动在 continue as new 后再次被调度,ID 将为 876bf371::2::2。
活动 actor 不存储任何状态,而是将所有结果数据发送回父工作流 actor。
下图说明了活动 actor 的典型生命周期。

活动 actor 是短寿命的:
Dapr 工作流通过使用 actor 提醒来确保工作流容错性,以从瞬态系统故障中恢复。 在调用应用程序工作流代码之前,工作流或活动 actor 将创建一个新的提醒。 这些提醒是"一次性"的,意味着它们将在成功触发后过期。 如果应用程序代码无中断地执行,提醒将被触发并过期。 但是,如果托管相关工作流或活动的节点或边车崩溃,提醒将重新激活相应的 actor,并且将永远重试执行。

Dapr 工作流内部使用 actor 来驱动工作流的执行。
与任何 actor 一样,这些工作流 actor 将其状态存储在配置的 actor 状态存储中。
这是通过在 Dapr 配置中指定状态存储组件,然后在配置的 actors 部分的 actorStateStore 属性中引用该状态存储来完成的。
阅读状态 API 参考和 actor API 参考以了解更多关于 actor 状态存储的信息。
如工作流 actor部分所述,工作流通过附加到历史日志来增量保存其状态。 工作流的历史日志分布在多个状态存储键中,以便每个"检查点"仅需要附加最新的条目。
每个检查点的大小由工作流在进入空闲状态之前调度的并发操作数确定。 顺序工作流因此会对状态存储进行较小的批量更新,而扇出/扇入工作流将需要更大的批量。 批量大小也受工作流调用活动或子工作流时输入和输出大小的影响。

不同的状态存储实现可能会隐式地限制您可以编写的工作流类型。 例如,Azure Cosmos DB 状态存储将项目大小限制为 2 MB 的 UTF-8 编码 JSON(来源)。 活动或子工作流的输入或输出负载作为单个记录存储在状态存储中,因此 2 MB 的项目限制意味着工作流和活动的输入和输出不能超过 2 MB 的 JSON 序列化数据。
同样,如果状态存储对批量事务的大小施加限制,这可能会限制工作流可以调度的并行操作数量。
可以从状态存储中清除工作流状态,包括其所有历史记录。 每个 Dapr SDK 都公开了用于清除特定工作流实例的所有元数据的 API。
每次工作流运行在状态存储中保存为历史记录的记录数由其复杂性或"形状"决定。换句话说,即活动、计时器、子工作流等的数量。 下表显示了不同工作流任务保存的记录数的一般指南。 根据重试或并发性,此数字可能更大或更小。
| 任务类型 | 保存的记录数 |
|---|---|
| 启动工作流 | 5 条记录 |
| 调用活动 | 3 条记录 |
| 计时器 | 3 条记录 |
| 触发事件 | 3 条记录 |
| 启动子工作流 | 8 条记录 |
dapr workflow --app-id myapp list
dapr workflow --app-id myapp history <instance-id>
工作流引擎支持以下状态存储:
由于 Dapr 工作流内部使用 actor 实现,Dapr 工作流具有与 actor 相同的可扩展性特征。 放置服务:
工作流的预期可扩展性由以下因素决定:
目标应用程序中工作流代码的实现细节也在单个工作流实例的可扩展性中发挥作用。 每个工作流实例一次在单个节点上执行,但工作流可以调度在其他节点上运行的活动和子工作流。
工作流还可以调度这些活动和子工作流并行运行,允许单个工作流可能在整个集群的所有可用节点上分布计算任务。
您可以使用 Dapr 配置配置工作流和活动的最大并发性,如下一节所述。
工作流不控制负载如何在集群中分布的细节。 例如,如果工作流调度 10 个活动任务并行运行,所有 10 个任务可能运行在多达 10 个不同的计算节点上,也可能运行在单个计算节点上。 实际扩展行为由 actor 放置服务决定,该服务管理表示工作流每个任务的 actor 的分布。

为了提供持久性和弹性保证,Dapr 工作流频繁写入状态存储并依赖提醒来驱动执行。 因此,Dapr 工作流可能不适合延迟敏感的工作负载。 高延迟的预期来源包括:
有关工作流 actor 的设计如何影响执行延迟的更多详细信息,请参阅提醒使用和执行保证部分。
默认情况下,当客户端调度工作流时,工作流引擎会等待工作流完全启动后再向客户端返回响应。 在返回之前等待工作流启动会降低工作流的调度吞吐量。 当调度具有开始时间的工作流时,工作流引擎不会等待工作流启动就向客户端返回响应。 要提高调度吞吐量,请考虑在调度工作流时添加"现在"的开始时间。 以下显示了在 Go SDK 中调度开始时间为"现在"的工作流的示例:
client.ScheduleNewWorkflow(ctx, "MyCoolWorkflow", workflow.WithStartTime(time.Now()))
当使用 Dapr Shared时,可能会有多个 daprd 边车在单个负载均衡器或服务后面运行。
因此,接收工作的辅助实例可能不是接收工作结果的实例。
Dapr 创建第三种 actor 类型来处理此场景:dapr.internal.{namespace}.{appID}.executor,用于将辅助结果路由回正确的工作流 actor,以确保正确操作。
本文提供有关如何编写由 Dapr 工作流引擎执行的工作流的高级概述。
Dapr 工作流逻辑使用通用编程语言实现,使你能够:
Dapr 边车不会加载任何工作流定义。相反,边车只是驱动工作流的执行,所有工作流活动都作为应用程序的一部分。
工作流活动 是工作流中的基本工作单元,是在业务流程中被编排的任务。
定义你希望工作流执行的工作流活动。活动是一个函数定义,可以接受输入和输出。以下示例创建了一个名为 hello_act 的计数器(活动),用于通知用户当前的计数器值。hello_act 是一个从名为 WorkflowActivityContext 的类派生的函数。
@wfr.activity(name='hello_act')
def hello_act(ctx: WorkflowActivityContext, wf_input):
global counter
counter += wf_input
print(f'New counter value is: {counter}!', flush=True)
定义你希望工作流执行的工作流活动。活动被包装在 WorkflowActivityContext 类中,该类实现了工作流活动。
export default class WorkflowActivityContext {
private readonly _innerContext: ActivityContext;
constructor(innerContext: ActivityContext) {
if (!innerContext) {
throw new Error("ActivityContext cannot be undefined");
}
this._innerContext = innerContext;
}
public getWorkflowInstanceId(): string {
return this._innerContext.orchestrationId;
}
public getWorkflowActivityId(): number {
return this._innerContext.taskId;
}
}
定义你希望工作流执行的工作流活动。活动是一个类定义,可以接受输入和输出。活动还参与依赖注入,例如绑定到 Dapr 客户端。
以下示例中调用的活动包括:
NotifyActivity:接收新订单通知。ReserveInventoryActivity:检查是否有足够的库存来满足新订单。ProcessPaymentActivity:处理订单付款。包括 NotifyActivity 用于发送成功订单通知。public class NotifyActivity : WorkflowActivity<Notification, object>
{
//...
public NotifyActivity(ILoggerFactory loggerFactory)
{
this.logger = loggerFactory.CreateLogger<NotifyActivity>();
}
//...
}
查看完整的 NotifyActivity.cs 工作流活动示例。
public class ReserveInventoryActivity : WorkflowActivity<InventoryRequest, InventoryResult>
{
//...
public ReserveInventoryActivity(ILoggerFactory loggerFactory, DaprClient client)
{
this.logger = loggerFactory.CreateLogger<ReserveInventoryActivity>();
this.client = client;
}
//...
}
查看完整的 ReserveInventoryActivity.cs 工作流活动示例。
public class ProcessPaymentActivity : WorkflowActivity<PaymentRequest, object>
{
//...
public ProcessPaymentActivity(ILoggerFactory loggerFactory)
{
this.logger = loggerFactory.CreateLogger<ProcessPaymentActivity>();
}
//...
}
定义你希望工作流执行的工作流活动。活动被包装在公共 DemoWorkflowActivity 类中,该类实现了工作流活动。
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class DemoWorkflowActivity implements WorkflowActivity {
@Override
public DemoActivityOutput run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(DemoWorkflowActivity.class);
logger.info("Starting Activity: " + ctx.getName());
var message = ctx.getInput(DemoActivityInput.class).getMessage();
var newMessage = message + " World!, from Activity";
logger.info("Message Received from input: " + message);
logger.info("Sending message to output: " + newMessage);
logger.info("Sleeping for 5 seconds to simulate long running operation...");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
logger.info("Activity finished");
var output = new DemoActivityOutput(message, newMessage);
logger.info("Activity returned: " + output);
return output;
}
}
定义你希望工作流执行的每个工作流活动。活动输入可以使用 ctx.GetInput 从上下文中解组。活动应定义为接受 ctx workflow.ActivityContext 参数并返回接口和错误的函数。
func BusinessActivity(ctx workflow.ActivityContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return "", err
}
// Do something here
return "result", nil
}
使用参数 ctx *workflow.WorkflowContext 定义工作流函数,并返回 any 和 error。从工作流内部调用你定义的活动。
func BusinessWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return nil, err
}
var output string
if err := ctx.CallActivity(BusinessActivity, workflow.ActivityInput(input)).Await(&output); err != nil {
return nil, err
}
if err := ctx.WaitForExternalEvent("businessEvent", time.Minute*60).Await(&output); err != nil {
return nil, err
}
if err := ctx.CreateTimer(time.Second).Await(nil); err != nil {
return nil, nil
}
return output, nil
}
在你的应用程序可以执行工作流之前,你必须将工作流编排器和其活动注册到工作流注册表中。这确保 Dapr 知道在执行工作流时调用哪些函数。
func main() {
// Create a workflow registry
r := workflow.NewRegistry()
// Register the workflow orchestrator
if err := r.AddWorkflow(BusinessWorkflow); err != nil {
log.Fatal(err)
}
fmt.Println("BusinessWorkflow registered")
// Register the workflow activities
if err := r.AddActivity(BusinessActivity); err != nil {
log.Fatal(err)
}
fmt.Println("BusinessActivity registered")
// Create workflow client and start worker
wclient, err := client.NewWorkflowClient()
if err != nil {
log.Fatal(err)
}
fmt.Println("Worker initialized")
ctx, cancel := context.WithCancel(context.Background())
if err = wclient.StartWorker(ctx, r); err != nil {
log.Fatal(err)
}
fmt.Println("runner started")
// Your application logic continues here...
// Example: Start a workflow
instanceID, err := wclient.ScheduleWorkflow(ctx, "BusinessWorkflow", workflow.WithInput(1))
if err != nil {
log.Fatalf("failed to start workflow: %v", err)
}
fmt.Printf("workflow started with id: %v\n", instanceID)
// Stop workflow worker when done
cancel()
fmt.Println("workflow worker successfully shutdown")
}
关于注册的关键要点:
workflow.NewRegistry() 创建工作流注册表r.AddWorkflow() 注册工作流函数r.AddActivity() 注册活动函数client.NewWorkflowClient() 创建工作流客户端wclient.StartWorker() 开始处理工作流wclient.ScheduleWorkflow 调度命名的工作流实例接下来,在工作流中注册和调用活动。
hello_world_wf 函数是一个从名为 DaprWorkflowContext 的类派生的函数,具有输入和输出参数类型。它还包括一个 yield 语句,该语句完成工作流的核心逻辑并调用工作流活动。
@wfr.workflow(name='hello_world_wf')
def hello_world_wf(ctx: DaprWorkflowContext, wf_input):
print(f'{wf_input}')
yield ctx.call_activity(hello_act, input=1)
yield ctx.call_activity(hello_act, input=10)
yield ctx.call_activity(hello_retryable_act, retry_policy=retry_policy)
yield ctx.call_child_workflow(child_retryable_wf, retry_policy=retry_policy)
# Change in event handling: Use when_any to handle both event and timeout
event = ctx.wait_for_external_event(event_name)
timeout = ctx.create_timer(timedelta(seconds=30))
winner = yield when_any([event, timeout])
if winner == timeout:
print('Workflow timed out waiting for event')
return 'Timeout'
yield ctx.call_activity(hello_act, input=100)
yield ctx.call_activity(hello_act, input=1000)
return 'Completed'
接下来,向 WorkflowRuntime 类注册工作流并启动工作流运行时。
export default class WorkflowRuntime {
//..
// Register workflow implementation for handling orchestrations
public registerWorkflow(workflow: TWorkflow): WorkflowRuntime {
const name = getFunctionName(workflow);
const workflowWrapper = (ctx: OrchestrationContext, input: any): any => {
const workflowContext = new WorkflowContext(ctx);
return workflow(workflowContext, input);
};
this.worker.addNamedOrchestrator(name, workflowWrapper);
return this;
}
// Register workflow activities
public registerActivity(fn: TWorkflowActivity<TInput, TOutput>): WorkflowRuntime {
const name = getFunctionName(fn);
const activityWrapper = (ctx: ActivityContext, input: TInput): TOutput => {
const wfActivityContext = new WorkflowActivityContext(ctx);
return fn(wfActivityContext, input);
};
this.worker.addNamedActivity(name, activityWrapper);
return this;
}
// Start the workflow runtime processing items and block.
public async start() {
await this.worker.start();
}
}
OrderProcessingWorkflow 类是从名为 Workflow 的基类派生的,具有输入和输出参数类型。它还包括一个 RunAsync 方法,该方法完成工作流的核心逻辑并调用工作流活动。
class OrderProcessingWorkflow : Workflow<OrderPayload, OrderResult>
{
public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order)
{
//...
await context.CallActivityAsync(
nameof(NotifyActivity),
new Notification($"Received order {orderId} for {order.Name} at {order.TotalCost:c}"));
//...
InventoryResult result = await context.CallActivityAsync<InventoryResult>(
nameof(ReserveInventoryActivity),
new InventoryRequest(RequestId: orderId, order.Name, order.Quantity));
//...
await context.CallActivityAsync(
nameof(ProcessPaymentActivity),
new PaymentRequest(RequestId: orderId, order.TotalCost, "USD"));
await context.CallActivityAsync(
nameof(NotifyActivity),
new Notification($"Order {orderId} processed successfully!"));
// End the workflow with a success result
return new OrderResult(Processed: true);
}
}
接下来,向 WorkflowRuntimeBuilder 注册工作流并启动工作流运行时。
public class DemoWorkflowWorker {
public static void main(String[] args) throws Exception {
// Register the Workflow with the builder.
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder().registerWorkflow(DemoWorkflow.class);
builder.registerActivity(DemoWorkflowActivity.class);
// Build and then start the workflow runtime pulling and executing tasks
try (WorkflowRuntime runtime = builder.build()) {
System.out.println("Start workflow runtime");
runtime.start();
}
System.exit(0);
}
}
使用参数 ctx *workflow.WorkflowContext 定义工作流函数,并返回 any 和 error。从工作流内部调用你定义的活动。
func BusinessWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var input int
if err := ctx.GetInput(&input); err != nil {
return nil, err
}
var output string
if err := ctx.CallActivity(BusinessActivity, workflow.ActivityInput(input)).Await(&output); err != nil {
return nil, err
}
if err := ctx.WaitForExternalEvent("businessEvent", time.Minute*60).Await(&output); err != nil {
return nil, err
}
if err := ctx.CreateTimer(time.Second).Await(nil); err != nil {
return nil, nil
}
return output, nil
}
最后,使用工作流编写应用程序。
在以下示例中,对于使用 Python SDK 的基本 Python hello world 应用程序,你的项目代码应包括:
DaprClient 的 Python 包,用于接收 Python SDK 功能。from datetime import timedelta
from time import sleep
from dapr.ext.workflow import (
WorkflowRuntime,
DaprWorkflowContext,
WorkflowActivityContext,
RetryPolicy,
DaprWorkflowClient,
when_any,
)
from dapr.conf import Settings
from dapr.clients.exceptions import DaprInternalError
settings = Settings()
counter = 0
retry_count = 0
child_orchestrator_count = 0
child_orchestrator_string = ''
child_act_retry_count = 0
instance_id = 'exampleInstanceID'
child_instance_id = 'childInstanceID'
workflow_name = 'hello_world_wf'
child_workflow_name = 'child_wf'
input_data = 'Hi Counter!'
event_name = 'event1'
event_data = 'eventData'
non_existent_id_error = 'no such instance exists'
retry_policy = RetryPolicy(
first_retry_interval=timedelta(seconds=1),
max_number_of_attempts=3,
backoff_coefficient=2,
max_retry_interval=timedelta(seconds=10),
retry_timeout=timedelta(seconds=100),
)
wfr = WorkflowRuntime()
@wfr.workflow(name='hello_world_wf')
def hello_world_wf(ctx: DaprWorkflowContext, wf_input):
print(f'{wf_input}')
yield ctx.call_activity(hello_act, input=1)
yield ctx.call_activity(hello_act, input=10)
yield ctx.call_activity(hello_retryable_act, retry_policy=retry_policy)
yield ctx.call_child_workflow(child_retryable_wf, retry_policy=retry_policy)
# Change in event handling: Use when_any to handle both event and timeout
event = ctx.wait_for_external_event(event_name)
timeout = ctx.create_timer(timedelta(seconds=30))
winner = yield when_any([event, timeout])
if winner == timeout:
print('Workflow timed out waiting for event')
return 'Timeout'
yield ctx.call_activity(hello_act, input=100)
yield ctx.call_activity(hello_act, input=1000)
return 'Completed'
@wfr.activity(name='hello_act')
def hello_act(ctx: WorkflowActivityContext, wf_input):
global counter
counter += wf_input
print(f'New counter value is: {counter}!', flush=True)
@wfr.activity(name='hello_retryable_act')
def hello_retryable_act(ctx: WorkflowActivityContext):
global retry_count
if (retry_count % 2) == 0:
print(f'Retry count value is: {retry_count}!', flush=True)
retry_count += 1
raise ValueError('Retryable Error')
print(f'Retry count value is: {retry_count}! This print statement verifies retry', flush=True)
retry_count += 1
@wfr.workflow(name='child_retryable_wf')
def child_retryable_wf(ctx: DaprWorkflowContext):
global child_orchestrator_string, child_orchestrator_count
if not ctx.is_replaying:
child_orchestrator_count += 1
print(f'Appending {child_orchestrator_count} to child_orchestrator_string!', flush=True)
child_orchestrator_string += str(child_orchestrator_count)
yield ctx.call_activity(
act_for_child_wf, input=child_orchestrator_count, retry_policy=retry_policy
)
if child_orchestrator_count < 3:
raise ValueError('Retryable Error')
@wfr.activity(name='act_for_child_wf')
def act_for_child_wf(ctx: WorkflowActivityContext, inp):
global child_orchestrator_string, child_act_retry_count
inp_char = chr(96 + inp)
print(f'Appending {inp_char} to child_orchestrator_string!', flush=True)
child_orchestrator_string += inp_char
if child_act_retry_count % 2 == 0:
child_act_retry_count += 1
raise ValueError('Retryable Error')
child_act_retry_count += 1
def main():
wfr.start()
wf_client = DaprWorkflowClient()
print('==========Start Counter Increase as per Input:==========')
wf_client.schedule_new_workflow(
workflow=hello_world_wf, input=input_data, instance_id=instance_id
)
wf_client.wait_for_workflow_start(instance_id)
# Sleep to let the workflow run initial activities
sleep(12)
assert counter == 11
assert retry_count == 2
assert child_orchestrator_string == '1aa2bb3cc'
# Pause Test
wf_client.pause_workflow(instance_id=instance_id)
metadata = wf_client.get_workflow_state(instance_id=instance_id)
print(f'Get response from {workflow_name} after pause call: {metadata.runtime_status.name}')
# Resume Test
wf_client.resume_workflow(instance_id=instance_id)
metadata = wf_client.get_workflow_state(instance_id=instance_id)
print(f'Get response from {workflow_name} after resume call: {metadata.runtime_status.name}')
sleep(2) # Give the workflow time to reach the event wait state
wf_client.raise_workflow_event(instance_id=instance_id, event_name=event_name, data=event_data)
print('========= Waiting for Workflow completion', flush=True)
try:
state = wf_client.wait_for_workflow_completion(instance_id, timeout_in_seconds=30)
if state.runtime_status.name == 'COMPLETED':
print('Workflow completed! Result: {}'.format(state.serialized_output.strip('"')))
else:
print(f'Workflow failed! Status: {state.runtime_status.name}')
except TimeoutError:
print('*** Workflow timed out!')
wf_client.purge_workflow(instance_id=instance_id)
try:
wf_client.get_workflow_state(instance_id=instance_id)
except DaprInternalError as err:
if non_existent_id_error in err._message:
print('Instance Successfully Purged')
sleep(10000)
wfr.shutdown()
if __name__ == '__main__':
main()
以下示例 是使用 JavaScript SDK 的基本 JavaScript 应用程序。与此示例一样,你的项目代码应包括:
mkdir my-wf && cd my-wf
npm init -y
npm i @dapr/dapr @microsoft/durabletask-js
npm i -D typescript ts-node @types/node
创建以下 tsconfig.json 文件:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}
创建以下 src/app.ts 文件:
import {
WorkflowRuntime,
WorkflowActivityContext,
WorkflowContext,
DaprWorkflowClient,
TWorkflow
} from "@dapr/dapr";
const workflowClient = new DaprWorkflowClient();
const workflowRuntime = new WorkflowRuntime();
// simple activity
const hello = async (_: WorkflowActivityContext, name: string) => `Hello ${name}!`;
// simple workflow: call the activity 3 times
const sequence: TWorkflow = async function* (ctx: WorkflowContext): any {
const out: string[] = [];
out.push(yield ctx.callActivity(hello, "Tokyo"));
out.push(yield ctx.callActivity(hello, "Seattle"));
out.push(yield ctx.callActivity(hello, "London"));
out.push(yield ctx.waitForExternalEvent("continue"));
return out;
};
async function main() {
workflowRuntime.registerWorkflow(sequence).registerActivity(hello);
await workflowRuntime.start();
const id = await workflowClient.scheduleNewWorkflow(sequence);
console.log("Scheduled:", id);
workflowClient.raiseEvent(id, "continue", "Go go go!");
const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30);
console.log("Done:", state?.runtimeStatus, "output:", state?.serializedOutput);
await new Promise(f => setTimeout(f, 100000));
await workflowRuntime.stop();
await workflowClient.stop();
}
main().catch((e) => { console.error(e); });
在以下 Program.cs 示例中,对于使用 .NET SDK 的基本 ASP.NET 订单处理应用程序,你的项目代码应包括:
Dapr.Workflow 的 NuGet 包,用于接收 .NET SDK 功能AddDaprWorkflow 的构建器using Dapr.Workflow;
//...
// Dapr Workflows are registered as part of the service configuration
builder.Services.AddDaprWorkflow(options =>
{
// Note that it's also possible to register a lambda function as the workflow
// or activity implementation instead of a class.
options.RegisterWorkflow<OrderProcessingWorkflow>();
// These are the activities that get invoked by the workflow(s).
options.RegisterActivity<NotifyActivity>();
options.RegisterActivity<ReserveInventoryActivity>();
options.RegisterActivity<ProcessPaymentActivity>();
});
WebApplication app = builder.Build();
// POST starts new order workflow instance
app.MapPost("/orders", async (DaprWorkflowClient client, [FromBody] OrderPayload orderInfo) =>
{
if (orderInfo?.Name == null)
{
return Results.BadRequest(new
{
message = "Order data was missing from the request",
example = new OrderPayload("Paperclips", 99.95),
});
}
//...
});
// GET fetches state for order workflow to report status
app.MapGet("/orders/{orderId}", async (string orderId, DaprWorkflowClient client) =>
{
WorkflowState state = await client.GetWorkflowStateAsync(orderId, true);
if (!state.Exists)
{
return Results.NotFound($"No order with ID = '{orderId}' was found.");
}
var httpResponsePayload = new
{
details = state.ReadInputAs<OrderPayload>(),
status = state.RuntimeStatus.ToString(),
result = state.ReadOutputAs<OrderResult>(),
};
//...
}).WithName("GetOrderInfoEndpoint");
app.Run();
如以下示例所示,使用 Java SDK 和 Dapr Workflow 的 hello-world 应用程序应包括:
io.dapr.workflows.client 的 Java 包,用于接收 Java SDK 客户端功能。io.dapr.workflows.WorkflowDemoWorkflow 类,它扩展了 Workflowpackage io.dapr.examples.workflows;
import com.microsoft.durabletask.CompositeTaskFailedException;
import com.microsoft.durabletask.Task;
import com.microsoft.durabletask.TaskCanceledException;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.WorkflowStub;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
/**
* Implementation of the DemoWorkflow for the server side.
*/
public class DemoWorkflow extends Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
ctx.getLogger().info("Starting Workflow: " + ctx.getName());
// ...
ctx.getLogger().info("Calling Activity...");
var input = new DemoActivityInput("Hello Activity!");
var output = ctx.callActivity(DemoWorkflowActivity.class.getName(), input, DemoActivityOutput.class).await();
// ...
};
}
}
如以下示例所示,使用 Go SDK 和 Dapr Workflow 的 hello-world 应用程序应包括:
client 的 Go 包,用于接收 Go SDK 客户端功能。BusinessWorkflow 方法package main
import (
"context"
"errors"
"fmt"
"log"
"strconv"
"time"
"github.com/dapr/durabletask-go/workflow"
"github.com/dapr/go-sdk/client"
)
var stage = 0
var failActivityTries = 0
func main() {
r := workflow.NewRegistry()
if err := r.AddWorkflow(BusinessWorkflow); err != nil {
log.Fatal(err)
}
fmt.Println("BusinessWorkflow registered")
if err := r.AddActivity(BusinessActivity); err != nil {
log.Fatal(err)
}
fmt.Println("BusinessActivity registered")
if err := r.AddActivity(FailActivity); err != nil {
log.Fatal(err)
}
fmt.Println("FailActivity registered")
wclient, err := client.NewWorkflowClient()
if err != nil {
log.Fatal(err)
}
fmt.Println("Worker initialized")
ctx, cancel := context.WithCancel(context.Background())
if err = wclient.StartWorker(ctx, r); err != nil {
log.Fatal(err)
}
fmt.Println("runner started")
// Start workflow test
// Set the start time to the current time to not wait for the workflow to
// "start". This is useful for increasing the throughput of creating
// workflows.
// workflow.WithStartTime(time.Now())
instanceID, err := wclient.ScheduleWorkflow(ctx, "BusinessWorkflow", workflow.WithInstanceID("a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9"), workflow.WithInput("1"))
if err != nil {
log.Fatalf("failed to start workflow: %v", err)
}
fmt.Printf("workflow started with id: %v\n", instanceID)
// Pause workflow test
err = wclient.SuspendWorkflow(ctx, instanceID, "")
if err != nil {
log.Fatalf("failed to pause workflow: %v", err)
}
respFetch, err := wclient.FetchWorkflowMetadata(ctx, instanceID, workflow.WithFetchPayloads(true))
if err != nil {
log.Fatalf("failed to fetch workflow: %v", err)
}
if respFetch.RuntimeStatus != workflow.StatusSuspended {
log.Fatalf("workflow not paused: %s: %v", respFetch.RuntimeStatus, respFetch)
}
fmt.Printf("workflow paused\n")
// Resume workflow test
err = wclient.ResumeWorkflow(ctx, instanceID, "")
if err != nil {
log.Fatalf("failed to resume workflow: %v", err)
}
respFetch, err = wclient.FetchWorkflowMetadata(ctx, instanceID, workflow.WithFetchPayloads(true))
if err != nil {
log.Fatalf("failed to get workflow: %v", err)
}
if respFetch.RuntimeStatus != workflow.StatusRunning {
log.Fatalf("workflow not running")
}
fmt.Println("workflow resumed")
fmt.Printf("stage: %d\n", stage)
// Raise Event
err = wclient.RaiseEvent(ctx, instanceID, "businessEvent", workflow.WithEventPayload("testData"))
if err != nil {
fmt.Printf("failed to raise event: %v", err)
}
fmt.Println("workflow event raised")
time.Sleep(time.Second) // allow workflow to advance
fmt.Printf("stage: %d\n", stage)
_, err = wclient.WaitForWorkflowCompletion(ctx, instanceID)
if err != nil {
log.Fatalf("failed to wait for workflow: %v", err)
}
fmt.Printf("fail activity executions: %d\n", failActivityTries)
respFetch, err = wclient.FetchWorkflowMetadata(ctx, instanceID, workflow.WithFetchPayloads(true))
if err != nil {
log.Fatalf("failed to get workflow: %v", err)
}
fmt.Printf("workflow status: %v\n", respFetch.String())
// Purge workflow test
err = wclient.PurgeWorkflowState(ctx, instanceID)
if err != nil {
log.Fatalf("failed to purge workflow: %v", err)
}
respFetch, err = wclient.FetchWorkflowMetadata(ctx, instanceID, workflow.WithFetchPayloads(true))
if err == nil || respFetch != nil {
log.Fatalf("failed to purge workflow: %v", err)
}
fmt.Println("workflow purged")
fmt.Printf("stage: %d\n", stage)
// Terminate workflow test
id, err := wclient.ScheduleWorkflow(ctx, "BusinessWorkflow", workflow.WithInstanceID("a7a4168d-3a1c-41da-8a4f-e7f6d9c718d9"), workflow.WithInput("1"))
if err != nil {
log.Fatalf("failed to start workflow: %v", err)
}
fmt.Printf("workflow started with id: %v\n", instanceID)
metadata, err := wclient.WaitForWorkflowStart(ctx, id)
if err != nil {
log.Fatalf("failed to get workflow: %v", err)
}
fmt.Printf("workflow status: %s\n", metadata.String())
err = wclient.TerminateWorkflow(ctx, id)
if err != nil {
log.Fatalf("failed to terminate workflow: %v", err)
}
fmt.Println("workflow terminated")
err = wclient.PurgeWorkflowState(ctx, id)
if err != nil {
log.Fatalf("failed to purge workflow: %v", err)
}
fmt.Println("workflow purged")
<-ctx.Done()
cancel()
fmt.Println("workflow worker successfully shutdown")
}
func BusinessWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var input string
if err := ctx.GetInput(&input); err != nil {
return nil, err
}
var output string
if err := ctx.CallActivity(BusinessActivity, workflow.WithActivityInput(input)).Await(&output); err != nil {
return nil, err
}
err := ctx.WaitForExternalEvent("businessEvent", time.Minute*60).Await(&output)
if err != nil {
return nil, err
}
if err := ctx.CallActivity(BusinessActivity, workflow.WithActivityInput(input)).Await(&output); err != nil {
return nil, err
}
if err := ctx.CallActivity(FailActivity, workflow.WithActivityRetryPolicy(&workflow.RetryPolicy{
MaxAttempts: 3,
InitialRetryInterval: 100 * time.Millisecond,
BackoffCoefficient: 2,
MaxRetryInterval: 1 * time.Second,
})).Await(nil); err == nil {
return nil, fmt.Errorf("unexpected no error executing fail activity")
}
return output, nil
}
func BusinessActivity(ctx workflow.ActivityContext) (any, error) {
var input string
if err := ctx.GetInput(&input); err != nil {
return "", err
}
iinput, err := strconv.Atoi(input)
if err != nil {
return "", err
}
stage += iinput
return fmt.Sprintf("Stage: %d", stage), nil
}
func FailActivity(ctx workflow.ActivityContext) (any, error) {
failActivityTries += 1
return nil, errors.New("dummy activity error")
}
通过你的 IDE 或 Dapr CLI 启动工作流应用程序(如果你想启动多个应用程序,请使用 Dapr 多应用运行;如果只启动一个应用程序,请使用常规 Dapr run 命令),然后调度一个新的工作流实例。
使用本地 Diagrid Dashboard 可视化和检查你的工作流状态,并深入查看详细的工作流执行历史。仪表板作为容器运行,连接到 Dapr 工作流使用的状态存储(默认情况下是本地 Redis 实例)。

使用 Docker 启动 Diagrid Dashboard 容器:
docker run -p 8080:8080 ghcr.io/diagridio/diagrid-dashboard:latest
在浏览器中打开仪表板,地址为 http://localhost:8080。
编写工作流后,你可以使用 Dapr CLI 进行测试:
dapr run --app-id workflow-app python3 app.py
确保应用程序正在运行:
dapr list
dapr workflow run hello_world_wf --app-id workflow-app --input 'hello world' --instance-id test-run
dapr workflow list --app-id workflow-app -o wide
dapr workflow list --app-id workflow-app --filter-status COMPLETED -o wide
dapr workflow history --app-id workflow-app test-run
dapr run --app-id workflow-app npx ts-node src/app.ts
确保应用程序正在运行:
dapr list
dapr workflow run sequence --app-id workflow-app --input 'hello world' --instance-id test-run
dapr workflow list --app-id workflow-app -o wide
dapr workflow raise-event --app-id workflow-app test-run/businessEvent
dapr workflow list --app-id workflow-app --filter-status COMPLETED -o wide
dapr workflow history --app-id workflow-app test-run
dapr run --app-id workflow-app dotnet run
确保应用程序正在运行:
dapr list
dapr workflow run OrderProcessingWorkflow --app-id workflow-app --instance-id test-run --input '{"name": "Paperclips", "totalCost": 99.95}'
dapr workflow list --app-id workflow-app -o wide
dapr workflow raise-event --app-id workflow-app test-run/incoming-purchase-order --input '{"name": "Paperclips", "totalCost": 99.95}'
dapr workflow list --app-id workflow-app --filter-status COMPLETED -o wide
dapr workflow history --app-id workflow-app test-run
dapr run --app-id workflow-app -- java -jar target/WorkflowService-0.0.1-SNAPSHOT.jar
确保应用程序正在运行:
dapr list
dapr workflow run DemoWorkflow --app-id workflow-app --instance-id test-run --input "input data"
dapr workflow list --app-id workflow-app -o wide
dapr workflow raise-event --app-id workflow-app test-run/TestEvent --input 'TestEventPayload'
dapr workflow raise-event --app-id workflow-app test-run/event1 --input 'TestEvent 1 Payload'
dapr workflow raise-event --app-id workflow-app test-run/event2 --input 'TestEvent 2 Payload'
dapr workflow raise-event --app-id workflow-app test-run/event3 --input 'TestEvent 3 Payload'
dapr workflow list --app-id workflow-app --filter-status COMPLETED -o wide
dapr workflow history --app-id workflow-app test-run
dapr run --app-id workflow-app go run main.go
确保应用程序正在运行:
dapr list
dapr workflow run BusinessWorkflow --app-id workflow-app --input '1' --instance-id test-run
dapr workflow list --app-id workflow-app -o wide
dapr workflow raise-event --app-id workflow-app test-run/businessEvent
dapr workflow list --app-id workflow-app --filter-status COMPLETED -o wide
dapr workflow history test-run --app-id workflow-app
dapr workflow list --app-id workflow-app --filter-status RUNNING -o wide
dapr workflow list --app-id workflow-app --filter-status FAILED -o wide
dapr workflow list --app-id workflow-app --filter-status COMPLETED -o wide
# 触发你的工作流正在等待的事件
dapr workflow raise-event <instance-id>/ApprovalReceived \
--app-id workflow-app \
--input '{"approved": true, "approver": "manager@company.com"}'
# 列出失败的工作流
dapr workflow list --app-id workflow-app --filter-status FAILED --output wide
# 获取失败工作流的详细历史
dapr workflow history <failed-instance-id> --app-id workflow-app --output json
# 修复问题后重新运行工作流
dapr workflow rerun <failed-instance-id> --app-id workflow-app --input '<new-input-json-data>'
现在你已经编写了一个工作流,学习如何管理它。
管理工作流 >>现在您已经在应用程序中编写了工作流及其活动,可以使用 CLI 或 API 调用来启动、终止、重运行和获取工作流信息。
Dapr CLI 提供了在自托管和 Kubernetes 环境中管理工作流实例的命令。
另请参阅工作流保留策略了解如何为已完成的工作流配置保留策略。
# 使用 `orderprocessing` 应用程序,启动一个新的工作流实例并传入输入数据
dapr workflow run OrderProcessingWorkflow \
--app-id orderprocessing \
--input '{"orderId": "12345", "amount": 100.50}'
# 使用特定实例 ID 启动新工作流
dapr workflow run OrderProcessingWorkflow \
--app-id orderprocessing \
--instance-id order-12345 \
--input '{"orderId": "12345"}'
# 计划在 2024 年 12 月 25 日上午 10:00:00(协调世界时 UTC)启动新工作流
dapr workflow run OrderProcessingWorkflow \
--app-id orderprocessing \
--start-time "2024-12-25T10:00:00Z"
# 列出应用的所有工作流
dapr workflow list
# 按状态筛选
dapr workflow list --filter-status RUNNING
# 按工作流名称和应用 ID 筛选
dapr workflow list --app-id orderprocessing --filter-name OrderProcessingWorkflow
# 按时间筛选(过去 24 小时内启动的工作流)
dapr workflow list --filter-max-age 24h
# 获取详细输出
dapr workflow list -o wide
# 获取执行历史
dapr workflow history order-12345
# 在特定应用 ID 上以 JSON 格式获取历史
dapr workflow history order-12345 --app-id orderprocessing --output json
# 暂停正在运行的工作流
dapr workflow suspend order-12345 \
--app-id orderprocessing \
--reason "Waiting for manual approval"
# 恢复已暂停的工作流
dapr workflow resume order-12345 \
--app-id orderprocessing \
--reason "Approved by manager"
# 终止工作流
dapr workflow terminate order-12345 \
--app-id orderprocessing \
--output '{"reason": "Cancelled by customer"}'
# 为等待中的工作流触发事件
dapr workflow raise-event order-12345/PaymentReceived \
--app-id orderprocessing \
--input '{"paymentId": "pay-67890", "amount": 100.50}'
# 从头重运行
dapr workflow rerun order-12345
# 从特定事件 ID 重运行(通过 history 命令发现)
dapr workflow rerun order-12345 --event-id 5
# 使用新的指定实例 ID 重运行
dapr workflow rerun order-12345 --new-instance-id order-12345-retry
请注意,从 CLI 清理工作流也会删除所有关联的 Scheduler 提醒。
<p>应用程序中必须运行工作流客户端才能执行清理操作。</p>
需要工作流客户端连接才能保持工作流状态机的完整性,防止数据损坏。 以下错误表明工作流客户端未运行:
failed to purge orchestration state: rpc error: code = FailedPrecondition desc = failed to purge orchestration state: failed to lookup actor: api error: code = FailedPrecondition desc = did not find address for actor
可以使用 --force 标志在没有工作流应用程序运行的情况下清理工作流;但是,这只应在您确定没有工作流实例正在运行时使用,否则将会损坏工作流状态机。
# 清理特定实例
dapr workflow purge order-12345
# 清理 30 天前已完成的所有工作流
dapr workflow purge --all-older-than 720h
# 清理所有终结状态的工作流(谨慎使用!)
dapr workflow purge --app-id orderprocessing --all
# 在没有运行工作流客户端的情况下强制清理(极度谨慎使用!)
dapr workflow purge order-12345 --force
所有命令都支持 Kubernetes 部署的 -k 标志:
# 在 Kubernetes 中列出工作流
dapr workflow list \
--kubernetes \
--namespace production \
--app-id orderprocessing
# 在 Kubernetes 中暂停工作流
dapr workflow suspend order-12345 \
--kubernetes \
--namespace production \
--app-id orderprocessing \
--reason "Maintenance window"
在自托管模式下,只需运行:
dapr workflow list
在 Kubernetes 模式下,需要指定 --kubernetes/-k 标志以及命名空间和应用 ID:
dapr workflow list -k
监控正在运行的工作流:使用筛选列表跟踪长期运行的实例
dapr workflow list --app-id orderprocessing --filter-status RUNNING --filter-max-age 24h
使用实例 ID:分配有意义的实例 ID 以便更轻松地跟踪
dapr workflow run OrderWorkflow --app-id orderprocessing --instance-id "order-$(date +%s)"
导出以供分析:导出工作流数据以供分析
dapr workflow list --app-id orderprocessing --output json > workflows.json
工作流提醒存储在 Scheduler 中,可以使用 dapr scheduler CLI 进行管理。
dapr scheduler list --filter workflow
NAME BEGIN COUNT LAST TRIGGER
workflow/my-app/instance1/timer-0-ABC123 +50.0h 0
workflow/my-app/instance2/timer-0-XYZ789 +50.0h 0
获取提醒详情
dapr scheduler get workflow/my-app/instance1/timer-0-ABC123 -o yaml
删除单个提醒:
dapr scheduler delete workflow/my-app/instance1/timer-0-ABC123
删除给定工作流应用的所有提醒:
dapr scheduler delete-all workflow/my-app
删除特定工作流实例的所有提醒:
dapr scheduler delete-all workflow/my-app/instance1
导出所有提醒:
dapr scheduler export -o workflow-reminders-backup.bin
从备份文件恢复:
dapr scheduler import -f workflow-reminders-backup.bin
在代码中管理工作流。在编写工作流指南的工作流示例中,工作流使用以下 API 注册到代码中:
from dapr.ext.workflow import WorkflowRuntime, DaprWorkflowContext, WorkflowActivityContext
from dapr.clients import DaprClient
# 合理的参数
instanceId = "exampleInstanceID"
workflowComponent = "dapr"
workflowName = "hello_world_wf"
eventName = "event1"
eventData = "eventData"
# 启动工作流
wf_client.schedule_new_workflow(
workflow=hello_world_wf, input=input_data, instance_id=instance_id
)
# 获取工作流信息
wf_client.get_workflow_state(instance_id=instance_id)
# 暂停工作流
wf_client.pause_workflow(instance_id=instance_id)
metadata = wf_client.get_workflow_state(instance_id=instance_id)
# 恢复工作流
wf_client.resume_workflow(instance_id=instance_id)
# 向工作流触发事件
wf_client.raise_workflow_event(instance_id=instance_id, event_name=event_name, data=event_data)
# 清理工作流
wf_client.purge_workflow(instance_id=instance_id)
# 等待工作流完成
wf_client.wait_for_workflow_completion(instance_id, timeout_in_seconds=30)
在代码中管理工作流。在编写工作流指南的工作流示例中,工作流使用以下 API 注册到代码中:
import { DaprClient } from "@dapr/dapr";
async function printWorkflowStatus(client: DaprClient, instanceId: string) {
const workflow = await client.workflow.get(instanceId);
console.log(
`Workflow ${workflow.workflowName}, created at ${workflow.createdAt.toUTCString()}, has status ${
workflow.runtimeStatus
}`,
);
console.log(`Additional properties: ${JSON.stringify(workflow.properties)}`);
console.log("--------------------------------------------------\n\n");
}
async function start() {
const client = new DaprClient();
// 启动新的工作流实例
const instanceId = await client.workflow.start("OrderProcessingWorkflow", {
Name: "Paperclips",
TotalCost: 99.95,
Quantity: 4,
});
console.log(`Started workflow instance ${instanceId}`);
await printWorkflowStatus(client, instanceId);
// 暂停工作流实例
await client.workflow.pause(instanceId);
console.log(`Paused workflow instance ${instanceId}`);
await printWorkflowStatus(client, instanceId);
// 恢复工作流实例
await client.workflow.resume(instanceId);
console.log(`Resumed workflow instance ${instanceId}`);
await printWorkflowStatus(client, instanceId);
// 终止工作流实例
await client.workflow.terminate(instanceId);
console.log(`Terminated workflow instance ${instanceId}`);
await printWorkflowStatus(client, instanceId);
// 等待工作流完成,30 秒!
await new Promise((resolve) => setTimeout(resolve, 30000));
await printWorkflowStatus(client, instanceId);
// 清理工作流实例
await client.workflow.purge(instanceId);
console.log(`Purged workflow instance ${instanceId}`);
// 这会抛出错误,因为工作流实例已不存在
await printWorkflowStatus(client, instanceId);
}
start().catch((e) => {
console.error(e);
process.exit(1);
});
在代码中管理工作流。在编写工作流指南的 OrderProcessingWorkflow 示例中,工作流已注册到代码中。现在您可以启动、终止和获取运行中工作流的信息:
string orderId = "exampleOrderId";
OrderPayload input = new OrderPayload("Paperclips", 99.95);
Dictionary<string, string> workflowOptions; // 这是一个可选参数
// 使用 orderId 作为工作流 ID 启动工作流。这返回一个字符串,包含特定工作流实例的实例 ID,无论我们是自行提供还是由系统生成
await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), orderId, input, workflowOptions);
// 获取工作流信息。此响应包含工作流状态、启动时间等信息!
WorkflowState currentState = await daprWorkflowClient.GetWorkflowStateAsync(orderId, orderId);
// 终止工作流
await daprWorkflowClient.TerminateWorkflowAsync(orderId);
// 触发事件(传入的采购订单),工作流将等待该事件
await daprWorkflowClient.RaiseEventAsync(orderId, "incoming-purchase-order", input);
// 暂停
await daprWorkflowClient.SuspendWorkflowAsync(orderId);
// 恢复
await daprWorkflowClient.ResumeWorkflowAsync(orderId);
// 清理工作流,从关联实例中移除所有收件箱和历史信息
await daprWorkflowClient.PurgeInstanceAsync(orderId);
在代码中管理工作流。在 Java SDK 的工作流示例中,工作流使用以下 API 注册到代码中:
package io.dapr.examples.workflows;
import io.dapr.workflows.client.DaprWorkflowClient;
import io.dapr.workflows.client.WorkflowInstanceStatus;
// ...
public class DemoWorkflowClient {
// ...
public static void main(String[] args) throws InterruptedException {
DaprWorkflowClient client = new DaprWorkflowClient();
try (client) {
// 启动工作流
String instanceId = client.scheduleNewWorkflow(DemoWorkflow.class, "input data");
// 获取工作流的状态信息
WorkflowInstanceStatus workflowMetadata = client.getInstanceState(instanceId, true);
// 等待或暂停工作流实例启动
try {
WorkflowInstanceStatus waitForInstanceStartResult =
client.waitForInstanceStart(instanceId, Duration.ofSeconds(60), true);
}
// 为工作流触发事件;您可以并行触发多个事件
client.raiseEvent(instanceId, "TestEvent", "TestEventPayload");
client.raiseEvent(instanceId, "event1", "TestEvent 1 Payload");
client.raiseEvent(instanceId, "event2", "TestEvent 2 Payload");
client.raiseEvent(instanceId, "event3", "TestEvent 3 Payload");
// 等待工作流完成任务
try {
WorkflowInstanceStatus waitForInstanceCompletionResult =
client.waitForInstanceCompletion(instanceId, Duration.ofSeconds(60), true);
}
// 清理工作流实例,移除与其关联的所有元数据
boolean purgeResult = client.purgeInstance(instanceId);
// 终止工作流实例
client.terminateWorkflow(instanceToTerminateId, null);
System.exit(0);
}
}
在代码中管理工作流。在 Go SDK 的工作流示例中,工作流使用以下 API 注册到代码中:
// 启动工作流
type StartWorkflowRequest struct {
InstanceID string // 可选的实例标识符
WorkflowComponent string
WorkflowName string
Options map[string]string // 可选的元数据
Input any // 可选的输入
SendRawInput bool // 设置为 True 以禁用输入上的序列化
}
type StartWorkflowResponse struct {
InstanceID string
}
// 获取工作流状态
type GetWorkflowRequest struct {
InstanceID string
WorkflowComponent string
}
type GetWorkflowResponse struct {
InstanceID string
WorkflowName string
CreatedAt time.Time
LastUpdatedAt time.Time
RuntimeStatus string
Properties map[string]string
}
// 清理工作流
type PurgeWorkflowRequest struct {
InstanceID string
WorkflowComponent string
}
// 终止工作流
type TerminateWorkflowRequest struct {
InstanceID string
WorkflowComponent string
}
// 暂停工作流
type PauseWorkflowRequest struct {
InstanceID string
WorkflowComponent string
}
// 恢复工作流
type ResumeWorkflowRequest struct {
InstanceID string
WorkflowComponent string
}
// 为运行中的工作流触发事件
type RaiseEventWorkflowRequest struct {
InstanceID string
WorkflowComponent string
EventName string
EventData any
SendRawData bool // 设置为 True 以禁用数据上的序列化
}
使用 HTTP 调用管理工作流。下面的示例将编写工作流示例中的属性与随机实例 ID 结合使用。
使用 ID 12345678 启动工作流,运行:
curl -X POST "http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678"
请注意,工作流实例 ID 只能包含字母数字字符、下划线和破折号。
使用 ID 12345678 终止工作流,运行:
curl -X POST "http://localhost:3500/v1.0/workflows/dapr/12345678/terminate"
对于支持订阅外部事件的工作流组件(如 Dapr Workflow engine),您可以使用以下"触发事件" API 将命名事件传递到特定的工作流实例。
curl -X POST "http://localhost:3500/v1.0/workflows/<workflowComponentName>/<instanceID>/raiseEvent/<eventName>"
eventName可以是任意函数。
为了计划停机时间、等待输入等,您可以暂停然后恢复工作流。使用 ID 12345678 暂停工作流直到触发恢复,运行:
curl -X POST "http://localhost:3500/v1.0/workflows/dapr/12345678/pause"
使用 ID 12345678 恢复工作流,运行:
curl -X POST "http://localhost:3500/v1.0/workflows/dapr/12345678/resume"
清理 API 可用于从底层状态存储中永久删除工作流元数据,包括所有存储的输入、输出和工作流历史记录。这通常有助于实现数据保留策略和释放资源。
只有处于 COMPLETED、FAILED 或 TERMINATED 状态的工作流实例才能被清理。如果工作流处于任何其他状态,调用清理将返回错误。
curl -X POST "http://localhost:3500/v1.0/workflows/dapr/12345678/purge"
使用 ID 12345678 获取工作流信息(输出和输入),运行:
curl -X GET "http://localhost:3500/v1.0/workflows/dapr/12345678"
现在您已了解如何管理工作流,学习如何在多个应用程序中执行工作流
多应用程序工作流>>单个工作流通常跨越多个应用程序、微服务或编程语言。 在这种情况下,活动或子工作流将在与托管父工作流不同的应用程序上执行。
这非常有用的一些场景包括:

下图显示了一个复杂工作流的示例场景,该工作流跨多个用不同语言编写的应用程序进行编排。每个应用程序的主要步骤和活动包括:
• App1: 主工作流服务 - 协调整个 ML 管道的顶级编排器
• App2: 数据处理管道 - 仅限 GPU 活动
• App3: ML 训练子工作流 - 包含子工作流和活动
• App4: 模型服务 - 强大的 GPU 应用程序,仅包含活动
工作流执行路由基于托管 Dapr 应用程序的 App ID。 默认情况下,完整的工作流执行托管在启动该工作流的 App ID 上。该工作流可以在该 App ID 的任何副本上执行,而不仅仅是调度该工作流的单个副本。
可以通过在工作流执行代码中指定目标 App ID 参数,在不同的 App ID 上执行活动和子工作流。 执行时,目标 App ID 执行活动或子工作流,并将结果返回给原始 App ID 的父工作流。
整个工作流执行可以分布在多个 App ID 上,没有限制,每个活动或子工作流都可以指定目标 App ID。 工作流的最终历史记录将由托管最顶层父工作流(或可将其视为根工作流)的 App ID 保存。
支持多应用工作流的 SDK - 多应用工作流通过 SDK 使用。 目前支持以下 SDK:
调用多应用活动或子工作流时:
拥有不同 App ID 的团队之间必须进行协调,以确保活动和子工作流在需要时已定义并可用,这一点至关重要。
活动通常需要一定的时间来完成,或者在资源或美元成本上执行起来很昂贵。 因此,即使在异常路径中,也不希望对同一轮次执行这些活动超过一次。 在 1.17 之前的多应用场景中,活动会通过网络调用将响应发布给托管拥有工作流的其他应用程序。 在托管工作流应用程序关闭或无法访问的情况下,结果将丢失,活动将被重试,从而导致活动的重复执行。
在 1.17 中,启用 `WorkflowsRemoteActivityReminder feature gate 将使活动结果在托管工作流应用程序处于离线或无法访问时,通过提醒发送给拥有该工作流的应用程序,从而确保结果不会丢失并避免重复执行。 在所有应用程序上使用 Dapr 1.17 版本的所有用户都应启用此选项。 为了在 Dapr 版本之间保持向后兼容性,该选项默认处于 禁用 状态,但将在未来的版本中默认启用。

以下示例展示如何在目标应用程序 App2 上执行活动 ActivityA。
func BusinessWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var output string
err := ctx.CallActivity("ActivityA",
workflow.WithActivityInput("my-input"),
workflow.WithActivityAppID("App2"), // 这里我们设置将执行此活动的目标 App ID。
).Await(&output)
if err != nil {
return nil, err
}
return output, nil
}
public class BusinessWorkflow implements Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
String output = ctx.callActivity(
ActivityA.class.getName(),
"my-input",
new WorkflowTaskOptions("App2"), // 这里我们设置将执行此活动的目标 App ID。
String.class
).await();
ctx.complete(output);
};
}
}
@wfr.workflow
def app1_workflow(ctx: wf.DaprWorkflowContext):
output = yield ctx.call_activity('ActivityA', input='my-input', app_id='App2')
return output
public sealed class BusinessWorkflow : Workflow<string, string>
{
public override async Task<string> RunAsync(WorkflowContext context, string input)
{
var options = new WorkflowTaskOptions { TargetAppId = "App2" };
var output = await context.CallActivityAsync<string>(nameof(ActivityA), input, options);
return output;
}
}

以下示例展示如何在目标应用程序 App2 上执行子工作流 Workflow2。
func BusinessWorkflow(ctx *workflow.WorkflowContext) (any, error) {
var output string
err := ctx.CallChildWorkflow("Workflow2",
workflow.WithChildWorkflowInput("my-input"),
workflow.WithChildWorkflowAppID("App2"), // 这里我们设置将执行此子工作流的目标 App ID。
).Await(&output)
if err != nil {
return nil, err
}
return output, nil
}
@wfr.workflow
def workflow1(ctx: wf.DaprWorkflowContext):
output = yield ctx.call_child_workflow(workflow='Workflow2', input='my-input', app_id='App2')
return output
public sealed class BusinessWorkflow : Workflow<string, string>
{
public override async Task<string> RunAsync(WorkflowContext context, string input)
{
var options = new ChildWorkflowTaskOptions { TargetAppId = "App2" };
var output = await context.CallChildWorkflowAsync<string>(nameof(Workflow2), input, options);
return output;
}
}
Dapr 工作流状态存储在[执行器状态存储]{{ %ref workflow-architecture.md#state-store-usage %}}中。 默认情况下,Dapr Workflows 会无限期保留工作流状态变更的完整历史记录。 这意味着历史工作流可以随时被查询和检查。 运行大量工作流或生成大量状态变更历史记录可能导致存储使用量增加,最终可能填满状态存储磁盘空间。
为帮助管理存储使用,Dapr Workflows 支持配置历史保留策略。
该保留策略定义了工作流状态变更历史在被删除前保留多长时间。
工作流只有在达到终态(Completed、Failed 或 Terminated)后才有资格被删除。
每个工作流终态都可以设置自定义的保留时长,也可以为未明确配置的终态设置默认保留时长。
时长定义为 Go duration string(例如,72h 表示 72 小时,30m 表示 30 分钟)。
为 Completed 工作流配置较短的保留时长可能很有用,同时为 Failed 和 Terminated 工作流保留较长时间以便进行调查。
以下示例配置设置了每个终态。
这里设置的 anyTerminal 属性不会生效,因为所有终态都已明确配置,但它包含在此处作为参考。
有关如何将配置应用到 Dapr 应用程序的更多信息,请参阅 Dapr 配置文档。
kind: Configuration
metadata:
name: appconfig
spec:
workflow:
stateRetentionPolicy:
anyTerminal: "360h"
completed: "1m"
failed: "720h"
terminated: "360h"
您可以使用以下配置来设置在任何时候可以执行的最大并发工作流和活动数量。 这些限制是按每个边车实例实施的,这意味着如果您的工作流应用有 10 个副本,则有效限制将是配置值的 10 倍。
设置这些限制可以帮助防止您的 Dapr 边车和应用程序出现资源耗尽,或者在活动突发导致资源争用时帮助减少积压的工作流。 这些限制不会区分不同的工作流或活动定义,因此它们适用于在边车中运行的所有工作流和活动。
有关如何将配置应用到您的 Dapr 应用程序的更多信息,请参阅 Dapr 配置文档。
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
workflow:
maxConcurrentWorkflowInvocations: 100 # 默认为无限
maxConcurrentActivityInvocations: 1000 # 默认为无限
您的应用程序可以使用 Dapr 的状态管理 API 在受支持的状态存储中保存、读取和查询键值对。使用状态存储组件,您可以构建有状态的、长时间运行的应用程序,用于保存和检索其状态(例如购物车或游戏会话状态)。例如,在下图中:

以下概述视频和演示展示了 Dapr 状态管理的工作原理。
通过状态管理 API 构建块,您的应用程序可以利用通常构建复杂且容易出错的特性,包括:
以下是状态管理 API 提供的功能:
Dapr 数据存储被建模为组件,可以在不更改服务代码的情况下进行替换。请参阅受支持的状态存储查看列表。
使用 Dapr,您可以在状态操作请求中包含额外的元数据,用于描述您希望如何处理该请求。您可以附加:
默认情况下,您的应用程序应假设数据存储是最终一致的,并使用**最后写入胜出(last-write-wins)**的并发模式。
并非所有存储都是平等的。为了确保应用程序的可移植性,您可以查询存储的元数据功能,并使代码适应不同的存储功能。
Dapr 使用 ETags 支持乐观并发控制(OCC)。当请求状态值时,Dapr 始终将 ETag 属性附加到返回的状态。当用户代码:
If-Match 标头附加 ETag。当提供的 ETag 与状态存储中的 ETag 匹配时,write 操作成功。
在许多应用程序中,数据更新冲突很少发生,因为客户端在业务上下文中自然分离以操作不同的数据。但是,如果您的应用程序选择使用 ETags,ETag 不匹配可能会导致请求被拒绝。建议您在代码中使用重试策略来补偿使用 ETags 时的冲突。
如果您的应用程序在写入请求中省略 ETags,Dapr 将在处理请求时跳过 ETag 检查。这将启用最后写入胜出模式,而使用 ETags 则是**首次写入胜出(first-write-wins)**模式。
阅读 API 参考以了解如何设置并发选项。
Dapr 支持强一致性和最终一致性,默认行为是最终一致性。
阅读 API 参考以了解如何设置一致性选项。
状态存储组件可能根据内容类型以不同方式维护和操作数据。Dapr 支持在状态管理 API中传递内容类型作为请求元数据的一部分。
设置内容类型是_可选的_,组件决定是否使用它。Dapr 只提供将此信息传递给组件的手段。
metadata.contentType 设置内容类型。例如,http://localhost:3500/v1.0/state/store?metadata.contentType=application/json。"contentType" : <content type> 来设置内容类型。Dapr 支持两种类型的多读或多写操作:批量或事务。阅读 API 参考以了解如何使用批量和多选项。
您可以将多个读取请求分组到批量(或批处理)操作中。在批量操作中,Dapr 将读取请求作为单独的请求提交给底层数据存储,并将它们作为单个结果返回。
您可以将写入、更新和删除操作分组到一个请求中,然后作为原子事务处理。该请求将作为事务操作集成功或失败。
事务状态存储可用于存储 actor 状态。要指定用于 actor 的状态存储,请在状态存储组件的元数据部分将属性 actorStateStore 的值指定为 true。Actor 状态以特定方案存储在事务状态存储中,从而允许一致的查询。所有 actor 只能使用单个状态存储组件作为状态存储。如果您的状态存储由分布式数据库支持,您必须确保它提供强一致性。阅读状态 API 参考和 actor API 参考以了解有关 actor 状态存储的更多信息。
您在保存 actor 状态时,应始终设置 TTL 元数据字段(ttlInSeconds),或使用所选 SDK 中的等效 API 调用,以确保状态最终被删除。阅读 actor 概述以获取更多信息。
Dapr 支持应用程序状态的自动客户端加密,并支持密钥轮换。这适用于所有 Dapr 状态存储。有关更多信息,请阅读如何:加密应用程序状态主题。
不同的应用程序在共享状态方面的需求各不相同。在一种场景中,您可能希望将所有状态封装在给定的应用程序中,并让 Dapr 为您管理访问。在另一种场景中,您可能希望两个应用程序处理相同的状态以获取和保存相同的键。
Dapr 使状态能够:
有关更多详细信息,请阅读如何:在应用程序之间共享状态
Dapr 使开发者能够使用发件箱模式在事务状态存储和任何消息代理之间实现单个事务。有关更多信息,请阅读如何启用事务性发件箱消息传递
有两种查询状态的方式:
使用_可选的_状态管理查询 API,您可以查询保存在状态存储中的键值数据,而不管底层数据库或存储技术如何。使用状态管理查询 API,您可以过滤、排序和分页键值数据。有关更多详细信息,请阅读如何:查询状态。
Dapr 保存和检索状态值时不进行任何转换。您可以直接从底层状态存储查询和聚合状态。 例如,要在 Redis 中获取与应用程序 ID “myApp” 关联的所有状态键,请使用:
KEYS "myApp*"
如果数据存储支持 SQL 查询,您可以使用 SQL 查询 actor 的状态。例如:
SELECT * FROM StateTable WHERE Id='<app-id>||<actor-type>||<actor-id>||<key>'
您还可以通过跨 actor 实例执行聚合查询来避免 actor 框架的常见基于回合的并发限制。例如,要计算所有温度计 actor 的平均温度,请使用:
SELECT AVG(value) FROM StateTable WHERE Id LIKE '<app-id>||<thermometer>||*||temperature'
Dapr 支持每个状态设置请求的生存时间(TTL)。这意味着应用程序可以为每个存储的状态设置生存时间,这些状态在过期后无法检索。
状态管理 API 可在状态管理 API 参考中找到,该参考描述了如何通过提供键来检索、保存、删除和查询状态值。
想要测试 Dapr 状态管理 API?通过以下快速入门和教程了解状态管理的实际应用:
| 快速入门/教程 | 描述 |
|---|---|
| 状态管理快速入门 | 使用状态管理 API 创建有状态的应用程序。 |
| Hello World | 推荐 演示如何在本地运行 Dapr。突出显示服务调用和状态管理。 |
| Hello World Kubernetes | 推荐 演示如何在 Kubernetes 中运行 Dapr。突出显示服务调用和_状态管理_。 |
想跳过快速入门?没问题。您可以直接在应用程序中试用状态管理构建块。安装 Dapr后,您可以从状态管理操作指南开始使用状态管理 API。
状态管理是任何新应用程序、遗留应用程序、单体应用程序或微服务应用程序最常见的需求之一。处理和测试不同的数据库库以及处理重试和故障可能既困难又耗时。
在本指南中,您将学习使用键/值状态 API 的基础知识,使应用程序能够保存、获取和删除状态。
下面的代码示例大致描述了一个处理订单的应用程序,该应用程序使用带有 Dapr 边车的订单处理服务。订单处理服务使用 Dapr 将状态存储在 Redis 状态存储中。

状态存储组件代表 Dapr 用于与数据库通信的资源。
为了本指南的目的,我们将使用 Redis 状态存储,但支持列表中的任何状态存储都可以使用。
当您在自托管模式下运行 dapr init 时,Dapr 会创建一个默认的 Redis statestore.yaml 并在您的本地机器上运行 Redis 状态存储,位于:
%UserProfile%\.dapr\components\statestore.yaml~/.dapr/components/statestore.yaml使用 statestore.yaml 组件,您可以轻松替换底层组件,而无需更改应用程序代码。
要将其部署到 Kubernetes 集群,请在下面的 YAML 中填写状态存储组件的 metadata 连接详细信息,保存为 statestore.yaml,然后运行 kubectl apply -f statestore.yaml。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
app-id,因为状态键会以此值作为前缀。如果您不设置 app-id,系统会在运行时为您生成一个。下次运行该命令时,会生成一个新的 app-id,您将无法访问之前保存的状态。下面的示例展示了如何使用 Dapr 状态管理 API 保存和检索单个键/值对。
using System.Text;
using System.Threading.Tasks;
using Dapr.Client;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
var random = new Random();
//从依赖注入注册中解析 DaprClient
using var client = app.Services.GetRequiredService<DaprClient>();
while(true)
{
await Task.Delay(TimeSpan.FromSeconds(5));
var orderId = random.Next(1,1000);
//使用 Dapr SDK 保存和获取状态
await client.SaveStateAsync(DAPR_STORE_NAME, "order_1", orderId.ToString());
await client.SaveStateAsync(DAPR_STORE_NAME, "order_2", orderId.ToString());
var result = await client.GetStateAsync<string>(DAPR_STORE_NAME, "order_1");
Console.WriteLine($"Result after get: {result}");
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run
//依赖项
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.State;
import io.dapr.client.domain.TransactionalStateOperation;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//代码
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
private static final String STATE_STORE_NAME = "statestore";
public static void main(String[] args) throws InterruptedException{
while(true) {
TimeUnit.MILLISECONDS.sleep(5000);
Random random = new Random();
int orderId = random.nextInt(1000-1) + 1;
DaprClient client = new DaprClientBuilder().build();
//使用 Dapr SDK 保存和获取状态
client.saveState(STATE_STORE_NAME, "order_1", Integer.toString(orderId)).block();
client.saveState(STATE_STORE_NAME, "order_2", Integer.toString(orderId)).block();
Mono<State<String>> result = client.getState(STATE_STORE_NAME, "order_1", String.class);
log.info("Result after get" + result);
}
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
#依赖项
import random
from time import sleep
import requests
import logging
from dapr.clients import DaprClient
from dapr.clients.grpc._state import StateItem
from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType
#代码
logging.basicConfig(level = logging.INFO)
DAPR_STORE_NAME = "statestore"
while True:
sleep(random.randrange(50, 5000) / 1000)
orderId = random.randint(1, 1000)
with DaprClient() as client:
#使用 Dapr SDK 保存和获取状态
client.save_state(DAPR_STORE_NAME, "order_1", str(orderId))
result = client.get_state(DAPR_STORE_NAME, "order_1")
logging.info('Result after get: ' + result.data.decode('utf-8'))
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 -- python3 OrderProcessingService.py
// 依赖项
import (
"context"
"log"
"math/rand"
"strconv"
"time"
dapr "github.com/dapr/go-sdk/client"
)
// 代码
func main() {
const STATE_STORE_NAME = "statestore"
rand.Seed(time.Now().UnixMicro())
for i := 0; i < 10; i++ {
orderId := rand.Intn(1000-1) + 1
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
err = client.SaveState(ctx, STATE_STORE_NAME, "order_1", []byte(strconv.Itoa(orderId)), nil)
if err != nil {
panic(err)
}
result, err := client.GetState(ctx, STATE_STORE_NAME, "order_1", nil)
if err != nil {
panic(err)
}
log.Println("Result after get:", string(result.Value))
time.Sleep(2 * time.Second)
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run OrderProcessingService.go
//依赖项
import { DaprClient, HttpMethod, CommunicationProtocolEnum } from '@dapr/dapr';
//代码
const daprHost = "127.0.0.1";
var main = function() {
for(var i=0;i<10;i++) {
sleep(5000);
var orderId = Math.floor(Math.random() * (1000 - 1) + 1);
start(orderId).catch((e) => {
console.error(e);
process.exit(1);
});
}
}
async function start(orderId) {
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
const STATE_STORE_NAME = "statestore";
//使用 Dapr SDK 保存和获取状态
await client.state.save(STATE_STORE_NAME, [
{
key: "order_1",
value: orderId.toString()
},
{
key: "order_2",
value: orderId.toString()
}
]);
var result = await client.state.get(STATE_STORE_NAME, "order_1");
console.log("Result after get: " + result);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
main();
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 npm start
启动 Dapr 边车:
dapr run --app-id orderprocessing --dapr-http-port 3601
在单独的终端中,将一个键/值对保存到您的状态存储中:
curl -X POST -H "Content-Type: application/json" -d '[{ "key": "order_1", "value": "250"}]' http://localhost:3601/v1.0/state/statestore
现在获取您刚刚保存的状态:
curl http://localhost:3601/v1.0/state/statestore/order_1
重启您的边车并尝试再次检索状态,以观察状态独立于应用程序持久存在。
启动 Dapr 边车:
dapr --app-id orderprocessing --dapr-http-port 3601 run
在单独的终端中,将一个键/值对保存到您的状态存储中:
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '[{"key": "order_1", "value": "250"}]' -Uri 'http://localhost:3601/v1.0/state/statestore'
现在获取您刚刚保存的状态:
Invoke-RestMethod -Uri 'http://localhost:3601/v1.0/state/statestore/order_1'
重启您的边车并尝试再次检索状态,以观察状态独立于应用程序持久存在。
以下是利用 Dapr SDK 删除状态的代码示例。
using Dapr.Client;
using System.Threading.Tasks;
const string DAPR_STORE_NAME = "statestore";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
//从依赖注入注册中解析 DaprClient
using var client = app.Services.GetRequiredService<DaprClient>();
//使用 DaprClient 删除状态
await client.DeleteStateAsync(DAPR_STORE_NAME, "order_1", cancellationToken: cancellationToken);
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run
//依赖项
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//代码
@SpringBootApplication
public class OrderProcessingServiceApplication {
public static void main(String[] args) throws InterruptedException{
String STATE_STORE_NAME = "statestore";
//使用 Dapr SDK 删除状态
DaprClient client = new DaprClientBuilder().build();
String storedEtag = client.getState(STATE_STORE_NAME, "order_1", String.class).block().getEtag();
client.deleteState(STATE_STORE_NAME, "order_1", storedEtag, null).block();
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
#依赖项
from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType
#代码
logging.basicConfig(level = logging.INFO)
DAPR_STORE_NAME = "statestore"
#使用 Dapr SDK 删除状态
with DaprClient() as client:
client.delete_state(store_name=DAPR_STORE_NAME, key="order_1")
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 -- python3 OrderProcessingService.py
//依赖项
import (
"context"
dapr "github.com/dapr/go-sdk/client"
)
//代码
func main() {
STATE_STORE_NAME := "statestore"
//使用 Dapr SDK 删除状态
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
if err := client.DeleteState(ctx, STATE_STORE_NAME, "order_1"); err != nil {
panic(err)
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run OrderProcessingService.go
//依赖项
import { DaprClient, HttpMethod, CommunicationProtocolEnum } from '@dapr/dapr';
//代码
const daprHost = "127.0.0.1";
var main = function() {
const STATE_STORE_NAME = "statestore";
//使用 Dapr SDK 保存和获取状态
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
await client.state.delete(STATE_STORE_NAME, "order_1");
}
main();
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 npm start
使用与上面相同的 Dapr 实例运行,执行以下命令:
curl -X DELETE 'http://localhost:3601/v1.0/state/statestore/order_1'
尝试再次获取状态。请注意,没有返回任何值。
使用与上面相同的 Dapr 实例运行,执行以下命令:
Invoke-RestMethod -Method Delete -Uri 'http://localhost:3601/v1.0/state/statestore/order_1'
尝试再次获取状态。请注意,没有返回任何值。
以下是利用 Dapr SDK 保存和检索多个状态的代码示例。
using Dapr.Client;
using System.Threading.Tasks;
const string DAPR_STORE_NAME = "statestore";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
//从依赖注入注册中解析 DaprClient
using var client = app.Services.GetRequiredService<DaprClient>();
IReadOnlyList<BulkStateItem> multipleStateResult = await client.GetBulkStateAsync(DAPR_STORE_NAME, new List<string> { "order_1", "order_2" }, parallelism: 1);
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run
上面的示例返回一个 BulkStateItem,其中包含您保存到状态的值的序列化格式。如果您希望 SDK 在每个批量响应项中对值进行反序列化,可以改为使用以下代码:
using Dapr.Client;
using System.Threading.Tasks;
const string DAPR_STORE_NAME = "statestore";
var builder = WebApplication.CreateBuilder(args);
builder.Serivces.AddDaprClient();
var app = builder.Build();
//从依赖注入注册中解析 DaprClient
using var client = app.Services.GetRequiredService<DaprClient>();
IReadOnlyList<BulkStateItem<Widget>> mulitpleStateResult = await client.GetBulkStateAsync<Widget>(DAPR_STORE_NAME, new List<string> { "widget_1", "widget_2" }, parallelism: 1);
record Widget(string Size, string Color);
//依赖项
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.State;
import java.util.Arrays;
//代码
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
public static void main(String[] args) throws InterruptedException{
String STATE_STORE_NAME = "statestore";
//使用 Dapr SDK 检索多个状态
DaprClient client = new DaprClientBuilder().build();
Mono<List<State<String>>> resultBulk = client.getBulkState(STATE_STORE_NAME,
Arrays.asList("order_1", "order_2"), String.class);
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
#依赖项
from dapr.clients import DaprClient
from dapr.clients.grpc._state import StateItem
#代码
logging.basicConfig(level = logging.INFO)
DAPR_STORE_NAME = "statestore"
orderId = 100
#使用 Dapr SDK 保存和检索多个状态
with DaprClient() as client:
client.save_bulk_state(store_name=DAPR_STORE_NAME, states=[StateItem(key="order_2", value=str(orderId))])
result = client.get_bulk_state(store_name=DAPR_STORE_NAME, keys=["order_1", "order_2"], states_metadata={"metakey": "metavalue"}).items
logging.info('Result after get bulk: ' + str(result))
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 -- python3 OrderProcessingService.py
// 依赖项
import (
"context"
"log"
"math/rand"
"strconv"
"time"
dapr "github.com/dapr/go-sdk/client"
)
// 代码
func main() {
const STATE_STORE_NAME = "statestore"
rand.Seed(time.Now().UnixMicro())
for i := 0; i < 10; i++ {
orderId := rand.Intn(1000-1) + 1
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
err = client.SaveState(ctx, STATE_STORE_NAME, "order_1", []byte(strconv.Itoa(orderId)), nil)
if err != nil {
panic(err)
}
keys := []string{"key1", "key2", "key3"}
items, err := client.GetBulkState(ctx, STATE_STORE_NAME, keys, nil, 100)
if err != nil {
panic(err)
}
for _, item := range items {
log.Println("Item from GetBulkState:", string(item.Value))
}
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run OrderProcessingService.go
//依赖项
import { DaprClient, HttpMethod, CommunicationProtocolEnum } from '@dapr/dapr';
//代码
const daprHost = "127.0.0.1";
var main = function() {
const STATE_STORE_NAME = "statestore";
var orderId = 100;
//使用 Dapr SDK 保存和检索多个状态
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
await client.state.save(STATE_STORE_NAME, [
{
key: "order_1",
value: orderId.toString()
},
{
key: "order_2",
value: orderId.toString()
}
]);
result = await client.state.getBulk(STATE_STORE_NAME, ["order_1", "order_2"]);
}
main();
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 npm start
使用与上面相同的 Dapr 实例运行,将两个键/值对保存到您的状态存储中:
curl -X POST -H "Content-Type: application/json" -d '[{ "key": "order_1", "value": "250"}, { "key": "order_2", "value": "550"}]' http://localhost:3601/v1.0/state/statestore
现在获取您刚刚保存的状态:
curl -X POST -H "Content-Type: application/json" -d '{"keys":["order_1", "order_2"]}' http://localhost:3601/v1.0/state/statestore/bulk
使用与上面相同的 Dapr 实例运行,将两个键/值对保存到您的状态存储中:
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '[{ "key": "order_1", "value": "250"}, { "key": "order_2", "value": "550"}]' -Uri 'http://localhost:3601/v1.0/state/statestore'
现在获取您刚刚保存的状态:
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{"keys":["order_1", "order_2"]}' -Uri 'http://localhost:3601/v1.0/state/statestore/bulk'
以下是利用 Dapr SDK 执行状态事务的代码示例。
using Dapr.Client;
using System.Threading.Tasks;
const string DAPR_STORE_NAME = "statestore";
var builder = WebApplication.CreateBuilder(args);
builder.Serivces.AddDaprClient();
var app = builder.Build();
//从依赖注入注册中解析 DaprClient
using var client = app.Services.GetRequiredService<DaprClient>();
var random = new Random();
while (true)
{
await Task.Delay(TimeSpan.FromSeconds(5));
var orderId = random.Next(1, 1000);
var requests = new List<StateTransactionRequest>
{
new StateTransactionRequest("order_3", JsonSerializer.SerializeToUtf8Bytes(orderId.ToString()), StateOperationType.Upsert),
new StateTransactionRequest("order_2", null, StateOperationType.Delete)
};
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
//使用 DaprClient 执行状态事务
await client.ExecuteStateTransactionAsync(DAPR_STORE_NAME, requests, cancellationToken: cancellationToken);
Console.WriteLine($"Order requested: {orderId}");
Console.WriteLine($"Result: {result}");
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run
//依赖项
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.State;
import io.dapr.client.domain.TransactionalStateOperation;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//代码
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
private static final String STATE_STORE_NAME = "statestore";
public static void main(String[] args) throws InterruptedException{
while(true) {
TimeUnit.MILLISECONDS.sleep(5000);
Random random = new Random();
int orderId = random.nextInt(1000-1) + 1;
DaprClient client = new DaprClientBuilder().build();
List<TransactionalStateOperation<?>> operationList = new ArrayList<>();
operationList.add(new TransactionalStateOperation<>(TransactionalStateOperation.OperationType.UPSERT,
new State<>("order_3", Integer.toString(orderId), "")));
operationList.add(new TransactionalStateOperation<>(TransactionalStateOperation.OperationType.DELETE,
new State<>("order_2")));
//使用 Dapr SDK 执行状态事务
client.executeStateTransaction(STATE_STORE_NAME, operationList).block();
log.info("Order requested: " + orderId);
}
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 mvn spring-boot:run
#依赖项
import random
from time import sleep
import requests
import logging
from dapr.clients import DaprClient
from dapr.clients.grpc._state import StateItem
from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType
#代码
logging.basicConfig(level = logging.INFO)
DAPR_STORE_NAME = "statestore"
while True:
sleep(random.randrange(50, 5000) / 1000)
orderId = random.randint(1, 1000)
with DaprClient() as client:
#使用 Dapr SDK 执行状态事务
client.execute_state_transaction(store_name=DAPR_STORE_NAME, operations=[
TransactionalStateOperation(
operation_type=TransactionOperationType.upsert,
key="order_3",
data=str(orderId)),
TransactionalStateOperation(key="order_3", data=str(orderId)),
TransactionalStateOperation(
operation_type=TransactionOperationType.delete,
key="order_2",
data=str(orderId)),
TransactionalStateOperation(key="order_2", data=str(orderId))
])
client.delete_state(store_name=DAPR_STORE_NAME, key="order_1")
logging.basicConfig(level = logging.INFO)
logging.info('Order requested: ' + str(orderId))
logging.info('Result: ' + str(result))
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 -- python3 OrderProcessingService.py
// 依赖项
package main
import (
"context"
"log"
"math/rand"
"strconv"
"time"
dapr "github.com/dapr/go-sdk/client"
)
// 代码
func main() {
const STATE_STORE_NAME = "statestore"
rand.Seed(time.Now().UnixMicro())
for i := 0; i < 10; i++ {
orderId := rand.Intn(1000-1) + 1
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
err = client.SaveState(ctx, STATE_STORE_NAME, "order_1", []byte(strconv.Itoa(orderId)), nil)
if err != nil {
panic(err)
}
result, err := client.GetState(ctx, STATE_STORE_NAME, "order_1", nil)
if err != nil {
panic(err)
}
ops := make([]*dapr.StateOperation, 0)
data1 := "data1"
data2 := "data2"
op1 := &dapr.StateOperation{
Type: dapr.StateOperationTypeUpsert,
Item: &dapr.SetStateItem{
Key: "key1",
Value: []byte(data1),
},
}
op2 := &dapr.StateOperation{
Type: dapr.StateOperationTypeDelete,
Item: &dapr.SetStateItem{
Key: "key2",
Value: []byte(data2),
},
}
ops = append(ops, op1, op2)
meta := map[string]string{}
err = client.ExecuteStateTransaction(ctx, STATE_STORE_NAME, meta, ops)
log.Println("Result after get:", string(result.Value))
time.Sleep(2 * time.Second)
}
}
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run OrderProcessingService.go
//依赖项
import { DaprClient, HttpMethod, CommunicationProtocolEnum } from '@dapr/dapr';
//代码
const daprHost = "127.0.0.1";
var main = function() {
for(var i=0;i<10;i++) {
sleep(5000);
var orderId = Math.floor(Math.random() * (1000 - 1) + 1);
start(orderId).catch((e) => {
console.error(e);
process.exit(1);
});
}
}
async function start(orderId) {
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
const STATE_STORE_NAME = "statestore";
//使用 Dapr SDK 保存和检索多个状态
await client.state.transaction(STATE_STORE_NAME, [
{
operation: "upsert",
request: {
key: "order_3",
value: orderId.toString()
}
},
{
operation: "delete",
request: {
key: "order_2"
}
}
]);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
main();
要为上述示例应用程序启动 Dapr 边车,请运行类似以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 npm start
使用与上面相同的 Dapr 实例运行,执行两个状态事务:
curl -X POST -H "Content-Type: application/json" -d '{"operations": [{"operation":"upsert", "request": {"key": "order_1", "value": "250"}}, {"operation":"delete", "request": {"key": "order_2"}}]}' http://localhost:3601/v1.0/state/statestore/transaction
现在查看您的状态事务的结果:
curl -X POST -H "Content-Type: application/json" -d '{"keys":["order_1", "order_2"]}' http://localhost:3601/v1.0/state/statestore/bulk
使用与上面相同的 Dapr 实例运行,将两个键/值对保存到您的状态存储中:
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{"operations": [{"operation":"upsert", "request": {"key": "order_1", "value": "250"}}, {"operation":"delete", "request": {"key": "order_2"}}]}' -Uri 'http://localhost:3601/v1.0/state/statestore/transaction'
现在查看您的状态事务的结果:
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '{"keys":["order_1", "order_2"]}' -Uri 'http://localhost:3601/v1.0/state/statestore/bulk'
通过状态查询 API,你可以检索、筛选和排序存储在状态存储组件中的键/值数据。查询 API 不是完整查询语言的替代品。
尽管状态存储是键/值存储,但 value 可能是一个具有自身层次结构、键和值的 JSON 文档。查询 API 允许你使用这些键/值来检索相应的文档。
通过 HTTP POST/PUT 或 gRPC 提交查询请求。请求体是包含 3 个条目的 JSON 映射:
filtersortpagefilterfilter 以树的形式指定查询条件,其中每个节点代表一元或多操作数操作。
支持以下操作:
| 操作符 | 操作数 | 描述 |
|---|---|---|
EQ | key:value | key == value |
NEQ | key:value | key != value |
GT | key:value | key > value |
GTE | key:value | key >= value |
LT | key:value | key < value |
LTE | key:value | key <= value |
IN | key:[]value | key == value[0] OR key == value[1] OR … OR key == value[n] |
AND | []operation | operation[0] AND operation[1] AND … AND operation[n] |
OR | []operation | operation[0] OR operation[1] OR … OR operation[n] |
操作数中的 key 类似于 JSONPath 表示法。键中的每个点表示嵌套的 JSON 结构。例如,考虑以下结构:
{
"shape": {
"name": "rectangle",
"dimensions": {
"height": 24,
"width": 10
},
"color": {
"name": "red",
"code": "#FF0000"
}
}
}
要比较颜色代码的值,键将是 shape.color.code。
如果省略 filter 部分,查询将返回所有条目。
sortsort 是一个有序的 key:order 对数组,其中:
key 是状态存储中的键order 是一个可选字符串,指示排序顺序:"ASC" 表示升序"DESC" 表示降序pagepage 包含 limit 和 token 参数。
limit 设置页面大小。token 是组件返回的迭代令牌,用于后续查询。在后台,此查询请求被转换为原生查询语言并由状态存储组件执行。
让我们看一些从简单到复杂的实际示例。
作为数据集,考虑一个包含员工 ID、组织、州和城市的 员工记录集合。请注意,该数据集是一个键/值对数组,其中:
key 是唯一 IDvalue 是包含员工记录的 JSON 对象。为了更好地说明功能,组织名称 和员工 ID 是一个嵌套的 JSON person 对象。
首先创建一个 MongoDB 实例作为你的状态存储。
docker run -d --rm -p 27017:27017 --name mongodb mongo:5
接下来,启动一个 Dapr 应用程序。请参阅 组件配置文件,该文件指示 Dapr 使用 MongoDB 作为其状态存储。
dapr run --app-id demo --dapr-http-port 3500 --resources-path query-api-examples/components/mongodb
使用员工数据集填充状态存储,以便稍后查询。
curl -X POST -H "Content-Type: application/json" -d @query-api-examples/dataset.json http://localhost:3500/v1.0/state/statestore
填充完成后,你可以检查状态存储中的数据。在下图中,MongoDB UI 的一部分显示了员工记录。

每个条目都有 _id 成员作为连接的对象键,以及包含 JSON 记录的 value 成员。
查询 API 允许你从此 JSON 结构中选择记录。
现在你可以运行示例查询了。
首先,查找加利福尼亚州的所有员工,并按员工 ID 降序排序。
这是 查询:
{
"filter": {
"EQ": { "state": "CA" }
},
"sort": [
{
"key": "person.id",
"order": "DESC"
}
]
}
此查询的 SQL 等效形式为:
SELECT * FROM c WHERE
state = "CA"
ORDER BY
person.id DESC
使用以下命令执行查询:
curl -s -X POST -H "Content-Type: application/json" -d @query-api-examples/query1.json http://localhost:3500/v1.0-alpha1/state/statestore/query | jq .
Invoke-RestMethod -Method Post -ContentType 'application/json' -InFile query-api-examples/query1.json -Uri 'http://localhost:3500/v1.0-alpha1/state/statestore/query'
查询结果是按请求顺序排列的匹配键/值对数组:
{
"results": [
{
"key": "3",
"data": {
"person": {
"org": "Finance",
"id": 1071
},
"city": "Sacramento",
"state": "CA"
},
"etag": "44723d41-deb1-4c23-940e-3e6896c3b6f7"
},
{
"key": "7",
"data": {
"city": "San Francisco",
"state": "CA",
"person": {
"id": 1015,
"org": "Dev Ops"
}
},
"etag": "0e69e69f-3dbc-423a-9db8-26767fcd2220"
},
{
"key": "5",
"data": {
"state": "CA",
"person": {
"org": "Hardware",
"id": 1007
},
"city": "Los Angeles"
},
"etag": "f87478fa-e5c5-4be0-afa5-f9f9d75713d8"
},
{
"key": "9",
"data": {
"person": {
"org": "Finance",
"id": 1002
},
"city": "San Diego",
"state": "CA"
},
"etag": "f5cf05cd-fb43-4154-a2ec-445c66d5f2f8"
}
]
}
现在,查找来自 “Dev Ops” 和 “Hardware” 组织的所有员工。
这是 查询:
{
"filter": {
"IN": { "person.org": [ "Dev Ops", "Hardware" ] }
}
}
此查询的 SQL 等效形式为:
SELECT * FROM c WHERE
person.org IN ("Dev Ops", "Hardware")
使用以下命令执行查询:
curl -s -X POST -H "Content-Type: application/json" -d @query-api-examples/query2.json http://localhost:3500/v1.0-alpha1/state/statestore/query | jq .
Invoke-RestMethod -Method Post -ContentType 'application/json' -InFile query-api-examples/query2.json -Uri 'http://localhost:3500/v1.0-alpha1/state/statestore/query'
与上一个示例类似,结果是匹配键/值对的数组。
在此示例中,查找:
此外,首先按州按字母降序排序,然后按员工 ID 升序排序。让我们一次处理最多 3 条记录。
这是 查询:
{
"filter": {
"OR": [
{
"EQ": { "person.org": "Dev Ops" }
},
{
"AND": [
{
"EQ": { "person.org": "Finance" }
},
{
"IN": { "state": [ "CA", "WA" ] }
}
]
}
]
},
"sort": [
{
"key": "state",
"order": "DESC"
},
{
"key": "person.id"
}
],
"page": {
"limit": 3
}
}
此查询的 SQL 等效形式为:
SELECT * FROM c WHERE
person.org = "Dev Ops" OR
(person.org = "Finance" AND state IN ("CA", "WA"))
ORDER BY
state DESC,
person.id ASC
LIMIT 3
使用以下命令执行查询:
curl -s -X POST -H "Content-Type: application/json" -d @query-api-examples/query3.json http://localhost:3500/v1.0-alpha1/state/statestore/query | jq .
Invoke-RestMethod -Method Post -ContentType 'application/json' -InFile query-api-examples/query3.json -Uri 'http://localhost:3500/v1.0-alpha1/state/statestore/query'
成功执行后,状态存储会返回一个 JSON 对象,其中包含匹配记录列表和分页令牌:
{
"results": [
{
"key": "1",
"data": {
"person": {
"org": "Dev Ops",
"id": 1036
},
"city": "Seattle",
"state": "WA"
},
"etag": "6f54ad94-dfb9-46f0-a371-e42d550adb7d"
},
{
"key": "4",
"data": {
"person": {
"org": "Dev Ops",
"id": 1042
},
"city": "Spokane",
"state": "WA"
},
"etag": "7415707b-82ce-44d0-bf15-6dc6305af3b1"
},
{
"key": "10",
"data": {
"person": {
"org": "Dev Ops",
"id": 1054
},
"city": "New York",
"state": "NY"
},
"etag": "26bbba88-9461-48d1-8a35-db07c374e5aa"
}
],
"token": "3"
}
分页令牌在 后续查询 中"按原样"使用,以获取下一批记录:
{
"filter": {
"OR": [
{
"EQ": { "person.org": "Dev Ops" }
},
{
"AND": [
{
"EQ": { "person.org": "Finance" }
},
{
"IN": { "state": [ "CA", "WA" ] }
}
]
}
]
},
"sort": [
{
"key": "state",
"order": "DESC"
},
{
"key": "person.id"
}
],
"page": {
"limit": 3,
"token": "3"
}
}
curl -s -X POST -H "Content-Type: application/json" -d @query-api-examples/query3-token.json http://localhost:3500/v1.0-alpha1/state/statestore/query | jq .
Invoke-RestMethod -Method Post -ContentType 'application/json' -InFile query-api-examples/query3-token.json -Uri 'http://localhost:3500/v1.0-alpha1/state/statestore/query'
此查询的结果为:
{
"results": [
{
"key": "9",
"data": {
"person": {
"org": "Finance",
"id": 1002
},
"city": "San Diego",
"state": "CA"
},
"etag": "f5cf05cd-fb43-4154-a2ec-445c66d5f2f8"
},
{
"key": "7",
"data": {
"city": "San Francisco",
"state": "CA",
"person": {
"id": 1015,
"org": "Dev Ops"
}
},
"etag": "0e69e69f-3dbc-423a-9db8-26767fcd2220"
},
{
"key": "3",
"data": {
"person": {
"org": "Finance",
"id": 1071
},
"city": "Sacramento",
"state": "CA"
},
"etag": "44723d41-deb1-4c23-940e-3e6896c3b6f7"
}
],
"token": "6"
}
通过这种方式,你可以在查询中更新分页令牌并遍历结果,直到不再返回记录。
状态查询 API 具有以下限制:
你可以在 相关链接 部分找到其他信息。
在本文中,你将学习如何使用可选的并发和一致性模型创建可水平扩展的有状态服务。使用状态管理 API 可以让开发者无需处理复杂的状态协调、冲突解决和故障处理。
状态存储组件代表 Dapr 用于与数据库通信的资源。在本指南中,我们将使用默认的 Redis 状态存储。
在自托管模式下运行 dapr init 时,Dapr 会创建一个默认的 Redis statestore.yaml,并在本地机器上运行一个 Redis 状态存储,位于:
%UserProfile%\.dapr\components\statestore.yaml~/.dapr/components/statestore.yaml使用 statestore.yaml 组件,你可以轻松地交换底层组件,而无需更改应用程序代码。
请参阅支持的状态存储列表。
使用强一致性时,Dapr 确保底层状态存储:
对于 get 请求,Dapr 确保存储返回副本之间一致的最新数据。默认值为最终一致性,除非在状态 API 的请求中另有指定。
以下示例说明了如何使用强一致性保存、获取和删除状态。该示例使用 Python 编写,但适用于任何编程语言。
import requests
import json
store_name = "redis-store" # name of the state store as specified in state store component yaml file
dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
stateReq = '[{ "key": "k1", "value": "Some Data", "options": { "consistency": "strong" }}]'
response = requests.post(dapr_state_url, json=stateReq)
import requests
import json
store_name = "redis-store" # name of the state store as specified in state store component yaml file
dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
response = requests.get(dapr_state_url + "/key1", headers={"consistency":"strong"})
print(response.headers['ETag'])
import requests
import json
store_name = "redis-store" # name of the state store as specified in state store component yaml file
dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
response = requests.delete(dapr_state_url + "/key1", headers={"consistency":"strong"})
如果未指定 concurrency 选项,则默认为最后写入并发模式。
Dapr 允许开发者在处理数据存储时选择两种常见的并发模式:
Dapr 使用版本号来确定特定键是否已更新。你可以:
如果自获取版本号以来版本信息已更改,则会抛出错误,要求你执行另一次读取以获取最新版本信息和状态。
Dapr 利用 ETag 来确定状态的版本号。ETag 从状态请求中的 ETag 头返回。使用 ETag,你的应用程序可以通过在 ETag 不匹配时出错来知道资源自上次检查以来已更新。
以下示例说明如何:
以下示例使用 Python 编写,但适用于任何编程语言。
import requests
import json
store_name = "redis-store" # name of the state store as specified in state store component yaml file
dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
response = requests.get(dapr_state_url + "/key1", headers={"concurrency":"first-write"})
etag = response.headers['ETag']
newState = '[{ "key": "k1", "value": "New Data", "etag": {}, "options": { "concurrency": "first-write" }}]'.format(etag)
requests.post(dapr_state_url, json=newState)
response = requests.delete(dapr_state_url + "/key1", headers={"If-Match": "{}".format(etag)})
在以下示例中,你将看到当版本已更改时如何重试保存状态操作:
import requests
import json
# This method saves the state and returns false if failed to save state
def save_state(data):
try:
store_name = "redis-store" # name of the state store as specified in state store component yaml file
dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
response = requests.post(dapr_state_url, json=data)
if response.status_code == 200:
return True
except:
return False
return False
# This method gets the state and returns the response, with the ETag in the header -->
def get_state(key):
response = requests.get("http://localhost:3500/v1.0/state/<state_store_name>/{}".format(key), headers={"concurrency":"first-write"})
return response
# Exit when save state is successful. success will be False if there's an ETag mismatch -->
success = False
while success != True:
response = get_state("key1")
etag = response.headers['ETag']
newState = '[{ "key": "key1", "value": "New Data", "etag": {}, "options": { "concurrency": "first-write" }}]'.format(etag)
success = save_state(newState)
事务性 outbox 模式是一种众所周知的设计模式,用于发送与应用程序状态变更相关的通知。事务性 outbox 模式使用一个横跨数据库与消息代理的单一事务来投递通知。
开发者在尝试自行实现此模式时面临着许多困难的技术挑战,通常需要编写容易出错的核心协调管理器,最多只能支持一两组数据库与消息代理的组合。
例如,你可以使用 outbox 模式:
借助 Dapr 的 outbox 支持,当调用 Dapr 的事务 API创建或更新应用程序状态时,你可以通知订阅者。
下图是从高层次概述 outbox 功能的工作原理:

Dapr outbox 在两个流程中处理请求:用户请求流程与后台消息流程。二者共同确保状态与事件保持一致。

交互顺序如下:
应用程序调用 Dapr 状态管理 API,以事务方式写入状态。
这是业务数据(例如订单或资料更新)提交进行持久化的入口点。
Dapr 向一个内部 outbox 主题发布一条带有唯一事务 ID 的意向消息。
这条持久化记录确保在任何数据库提交发生之前,事件意向就已经存在。
状态与一个事务标记以原子方式写入同一个状态存储。
业务数据与标记在同一事务中提交,防止部分写入。
事务提交后,应用程序收到成功响应。
此时应用程序可以继续执行,已知状态已保存,事件意向得到保证。
后台订阅者读取意向消息。
当 outbox 启用时,Dapr 会启动消费者来处理内部 outbox 主题。
订阅者在状态存储中验证事务标记。
此项检查确认数据库提交已成功,然后才会进行外部发布。
验证后的业务事件被发布到外部发布订阅主题。
事件被发送到已配置的代理(Kafka、RabbitMQ 等),其他服务可以消费该事件。
标记从状态存储中清理(删除)。
事件成功投递后,这可防止数据库无限制地增长。
消息被确认并从内部主题中移除
如果发布或清理失败,Dapr 会重试,确保可靠的至少一次投递。
outbox 功能需要 Dapr 支持的事务型状态存储。
了解更多关于你可以使用的事务方法。
任何 Dapr 支持的发布订阅代理均可与 outbox 功能一起使用。
内部 outbox 主题
当启用 outbox 时,Dapr 会使用以下命名约定创建一个内部主题:{namespace}{appID}{topic}outbox,其中:
namespace:Dapr 应用程序命名空间(如果已配置)appID:Dapr 应用程序标识符topic:在 outboxPublishTopic 元数据中指定的值通过这种方式,每个 outbox 主题在每个应用程序和外部主题范围内被唯一标识,防止多租户环境中的路由冲突。
要启用 outbox 功能,请在状态存储组件上添加以下必填和可选字段:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mysql-outbox
spec:
type: state.mysql
version: v1
metadata:
- name: connectionString
value: "<CONNECTION STRING>"
- name: outboxPublishPubsub # 必填
value: "mypubsub"
- name: outboxPublishTopic # 必填
value: "newOrder"
- name: outboxPubsub # 可选
value: "myOutboxPubsub"
- name: outboxDiscardWhenMissingState #可选。默认为 false
value: false
| 名称 | 必填 | 默认值 | 描述 |
|---|---|---|---|
| outboxPublishPubsub | 是 | N/A | 设置发布订阅组件的名称,用于在发布状态变更时投递通知 |
| outboxPublishTopic | 是 | N/A | 设置在用 outboxPublishPubsub 配置的发布订阅上接收状态变更的主题。消息正文将是针对 insert 或 update 操作的状态事务项 |
| outboxPubsub | 否 | outboxPublishPubsub | 设置 Dapr 用于协调状态与发布订阅事务的发布订阅组件。如果未设置,则使用用 outboxPublishPubsub 配置的发布订阅组件。如果你想将用于发送通知状态变更的发布订阅组件与用于协调事务的组件分开,此设置会很有用 |
| outboxDiscardWhenMissingState | 否 | false | 通过将 outboxDiscardWhenMissingState 设置为 true,如果 Dapr 无法在数据库中找到状态,它将丢弃该事务并且不再重试。如果状态存储数据在 Dapr 能够投递消息之前因任何原因被删除,并且你希望 Dapr 从发布订阅中删除这些项并停止重试以获取状态,则此设置会很有用 |
如果你希望使用同一个状态存储来发送 outbox 与非 outbox 消息,只需定义两个连接到同一个状态存储的状态存储组件,其中一个启用 outbox 功能,另一个不启用。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mysql
spec:
type: state.mysql
version: v1
metadata:
- name: connectionString
value: "<CONNECTION STRING>"
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mysql-outbox
spec:
type: state.mysql
version: v1
metadata:
- name: connectionString
value: "<CONNECTION STRING>"
- name: outboxPublishPubsub # 必填
value: "mypubsub"
- name: outboxPublishTopic # 必填
value: "newOrder"
你可以通过设置另一个事务来覆盖发布到发布订阅代理的 outbox 模式消息,该事务不会被保存到数据库,并且被明确标记为投影(projection)。该事务会添加一个名为 outbox.projection 的元数据键,其值设置为 true。当添加到在事务中保存的状态数组时,写入状态时会忽略此负载,并将该数据用作发送到上游订阅者的负载。
若要正确使用,状态存储上的操作与消息投影之间的 key 值必须匹配。如果键不匹配,整个事务将失败。
如果你为同一个键启用了两个或更多 outbox.projection 状态项,则使用第一个定义的项,忽略其他项。
在以下状态事务的 Python SDK 示例中,值 "2" 被保存到数据库,而值 "3" 被发布到最终用户主题。
DAPR_STORE_NAME = "statestore"
async def main():
client = DaprClient()
client.execute_state_transaction(
store_name=DAPR_STORE_NAME,
operations=[
# 定义第一个状态操作以保存值 "2"
TransactionalStateOperation(
key='key1', data='2', metadata={'outbox.projection': 'false'}
),
# 定义第二个状态操作以发布带有元数据的值 "3"
TransactionalStateOperation(
key='key1', data='3', metadata={'outbox.projection': 'true'}
),
],
)
print("State transaction executed.")
通过将元数据项 "outbox.projection" 设置为 "true" 并确保 key 值匹配(key1):
在以下状态事务的 JavaScript SDK 示例中,值 "2" 被保存到数据库,而值 "3" 被发布到最终用户主题。
const { DaprClient, StateOperationType } = require('@dapr/dapr');
const DAPR_STORE_NAME = "statestore";
async function main() {
const client = new DaprClient();
// 定义第一个状态操作以保存值 "2"
const op1 = {
operation: StateOperationType.UPSERT,
request: {
key: "key1",
value: "2"
}
};
// 定义第二个状态操作以发布带有元数据的值 "3"
const op2 = {
operation: StateOperationType.UPSERT,
request: {
key: "key1",
value: "3",
metadata: {
"outbox.projection": "true"
}
}
};
// 创建状态操作列表
const ops = [op1, op2];
// 执行状态事务
await client.state.transaction(DAPR_STORE_NAME, ops);
console.log("State transaction executed.");
}
main().catch(err => {
console.error(err);
});
通过将元数据项 "outbox.projection" 设置为 "true" 并确保 key 值匹配(key1):
在以下状态事务的 .NET SDK 示例中,值 "2" 被保存到数据库,而值 "3" 被发布到最终用户主题。
public class Program
{
private const string DAPR_STORE_NAME = "statestore";
public static async Task Main(string[] args)
{
var client = new DaprClientBuilder().Build();
// 定义第一个状态操作以保存值 "2"
var op1 = new StateTransactionRequest(
key: "key1",
value: Encoding.UTF8.GetBytes("2"),
operationType: StateOperationType.Upsert
);
// 定义第二个状态操作以发布带有元数据的值 "3"
var metadata = new Dictionary<string, string>
{
{ "outbox.projection", "true" }
};
var op2 = new StateTransactionRequest(
key: "key1",
value: Encoding.UTF8.GetBytes("3"),
operationType: StateOperationType.Upsert,
metadata: metadata
);
// 创建状态操作列表
var ops = new List<StateTransactionRequest> { op1, op2 };
// 执行状态事务
await client.ExecuteStateTransactionAsync(DAPR_STORE_NAME, ops);
Console.WriteLine("State transaction executed.");
}
}
通过将元数据项 "outbox.projection" 设置为 "true" 并确保 key 值匹配(key1):
在以下状态事务的 Java SDK 示例中,值 "2" 被保存到数据库,而值 "3" 被发布到最终用户主题。
public class Main {
private static final String DAPR_STORE_NAME = "statestore";
public static void main(String[] args) {
try (DaprClient client = new DaprClientBuilder().build()) {
// 定义第一个状态操作以保存值 "2"
State<String> state1 = new State<>(
"key1",
"2",
null, // etag
null // 并发与一致性选项
);
// 定义第二个状态操作以发布带有元数据的值 "3"
Map<String, String> metadata = new HashMap<>();
metadata.put("outbox.projection", "true");
State<String> state2 = new State<>(
"key1",
"3",
null, // etag
metadata,
null // 并发与一致性选项
);
TransactionalStateOperation<String> op1 = new TransactionalStateOperation<>(
TransactionalStateOperation.OperationType.UPSERT, state1
);
TransactionalStateOperation<String> op2 = new TransactionalStateOperation<>(
TransactionalStateOperation.OperationType.UPSERT, state2
);
// 创建事务状态操作列表
List<TransactionalStateOperation<?>> ops = new ArrayList<>();
ops.add(op1);
ops.add(op2);
// 配置事务请求,设置状态存储
ExecuteStateTransactionRequest transactionRequest = new ExecuteStateTransactionRequest(DAPR_STORE_NAME);
transactionRequest.setOperations(ops);
// 执行状态事务
client.executeStateTransaction(transactionRequest).block();
System.out.println("State transaction executed.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
通过将元数据项 "outbox.projection" 设置为 "true" 并确保 key 值匹配(key1):
在以下状态事务的 Go SDK 示例中,值 "2" 被保存到数据库,而值 "3" 被发布到最终用户主题。
ops := make([]*dapr.StateOperation, 0)
op1 := &dapr.StateOperation{
Type: dapr.StateOperationTypeUpsert,
Item: &dapr.SetStateItem{
Key: "key1",
Value: []byte("2"),
},
}
op2 := &dapr.StateOperation{
Type: dapr.StateOperationTypeUpsert,
Item: &dapr.SetStateItem{
Key: "key1",
Value: []byte("3"),
// 覆盖保存到数据库的数据负载
Metadata: map[string]string{
"outbox.projection": "true",
},
},
}
ops = append(ops, op1, op2)
meta := map[string]string{}
err := testClient.ExecuteStateTransaction(ctx, store, meta, ops)
通过将元数据项 "outbox.projection" 设置为 "true" 并确保 key 值匹配(key1):
你可以使用以下 HTTP 请求传递消息覆盖:
curl -X POST http://localhost:3500/v1.0/state/starwars/transaction \
-H "Content-Type: application/json" \
-d '{
"operations": [
{
"operation": "upsert",
"request": {
"key": "order1",
"value": {
"orderId": "7hf8374s",
"type": "book",
"name": "The name of the wind"
}
}
},
{
"operation": "upsert",
"request": {
"key": "order1",
"value": {
"orderId": "7hf8374s"
},
"metadata": {
"outbox.projection": "true"
},
"contentType": "application/json"
}
}
]
}'
通过将元数据项 "outbox.projection" 设置为 "true" 并确保 key 值匹配(key1):
你可以使用自定义 CloudEvent 元数据来覆盖已发布的 outbox 事件上的 Dapr 生成的 CloudEvent 字段。
async def execute_state_transaction():
async with DaprClient() as client:
# 定义状态操作
ops = []
op1 = {
'operation': 'upsert',
'request': {
'key': 'key1',
'value': b'2', # 将字符串转换为字节数组
'metadata': {
'cloudevent.id': 'unique-business-process-id',
'cloudevent.source': 'CustomersApp',
'cloudevent.type': 'CustomerCreated',
'cloudevent.subject': '123',
'my-custom-ce-field': 'abc'
}
}
}
ops.append(op1)
# 执行状态事务
store_name = 'your-state-store-name'
try:
await client.execute_state_transaction(store_name, ops)
print('State transaction executed.')
except Exception as e:
print('Error executing state transaction:', e)
# 运行异步函数
if __name__ == "__main__":
asyncio.run(execute_state_transaction())
const { DaprClient } = require('dapr-client');
async function executeStateTransaction() {
// 初始化 Dapr 客户端
const daprClient = new DaprClient();
// 定义状态操作
const ops = [];
const op1 = {
operationType: 'upsert',
request: {
key: 'key1',
value: Buffer.from('2'),
metadata: {
'id': 'unique-business-process-id',
'source': 'CustomersApp',
'type': 'CustomerCreated',
'subject': '123',
'my-custom-ce-field': 'abc'
}
}
};
ops.push(op1);
// 执行状态事务
const storeName = 'your-state-store-name';
const metadata = {};
}
executeStateTransaction();
public class StateOperationExample
{
public async Task ExecuteStateTransactionAsync()
{
var daprClient = new DaprClientBuilder().Build();
// 将值 "2" 定义为字符串并将其序列化为字节数组
var value = "2";
var valueBytes = JsonSerializer.SerializeToUtf8Bytes(value);
// 定义第一个状态操作以保存带有元数据的值 "2"
// 覆盖 Cloudevent 元数据
var metadata = new Dictionary<string, string>
{
{ "cloudevent.id", "unique-business-process-id" },
{ "cloudevent.source", "CustomersApp" },
{ "cloudevent.type", "CustomerCreated" },
{ "cloudevent.subject", "123" },
{ "my-custom-ce-field", "abc" }
};
var op1 = new StateTransactionRequest(
key: "key1",
value: valueBytes,
operationType: StateOperationType.Upsert,
metadata: metadata
);
// 创建状态操作列表
var ops = new List<StateTransactionRequest> { op1 };
// 执行状态事务
var storeName = "your-state-store-name";
await daprClient.ExecuteStateTransactionAsync(storeName, ops);
Console.WriteLine("State transaction executed.");
}
public static async Task Main(string[] args)
{
var example = new StateOperationExample();
await example.ExecuteStateTransactionAsync();
}
}
public class StateOperationExample {
public static void main(String[] args) {
executeStateTransaction();
}
public static void executeStateTransaction() {
// 构建 Dapr 客户端
try (DaprClient daprClient = new DaprClientBuilder().build()) {
// 覆盖 CloudEvent 元数据
Map<String, String> metadata = new HashMap<>();
metadata.put("cloudevent.id", "unique-business-process-id");
metadata.put("cloudevent.source", "CustomersApp");
metadata.put("cloudevent.type", "CustomerCreated");
metadata.put("cloudevent.subject", "123");
metadata.put("my-custom-ce-field", "abc");
State<String> state = new State<>(
"key1", // 定义键 "key1"
"value1", // 定义值 "value1"
null, // etag
metadata,
null // 并发与一致性选项
);
// 定义状态操作
List<TransactionalStateOperation<?>> ops = new ArrayList<>();
TransactionalStateOperation<String> op1 = new TransactionalStateOperation<>(
TransactionalStateOperation.OperationType.UPSERT,
state
);
ops.add(op1);
// 执行状态事务
String storeName = "your-state-store-name";
daprClient.executeStateTransaction(storeName, ops).block();
System.out.println("State transaction executed.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
func main() {
// 创建 Dapr 客户端
client, err := dapr.NewClient()
if err != nil {
log.Fatalf("failed to create Dapr client: %v", err)
}
defer client.Close()
ctx := context.Background()
store := "your-state-store-name"
// 定义状态操作
ops := make([]*dapr.StateOperation, 0)
op1 := &dapr.StateOperation{
Type: dapr.StateOperationTypeUpsert,
Item: &dapr.SetStateItem{
Key: "key1",
Value: []byte("2"),
// 覆盖 Cloudevent 元数据
Metadata: map[string]string{
"cloudevent.id": "unique-business-process-id",
"cloudevent.source": "CustomersApp",
"cloudevent.type": "CustomerCreated",
"cloudevent.subject": "123",
"my-custom-ce-field": "abc",
},
},
}
ops = append(ops, op1)
// 事务的元数据(如果有)
meta := map[string]string{}
// 执行状态事务
err = client.ExecuteStateTransaction(ctx, store, meta, ops)
if err != nil {
log.Fatalf("failed to execute state transaction: %v", err)
}
log.Println("State transaction executed.")
}
curl -X POST http://localhost:3500/v1.0/state/starwars/transaction \
-H "Content-Type: application/json" \
-d '{
"operations": [
{
"operation": "upsert",
"request": {
"key": "key1",
"value": "2"
}
},
],
"metadata": {
"id": "unique-business-process-id",
"source": "CustomersApp",
"type": "CustomerCreated",
"subject": "123",
"my-custom-ce-field": "abc",
}
}'
data CloudEvent 字段保留仅供 Dapr 使用,不可自定义。Dapr 提供了多种在应用程序之间共享状态的方式。
不同的架构在共享状态方面可能有不同的需求。在一种场景中,你可能希望:
在另一种场景中,你可能需要两个应用程序同时操作相同的状态,获取和保存相同的键。
为启用状态共享,Dapr 支持以下键前缀策略:
| 键前缀 | 描述 |
|---|---|
appid | 默认策略,允许你仅通过具有指定 appid 的应用程序管理状态。所有状态键都将以 appid 为前缀,并限定在该应用程序范围内。 |
name | 使用状态存储组件的名称作为前缀。多个应用程序可以共享给定状态存储的相同状态。 |
namespace | 如果设置,此设置会将 appid 键加上配置的命名空间作为前缀,从而得到一个限定在给定命名空间范围内的键。这允许具有相同 appid 但在不同命名空间中的应用重用相同的状态存储。如果未配置命名空间,该设置会回退到 appid 策略。有关 Dapr 中命名空间的更多信息,请参阅操作指南:将组件限定到一个或多个应用程序 |
none | 不使用前缀。多个应用程序在不同状态存储之间共享状态。 |
要指定前缀策略,请在状态组件上添加名为 keyPrefix 的元数据键:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
namespace: production
spec:
type: state.redis
version: v1
metadata:
- name: keyPrefix
value: <key-prefix-strategy>
以下示例演示了使用每种支持的前缀策略进行状态检索的情况。
appid(默认)在下面的示例中,应用程序 ID 为 myApp 的 Dapr 应用程序正在将状态保存到名为 redis 的状态存储中:
curl -X POST http://localhost:3500/v1.0/state/redis \
-H "Content-Type: application/json"
-d '[
{
"key": "darth",
"value": "nihilus"
}
]'
该键将保存为 myApp||darth。
namespace在命名空间 production 中运行、应用程序 ID 为 myApp 的 Dapr 应用程序正在将状态保存到名为 redis 的状态存储中:
curl -X POST http://localhost:3500/v1.0/state/redis \
-H "Content-Type: application/json"
-d '[
{
"key": "darth",
"value": "nihilus"
}
]'
该键将保存为 production.myApp||darth。
name在下面的示例中,应用程序 ID 为 myApp 的 Dapr 应用程序正在将状态保存到名为 redis 的状态存储中:
curl -X POST http://localhost:3500/v1.0/state/redis \
-H "Content-Type: application/json"
-d '[
{
"key": "darth",
"value": "nihilus"
}
]'
该键将保存为 redis||darth。
none在下面的示例中,应用程序 ID 为 myApp 的 Dapr 应用程序正在将状态保存到名为 redis 的状态存储中:
curl -X POST http://localhost:3500/v1.0/state/redis \
-H "Content-Type: application/json"
-d '[
{
"key": "darth",
"value": "nihilus"
}
]'
该键将保存为 darth。
对应用状态进行静态加密,以在企业工作负载或受监管环境中提供更强的安全性。Dapr 基于 Galois/Counter Mode (GCM) 中的 AES 提供自动客户端加密,支持 128、192 和 256 位密钥。
除了自动加密外,Dapr 还支持主加密密钥和辅助加密密钥,使开发人员和运维团队更容易启用密钥轮换策略。所有 Dapr 状态存储都支持此功能。
加密密钥始终从机密中获取,不能在 metadata 部分以纯文本值形式提供。
将以下 metadata 部分添加到任何 Dapr 支持的状态存储:
metadata:
- name: primaryEncryptionKey
secretKeyRef:
name: mysecret
key: mykey # key 是可选的。
例如,这是一个 Redis 加密状态存储的完整 YAML:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: primaryEncryptionKey
secretKeyRef:
name: mysecret
key: mykey
现在您已经配置了一个 Dapr 状态存储,可以从名为 mysecret 的机密中获取加密密钥,该机密在名为 mykey 的键中包含实际的加密密钥。
实际的加密密钥必须是有效的十六进制编码的加密密钥。虽然支持 192 位和 256 位密钥,但建议您使用 128 位加密密钥。如果加密密钥无效,Dapr 会报错并退出。
例如,您可以使用以下命令生成随机的十六进制编码的 128 位(16 字节)密钥:
openssl rand 16 | hexdump -v -e '/1 "%02x"'
# 结果将类似于 "cb321007ad11a9d23f963bff600d58e0"
请注意,机密存储不必支持键。
为了支持密钥轮换,Dapr 提供了一种指定辅助加密密钥的方法:
metadata:
- name: primaryEncryptionKey
secretKeyRef:
name: mysecret
key: mykey
- name: secondaryEncryptionKey
secretKeyRef:
name: mysecret2
key: mykey2
当 Dapr 启动时,它会获取包含 metadata 部分中列出的加密密钥的机密。Dapr 自动知道哪个状态项已使用哪个密钥加密,因为它将 secretKeyRef.name 字段附加到实际状态键的末尾。
要轮换密钥:
primaryEncryptionKey 更改为指向包含您新密钥的机密。secondaryEncryptionKey。新数据将使用新密钥加密,而任何检索到的旧数据将使用辅助密钥解密。
对使用旧密钥加密的数据项的任何更新都将使用新密钥重新加密。
Dapr 在保存和检索状态时不会转换状态值。Dapr 要求所有状态存储实现遵守特定的键格式方案(参见[状态管理规范](https://docs.dapr.io/zh-hans/reference/api/state_api/)。您可以直接与底层存储交互来操作状态数据,例如:
要连接到您的 Cosmos DB 实例,您可以:
要获取与应用程序 “myapp” 关联的所有状态键,请使用以下查询:
SELECT * FROM states WHERE CONTAINS(states.id, 'myapp||')
上述查询返回 id 包含 “myapp-” 的所有文档,这是状态键的前缀。
要获取应用程序 “myapp” 中键为 “balance” 的状态数据,请使用以下查询:
SELECT * FROM states WHERE states.id = 'myapp||balance'
读取返回文档中的 value 字段。要获取状态版本/ETag,请使用以下命令:
SELECT states._etag FROM states WHERE states.id = 'myapp||balance'
要获取应用程序 ID 为 “mypets” 中 Actor 类型为 “cat” 且实例 ID 为 “leroy” 的 Actor 关联的所有状态键,请使用以下命令:
SELECT * FROM states WHERE CONTAINS(states.id, 'mypets||cat||leroy||')
而要获取特定的 Actor 状态(如 “food”),请使用以下命令:
SELECT * FROM states WHERE states.id = 'mypets||cat||leroy||food'
Dapr 在保存和检索状态时不会转换状态值。Dapr 要求所有状态存储实现都遵循一定的键格式方案(参见状态管理规范)。您可以直接与底层存储交互来操作状态数据,例如:
Dapr 提供了一个 Redis state store component 作为可插拔组件。
您可以使用官方的 redis-cli 或任何其他兼容 Redis 的工具连接到 Redis 状态存储,直接查询 Dapr 状态。如果您是在容器中运行 Redis,使用 redis-cli 的最简单方法是通过容器:
docker run --rm -it --link <Redis 容器的名称> redis redis-cli -h <Redis 容器的名称>
要获取与应用 “myapp” 关联的所有状态键,请使用命令:
KEYS myapp*
上述命令返回现有键的列表,例如:
1) "myapp||balance"
2) "myapp||amount"
Dapr 将状态值保存为哈希值。每个哈希值包含一个 “data” 字段,其中包含:
例如,要获取应用 “myapp” 的键 “balance” 对应的状态数据,请使用命令:
HGET myapp||balance data
要获取状态版本/ETag,请使用命令:
HGET myapp||balance version
要获取与应用 ID “mypets” 中 actor 类型为 “cat” 且实例 ID 为 “leroy” 的 actor 关联的所有状态键,请使用命令:
KEYS mypets||cat||leroy*
要获取特定的 actor 状态(如 “food”),请使用命令:
HGET mypets||cat||leroy||food value
Dapr 在保存和检索状态时不会转换状态值。Dapr 要求所有状态存储实现都遵守一定的键格式方案(请参阅状态管理规范)。你可以直接与底层存储交互来操作状态数据,例如:
连接 SQL Server 实例最简单的方式是使用:
要获取与应用程序 “myapp” 关联的所有状态键,请使用以下查询:
SELECT * FROM states WHERE [Key] LIKE 'myapp||%'
上述查询返回所有 ID 包含 “myapp||” 的行,这是状态键的前缀。
要获取应用程序 “myapp” 中键为 “balance” 的状态数据,请使用以下查询:
SELECT * FROM states WHERE [Key] = 'myapp||balance'
读取返回行的 Data 字段。要获取状态版本/ETag,请使用以下命令:
SELECT [RowVersion] FROM states WHERE [Key] = 'myapp||balance'
要获取 JSON 数据中 “color” 值等于 “blue” 的所有状态数据,请使用以下查询:
SELECT * FROM states WHERE JSON_VALUE([Data], '$.color') = 'blue'
要获取属于应用程序 ID 为 “mypets”、Actor 类型为 “cat”、实例 ID 为 “leroy” 的所有 Actor 状态键,请使用以下命令:
SELECT * FROM states WHERE [Key] LIKE 'mypets||cat||leroy||%'
要获取特定的 Actor 状态(如 “food”),请使用以下命令:
SELECT * FROM states WHERE [Key] = 'mypets||cat||leroy||food'
Dapr 支持为每个状态设置请求级的生存时间(TTL)。这意味着应用程序可以为每个存储的状态设置 TTL,过期后将无法检索这些状态。
对于支持的 状态存储,您只需在发布消息时设置 ttlInSeconds 元数据。其他 状态存储 将忽略此值。对于某些 状态存储,您可以按表/容器指定默认过期时间。
当 状态存储 组件原生支持状态 TTL 时,Dapr 直接转发 TTL 配置,不添加任何额外逻辑,保持可预测的行为。当组件以不同方式处理过期状态时,这非常有用。
未指定 TTL 时,将保留 状态存储 的默认行为。
持久化状态适用于所有允许为所有数据指定默认 TTL 的 状态存储,无论是:
未指定特定 TTL 时,数据将在该全局 TTL 时间段后过期。这不由 Dapr 处理。
此外,所有 状态存储 还支持_显式_持久化数据的选项。这意味着您可以忽略默认数据库策略(可能在 Dapr 外部设置或通过 Dapr 组件设置)来无限期保留给定的数据库记录。您可以通过将 ttlInSeconds 设置为 -1 值来实现此目的。该值表示忽略任何设置的 TTL 值。
请参阅 状态存储 组件指南中的 TTL 列。
您可以在 状态存储 set 请求的元数据中设置状态 TTL:
#dependencies
from dapr.clients import DaprClient
#code
DAPR_STORE_NAME = "statestore"
with DaprClient() as client:
client.save_state(DAPR_STORE_NAME, "order_1", str(orderId), state_metadata={
'ttlInSeconds': '120'
})
要启动 Dapr 边车 并运行上述示例应用程序,您需要运行类似于以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 -- python3 OrderProcessingService.py
// dependencies
using Dapr.Client;
// code
await client.SaveStateAsync(storeName, stateKeyName, state, metadata: new Dictionary<string, string>() {
{
"ttlInSeconds", "120"
}
});
要启动 Dapr 边车 并运行上述示例应用程序,您需要运行类似于以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run
// dependencies
import (
dapr "github.com/dapr/go-sdk/client"
)
// code
md := map[string]string{"ttlInSeconds": "120"}
if err := client.SaveState(ctx, store, "key1", []byte("hello world"), md); err != nil {
panic(err)
}
要启动 Dapr 边车 并运行上述示例应用程序,您需要运行类似于以下的命令:
dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 go run .
curl -X POST -H "Content-Type: application/json" -d '[{ "key": "order_1", "value": "250", "metadata": { "ttlInSeconds": "120" } }]' http://localhost:3601/v1.0/state/statestore
Invoke-RestMethod -Method Post -ContentType 'application/json' -Body '[{"key": "order_1", "value": "250", "metadata": {"ttlInSeconds": "120"}}]' -Uri 'http://localhost:3601/v1.0/state/statestore'
使用 Dapr 的绑定 API,您可以通过来自外部系统的事件触发您的应用程序,并与外部系统进行交互。通过绑定 API,您可以:
例如,通过绑定,您的应用程序可以响应传入的 Twilio/SMS 消息,而无需:

在上图中:
"create"。绑定是独立于 Dapr 运行时开发的。您可以查看并贡献绑定。
使用输入绑定,您可以在外部资源发生事件时触发您的应用程序。可选的有效负载和元数据可能会随请求一起发送。
以下概述视频和演示演示了 Dapr 输入绑定的工作原理。
要接收来自输入绑定的事件:
阅读使用输入绑定创建事件驱动应用程序指南以开始使用输入绑定。
使用输出绑定,您可以调用外部资源。可选的有效负载和元数据可以随调用请求一起发送。
以下概述视频和演示演示了 Dapr 输出绑定的工作原理。
要调用输出绑定:
"create""update""delete""exec"阅读使用输出绑定与外部资源交互指南以开始使用输出绑定。
您可以提供 direction 元数据字段来指示绑定组件支持的方向。这样做可以避免 Dapr 边车处于"等待应用程序就绪"状态,从而减少 Dapr 边车与应用程序之间的生命周期依赖关系:
"input""output""input, output"direction 属性。想要测试 Dapr 绑定 API?通过以下快速入门和教程来了解绑定的实际操作:
| 快速入门/教程 | 描述 |
|---|---|
| 绑定快速入门 | 使用输入绑定响应事件,使用输出绑定调用操作,与外部系统协作。 |
| 绑定教程 | 演示如何使用 Dapr 为其他组件创建输入和输出绑定。使用 Kafka 绑定。 |
想要跳过快速入门?没问题。您可以直接在应用程序中尝试绑定构建块,以调用输出绑定和触发输入绑定。在安装 Dapr后,您可以从输入绑定操作指南开始使用绑定 API。
使用输入绑定,当外部资源发生事件时,可以触发您的应用程序。外部资源可以是队列、消息管道、云服务、文件系统等。请求可以随附可选的 payload 和 metadata。
输入绑定非常适合事件驱动处理、数据管道,或通常用于响应事件并执行进一步处理。Dapr 输入绑定允许您:

本指南以 Kafka 绑定为例。您可以从绑定组件列表中找到您首选的绑定规范。在本指南中:
checkout(要调用的绑定名称)调用 /binding 端点。data 字段中,可以是任何 JSON 可序列化的值。operation 字段告知绑定需要采取什么操作。例如,Kafka 绑定支持 create 操作。创建一个 binding.yaml 文件并保存到应用程序目录中的 components 子文件夹。
创建一个名为 checkout 的新绑定组件。在 metadata 部分,配置以下与 Kafka 相关的属性:
创建绑定组件时,指定绑定支持的 direction。
在 dapr run 命令中使用 --resources-path 标志指向您的自定义资源目录。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: checkout
spec:
type: bindings.kafka
version: v1
metadata:
# Kafka broker 连接设置
- name: brokers
value: localhost:9092
# consumer 配置:topic 和 consumer group
- name: topics
value: sample
- name: consumerGroup
value: group1
# publisher 配置:topic
- name: publishTopic
value: sample
- name: authRequired
value: false
- name: direction
value: input
要部署到 Kubernetes 集群,运行 kubectl apply -f binding.yaml。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: checkout
spec:
type: bindings.kafka
version: v1
metadata:
# Kafka broker 连接设置
- name: brokers
value: localhost:9092
# consumer 配置:topic 和 consumer group
- name: topics
value: sample
- name: consumerGroup
value: group1
# publisher 配置:topic
- name: publishTopic
value: sample
- name: authRequired
value: false
- name: direction
value: input
配置您的应用程序以接收传入事件。如果您使用 HTTP,则需要:
POST 端点,端点名称为绑定名称,即 binding.yaml 文件中 metadata.name 指定的名称。OPTIONS 请求。以下是利用 Dapr SDK 演示输入绑定的代码示例。
以下示例演示如何使用 ASP.NET Core 控制器配置输入绑定。
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Mvc;
namespace CheckoutService.controller;
[ApiController]
public sealed class CheckoutServiceController : ControllerBase
{
[HttpPost("/checkout")]
public ActionResult<string> getCheckout([FromBody] int orderId)
{
Console.WriteLine($"Received Message: {orderId}");
return $"CID{orderId}";
}
}
以下示例演示如何使用 minimal API 方式配置相同的输入绑定:
app.MapPost("checkout", ([FromBody] int orderId) =>
{
Console.WriteLine($"Received Message: {orderId}");
return $"CID{orderId}"
});
以下示例演示如何使用 minimal API 方式配置相同的输入绑定:
app.MapPost("checkout", ([FromBody] int orderId) =>
{
Console.WriteLine($"Received Message: {orderId}");
return $"CID{orderId}"
});
//dependencies
import org.springframework.web.bind.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
//code
@RestController
@RequestMapping("/")
public class CheckoutServiceController {
private static final Logger log = LoggerFactory.getLogger(CheckoutServiceController.class);
@PostMapping(path = "/checkout")
public Mono<String> getCheckout(@RequestBody(required = false) byte[] body) {
return Mono.fromRunnable(() ->
log.info("Received Message: " + new String(body)));
}
}
#dependencies
import logging
from dapr.ext.grpc import App, BindingRequest
#code
app = App()
@app.binding('checkout')
def getCheckout(request: BindingRequest):
logging.basicConfig(level = logging.INFO)
logging.info('Received Message : ' + request.text())
app.run(6002)
//dependencies
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
)
//code
func getCheckout(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var orderId int
err := json.NewDecoder(r.Body).Decode(&orderId)
log.Println("Received Message: ", orderId)
if err != nil {
log.Printf("error parsing checkout input binding payload: %s", err)
w.WriteHeader(http.StatusOK)
return
}
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/checkout", getCheckout).Methods("POST", "OPTIONS")
http.ListenAndServe(":6002", r)
}
//dependencies
import { DaprServer, CommunicationProtocolEnum } from '@dapr/dapr';
//code
const daprHost = "127.0.0.1";
const serverHost = "127.0.0.1";
const serverPort = "6002";
const daprPort = "3602";
start().catch((e) => {
console.error(e);
process.exit(1);
});
async function start() {
const server = new DaprServer({
serverHost,
serverPort,
communicationProtocol: CommunicationProtocolEnum.HTTP,
clientOptions: {
daprHost,
daprPort,
}
});
await server.binding.receive('checkout', async (orderId) => console.log(`Received Message: ${JSON.stringify(orderId)}`));
await server.start();
}
从您的 HTTP 处理程序返回 200 OK 响应,告知 Dapr 您已成功处理事件。
返回 200 OK 以外的任何响应,告知 Dapr 事件在您的应用程序中未正确处理,并计划重新传递该事件。例如,返回 500 Error。
默认情况下,传入事件将发送到与输入绑定名称对应的 HTTP 端点。您可以通过在 binding.yaml 中设置以下 metadata 属性来覆盖此设置:
name: mybinding
spec:
type: binding.rabbitmq
metadata:
- name: route
value: /onevent
事件传递保证由绑定实现控制。根据绑定实现的不同,事件传递可以是恰好一次或至少一次。
使用输出绑定,您可以调用外部资源。调用请求中可以发送可选的有效负载和元数据。

本指南以 Kafka 绑定为例。您可以从绑定组件列表中找到您需要的绑定规范。在本指南中:
/binding 端点,并传递 checkout(要调用的绑定名称)来执行操作。data 字段中,可以是任何可序列化为 JSON 的值。operation 字段告诉绑定需要执行什么操作。例如,Kafka 绑定支持 create 操作。创建一个 binding.yaml 文件,并将其保存到应用程序目录中的 components 子文件夹中。
创建一个名为 checkout 的新绑定组件。在 metadata 部分中,配置以下与 Kafka 相关的属性:
创建绑定组件时,指定绑定的受支持的 direction。
使用 dapr run 命令时,通过 --resources-path 标志指向您的自定义资源目录。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: checkout
spec:
type: bindings.kafka
version: v1
metadata:
# Kafka broker 连接设置
- name: brokers
value: localhost:9092
# 消费者配置:主题和消费者组
- name: topics
value: sample
- name: consumerGroup
value: group1
# 发布者配置:主题
- name: publishTopic
value: sample
- name: authRequired
value: false
- name: direction
value: output
要将以下 binding.yaml 文件部署到 Kubernetes 集群中,请运行 kubectl apply -f binding.yaml。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: checkout
spec:
type: bindings.kafka
version: v1
metadata:
# Kafka broker 连接设置
- name: brokers
value: localhost:9092
# 消费者配置:主题和消费者组
- name: topics
value: sample
- name: consumerGroup
value: group1
# 发布者配置:主题
- name: publishTopic
value: sample
- name: authRequired
value: false
- name: direction
value: output
下面的代码示例利用 Dapr SDK 来调用运行中的 Dapr 实例上的输出绑定端点。
以下是在 .NET 6+ 中使用顶级语句的控制台应用程序示例:
using System.Text;
using System.Threading.Tasks;
using Dapr.Client;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
const string BINDING_NAME = "checkout";
const string BINDING_OPERATION = "create";
var random = new Random();
using var daprClient = app.Services.GetRequiredService<DaprClient>();
while (true)
{
await Task.Delay(TimeSpan.FromSeconds(5));
var orderId = random.Next(1, 1000);
await client.InvokeBindingAsync(BINDING_NAME, BINDING_OPERATION, orderId);
Console.WriteLine($"Sending message: {orderId}");
}
//dependencies
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.HttpExtension;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;
//code
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
public static void main(String[] args) throws InterruptedException{
String BINDING_NAME = "checkout";
String BINDING_OPERATION = "create";
while(true) {
TimeUnit.MILLISECONDS.sleep(5000);
Random random = new Random();
int orderId = random.nextInt(1000-1) + 1;
DaprClient client = new DaprClientBuilder().build();
//使用 Dapr SDK 调用输出绑定
client.invokeBinding(BINDING_NAME, BINDING_OPERATION, orderId).block();
log.info("Sending message: " + orderId);
}
}
}
#dependencies
import random
from time import sleep
import requests
import logging
import json
from dapr.clients import DaprClient
#code
logging.basicConfig(level = logging.INFO)
BINDING_NAME = 'checkout'
BINDING_OPERATION = 'create'
while True:
sleep(random.randrange(50, 5000) / 1000)
orderId = random.randint(1, 1000)
with DaprClient() as client:
#使用 Dapr SDK 调用输出绑定
resp = client.invoke_binding(BINDING_NAME, BINDING_OPERATION, json.dumps(orderId))
logging.basicConfig(level = logging.INFO)
logging.info('Sending message: ' + str(orderId))
//dependencies
import (
"context"
"log"
"math/rand"
"time"
"strconv"
dapr "github.com/dapr/go-sdk/client"
)
//code
func main() {
BINDING_NAME := "checkout";
BINDING_OPERATION := "create";
for i := 0; i < 10; i++ {
time.Sleep(5000)
orderId := rand.Intn(1000-1) + 1
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
//使用 Dapr SDK 调用输出绑定
in := &dapr.InvokeBindingRequest{ Name: BINDING_NAME, Operation: BINDING_OPERATION , Data: []byte(strconv.Itoa(orderId))}
err = client.InvokeOutputBinding(ctx, in)
log.Println("Sending message: " + strconv.Itoa(orderId))
}
}
//dependencies
import { DaprClient, CommunicationProtocolEnum } from "@dapr/dapr";
//code
const daprHost = "127.0.0.1";
(async function () {
for (var i = 0; i < 10; i++) {
await sleep(2000);
const orderId = Math.floor(Math.random() * (1000 - 1) + 1);
try {
await sendOrder(orderId)
} catch (err) {
console.error(e);
process.exit(1);
}
}
})();
async function sendOrder(orderId) {
const BINDING_NAME = "checkout";
const BINDING_OPERATION = "create";
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
//使用 Dapr SDK 调用输出绑定
const result = await client.binding.send(BINDING_NAME, BINDING_OPERATION, orderId);
console.log("Sending message: " + orderId);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
您也可以使用 HTTP 来调用输出绑定端点:
curl -X POST -H 'Content-Type: application/json' http://localhost:3601/v1.0/bindings/checkout -d '{ "data": 100, "operation": "create" }'
观看此视频了解如何使用双向输出绑定。
Actor 模式将 actor 描述为最低级别的"计算单元"。换句话说,你需要将代码编写在一个自包含的单元(称为 actor)中,该单元接收消息并一次处理一条消息,无需任何并发或线程机制。
当代码处理消息时,它可以向其他 actor 发送一条或多条消息,或创建新的 actor。底层运行时负责管理每个 actor 的运行方式、时间和位置,并在 actor 之间路由消息。
大量 actor 可以同时执行,并且 actor 之间相互独立执行。
Dapr 包含一个专门实现虚拟 Actor 模式的运行时。通过 Dapr 的实现,你可以根据 actor 模型编写 Dapr actor,Dapr 利用底层平台提供的可扩展性和可靠性保证。
每个 actor 都定义为 actor 类型的实例,就像对象是类的实例一样。例如,可能有一个实现了计算器功能的 actor 类型,并且可能有多个该类型的 actor 分布在集群中的各个节点上。每个这样的 actor 都通过 actor ID 进行唯一标识。

下面的概述视频和演示展示了 Dapr 中 actor 的工作原理。
Dapr actor 基于状态管理和服务调用 API 创建具有身份的有状态、长时间运行的对象。Dapr Workflow 与 Dapr Actor 相关,workflow 构建在 actor 之上,提供更高级别的抽象来编排一组 actor,实现常见的 workflow 模式并代表你管理 actor 的生命周期。
Dapr actor 旨在提供一种在分布式系统中封装状态和行为的方式。actor 可以由客户端应用程序按需激活。当 actor 被激活时,它会被分配一个唯一的身份,这允许它在多次调用之间维护其状态。这使得 actor 非常适合构建有状态、可扩展和容错的分布式应用程序。
另一方面,Dapr Workflow 提供了一种定义和编排涉及分布式系统中多个服务和组件的复杂工作流的方式。Workflow 允许你定义需要按特定顺序执行的步骤或任务序列,并可用于实现业务流程、事件驱动的工作流和其他类似场景。
如上所述,Dapr Workflow 构建在 Dapr Actor 之上,管理其激活和生命周期。
与任何其他技术决策一样,你应该根据要解决的问题来决定是否使用 actor。例如,如果你正在构建聊天应用程序,你可能会使用 Dapr actor 来实现聊天室和用户之间的单独聊天会话,因为每个聊天会话都需要维护自己的状态并且是可扩展和容错的。
一般来说,如果满足以下条件,请考虑使用 actor 模式来为你的问题或场景建模:
当你需要定义和编排涉及多个服务和组件的复杂工作流时,你将使用 Dapr Workflow。例如,使用前面的聊天应用程序示例,你可能会使用 Dapr Workflow 来定义应用程序的总体工作流,例如如何注册新用户、如何发送和接收消息,以及应用程序如何处理错误和异常。
了解有关 Dapr Workflow 的更多信息以及如何在应用程序中使用工作流。
Actor 唯一定义为 actor 类型的实例,类似于对象是类的实例。例如,你可能有一个实现了计算器功能的 actor 类型。该类型的许多 actor 可能分布在集群中的各个节点上。
每个 actor 都通过 actor ID 进行唯一标识。actor ID 可以是你选择的_任何_字符串值。如果你不提供 actor ID,Dapr 会为你生成一个随机字符串作为 ID。
Dapr 支持命名空间 actor。可以将 actor 类型部署到不同的命名空间中。你可以在同一命名空间中调用这些 actor 的实例。
由于 Dapr actor 是虚拟的,因此不需要显式创建或销毁它们。Dapr actor 运行时:
actor 的状态超出对象的生命周期,因为状态存储在为 Dapr 运行时配置的状态提供程序中。
为了提供可扩展性和可靠性,actor 实例分布在集群中,Dapr 在整个集群中分布 actor 实例并自动将它们迁移到健康节点。
你可以通过 HTTP 调用 actor 方法来调用它们,如下面的常规示例所示。

Dapr actor 运行时为访问 actor 方法提供了一个简单的基于轮次的访问模型。基于轮次的访问大大简化了并发系统,因为不需要用于数据访问的同步机制。
事务性状态存储可用于存储 actor 状态。无论你是否打算在 actor 中存储任何状态,都必须在状态存储组件的元数据部分为属性 actorStateStore 指定值 true。Actor 状态以特定方案存储在事务性状态存储中,从而允许进行一致的查询。只能使用单个状态存储组件作为所有 actor 的状态存储。阅读状态 API 参考和 actor API 参考以了解有关 actor 状态存储的更多信息。
Actor 可以通过注册定时器或提醒来为自己安排定期工作。
定时器和提醒的功能非常相似。主要区别在于,Dapr actor 运行时在停用后不保留有关定时器的任何信息,而使用 Dapr actor 状态提供程序持久化有关提醒的信息。
这种区别允许用户在轻量级但无状态的定时器与资源需求更高但有状态的提醒之间进行权衡。
下面的概述视频和演示展示了 actor 定时器和提醒的工作原理。
既然您已经从高层次了解了 Actor 构建块,让我们深入探讨 Dapr 中 Actor 包含的功能和概念。
Dapr Actor 是虚拟的,这意味着它们的生命周期与其内存中的表示形式无关。因此,它们不需要被显式创建或销毁。Dapr Actor 运行时在首次收到针对该 Actor ID 的请求时自动激活 Actor。如果 Actor 在一段时间内未被使用,Dapr Actor 运行时会回收内存中的对象。如果稍后需要重新激活,它也会保留有关 Actor 存在的知识。
调用 Actor 方法、定时器和提醒会重置 Actor 空闲时间。例如,提醒触发会保持 Actor 处于活动状态。
Dapr 运行时用于检查 Actor 是否可以被垃圾回收的空闲超时和扫描间隔是可配置的。当 Dapr 运行时调用 Actor 服务以获取支持的 Actor 类型时,可以传递此信息。
由于虚拟 Actor 模型,这种虚拟 Actor 生命周期抽象存在一些注意事项,实际上 Dapr Actor 的实现有时会偏离此模型。
首次向其 Actor ID 发送消息时,Actor 会自动激活(导致构造 Actor 对象)。一段时间后,Actor 对象被垃圾回收。将来,再次使用 Actor ID 会导致构造新的 Actor 对象。Actor 的状态比对象的生命周期更长,因为状态存储在为 Dapr 运行时配置的状态提供程序中。
为了提供可扩展性和可靠性,Actor 实例分布在整个集群中,Dapr 会根据需要自动将它们从故障节点迁移到健康节点。
Actor 分布在 Actor 服务的实例中,这些实例分布在集群中的节点上。每个服务实例包含给定 Actor 类型的一组 Actor。
Dapr Actor 运行时通过 Actor Placement 服务为您管理分布方案和键范围设置。当创建服务的新实例时:
Placement 服务计算给定 Actor 类型在所有实例中的分区。每个 Actor 类型的此分区数据表会在环境中运行的每个 Dapr 实例中更新和存储,并且可以随着创建和销毁新的 Actor 服务实例而动态变化。

当客户端调用具有特定 ID 的 Actor(例如,actor id 123)时,客户端的 Dapr 实例会对 Actor 类型和 ID 进行哈希处理,并使用该信息调用可以服务于该特定 Actor ID 请求的相应 Dapr 实例。因此,对于任何给定的 Actor ID,总是调用相同的分区(或服务实例)。下图显示了这一点。

这简化了一些选择,但也带来了一些考虑:
您可以通过调用 HTTP 端点与 Dapr 交互以调用 Actor 方法。
POST/GET/PUT/DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/<method/state/timers/reminders>
您可以在请求正文中为 Actor 方法提供任何数据,请求的响应将在响应正文中,即来自 Actor 调用的数据。
另一种也许更方便的与 Actor 交互的方式是通过 SDK。Dapr 目前支持 .NET、Java 和 Python 的 Actor SDK。
有关更多详细信息,请参阅 Dapr Actor 功能。
Dapr Actor 运行时为访问 Actor 方法提供了一个简单的基于轮次的访问模型。这意味着在任何时候,一个 Actor 对象的代码中只能有一个线程处于活动状态。基于轮次的访问大大简化了并发系统,因为不需要数据访问的同步机制。这也意味着系统在设计时必须特别考虑每个 Actor 实例的单线程访问性质。
单个 Actor 实例一次不能处理多个请求。如果期望 Actor 实例处理并发请求,它可能会导致吞吐量瓶颈。
如果两个 Actor 之间存在循环请求,同时对外部请求之一发出外部请求,Actor 可能会相互死锁。Dapr Actor 运行时会在 Actor 调用时自动超时并向调用者抛出异常,以中断可能的死锁情况。

要允许 Actor “重入"并调用自身的方法,请参阅 Actor 重入。
一个轮次包括响应来自其他 Actor 或客户端的请求而对 Actor 方法进行的完整执行,或定时器/提醒回调的完整执行。尽管这些方法和回调是异步的,但 Dapr Actor 运行时不会交错它们。一个轮次必须完全完成后才允许进行新的轮次。换句话说,当前正在执行的 Actor 方法或定时器/提醒回调必须在允许新的方法或回调调用之前完全完成。如果执行已从方法或回调返回,并且方法或回调返回的任务已完成,则认为方法或回调已完成。值得强调的是,即使在不同方法、定时器和回调之间也会遵守基于轮次的并发。
Dapr Actor 运行时通过在轮次开始时获取每个 Actor 的锁并在轮次结束时释放锁来强制执行基于轮次的并发。因此,基于轮次的并发是针对每个 Actor 强制执行的,而不是跨 Actor 强制执行的。Actor 方法和定时器/提醒回调可以同时代表不同的 Actor 执行。
以下示例说明了上述概念。考虑一个实现两个异步方法(例如,Method1 和 Method2)、一个定时器和一个提醒的 Actor 类型。下图显示了代表属于此 Actor 类型的两个 Actor(ActorId1 和 ActorId2)执行这些方法和回调的时间线示例。

您可以使用以下配置参数修改默认的 Dapr Actor 运行时行为。
| 参数 | 描述 | 默认值 |
|---|---|---|
entities | 此主机支持的 Actor 类型。 | N/A |
actorIdleTimeout | 停用空闲 Actor 前的超时时间。每隔 actorScanInterval 间隔检查一次超时。 | 60 分钟 |
actorScanInterval | 扫描需要停用的空闲 Actor 的频率。空闲时间超过 actor_idle_timeout 的 Actor 将被停用。 | 30 秒 |
drainOngoingCallTimeout | 正在迁移重平衡 Actor 时的持续时间。这指定了当前活动 Actor 方法完成的超时时间。如果没有当前 Actor 方法调用,则忽略此参数。 | 60 秒 |
drainRebalancedActors | 如果为 true,Dapr 将等待 drainOngoingCallTimeout 持续时间,以允许当前 Actor 调用完成,然后再尝试停用 Actor。 | true |
reentrancy (ActorReentrancyConfig) | 配置 Actor 的重入行为。如果未提供,则禁用重入。 | 禁用,false |
entitiesConfig | 使用配置数组为每个 Actor 类型单独配置。在各个实体配置中指定的任何实体也必须在顶层 entities 字段中指定。 | N/A |
// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Register actor runtime with DI
services.AddActors(options =>
{
// Register actor types and configure actor settings
options.Actors.RegisterActor<MyActor>();
// Configure default settings
options.ActorIdleTimeout = TimeSpan.FromMinutes(60);
options.ActorScanInterval = TimeSpan.FromSeconds(30);
options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(60);
options.DrainRebalancedActors = true;
options.ReentrancyConfig = new() { Enabled = false };
// Add a configuration for a specific actor type.
// This actor type must have a matching value in the base level 'entities' field. If it does not, the configuration will be ignored.
// If there is a matching entity, the values here will be used to overwrite any values specified in the root configuration.
// In this example, `ReentrantActor` has reentrancy enabled; however, 'MyActor' will not have reentrancy enabled.
options.Actors.RegisterActor<ReentrantActor>(typeOptions: new()
{
ReentrancyConfig = new()
{
Enabled = true,
}
});
});
// Register additional services for use with actors
services.AddSingleton<BankService>();
}
import { CommunicationProtocolEnum, DaprClient, DaprServer } from "@dapr/dapr";
// Configure the actor runtime with the DaprClientOptions.
const clientOptions = {
actor: {
actorIdleTimeout: "1h",
actorScanInterval: "30s",
drainOngoingCallTimeout: "1m",
drainRebalancedActors: true,
reentrancy: {
enabled: true,
maxStackDepth: 32,
},
},
};
// Use the options when creating DaprServer and DaprClient.
// Note, DaprServer creates a DaprClient internally, which needs to be configured with clientOptions.
const server = new DaprServer(serverHost, serverPort, daprHost, daprPort, clientOptions);
const client = new DaprClient(daprHost, daprPort, CommunicationProtocolEnum.HTTP, clientOptions);
from datetime import timedelta
from dapr.actor.runtime.config import ActorRuntimeConfig, ActorReentrancyConfig
ActorRuntime.set_actor_config(
ActorRuntimeConfig(
actor_idle_timeout=timedelta(hours=1),
actor_scan_interval=timedelta(seconds=30),
drain_ongoing_call_timeout=timedelta(minutes=1),
drain_rebalanced_actors=True,
reentrancy=ActorReentrancyConfig(enabled=False),
)
)
// import io.dapr.actors.runtime.ActorRuntime;
// import java.time.Duration;
ActorRuntime.getInstance().getConfig().setActorIdleTimeout(Duration.ofMinutes(60));
ActorRuntime.getInstance().getConfig().setActorScanInterval(Duration.ofSeconds(30));
ActorRuntime.getInstance().getConfig().setDrainOngoingCallTimeout(Duration.ofSeconds(60));
ActorRuntime.getInstance().getConfig().setDrainBalancedActors(true);
ActorRuntime.getInstance().getConfig().setActorReentrancyConfig(false, null);
const (
defaultActorType = "basicType"
reentrantActorType = "reentrantType"
)
type daprConfig struct {
Entities []string `json:"entities,omitempty"`
ActorIdleTimeout string `json:"actorIdleTimeout,omitempty"`
ActorScanInterval string `json:"actorScanInterval,omitempty"`
DrainOngoingCallTimeout string `json:"drainOngoingCallTimeout,omitempty"`
DrainRebalancedActors bool `json:"drainRebalancedActors,omitempty"`
Reentrancy config.ReentrancyConfig `json:"reentrancy,omitempty"`
EntitiesConfig []config.EntityConfig `json:"entitiesConfig,omitempty"`
}
var daprConfigResponse = daprConfig{
Entities: []string{defaultActorType, reentrantActorType},
ActorIdleTimeout: actorIdleTimeout,
ActorScanInterval: actorScanInterval,
DrainOngoingCallTimeout: drainOngoingCallTimeout,
DrainRebalancedActors: drainRebalancedActors,
Reentrancy: config.ReentrancyConfig{Enabled: false},
EntitiesConfig: []config.EntityConfig{
{
// Add a configuration for a specific actor type.
// This actor type must have a matching value in the base level 'entities' field. If it does not, the configuration will be ignored.
// If there is a matching entity, the values here will be used to overwrite any values specified in the root configuration.
// In this example, `reentrantActorType` has reentrancy enabled; however, 'defaultActorType' will not have reentrancy enabled.
Entities: []string{reentrantActorType},
Reentrancy: config.ReentrancyConfig{
Enabled: true,
MaxStackDepth: &maxStackDepth,
},
},
},
}
func configHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(daprConfigResponse)
}
Dapr 中的命名空间提供隔离能力,从而实现多租户。通过 Actor 命名空间,相同的 Actor 类型可以部署到不同的命名空间中。你可以调用同一命名空间中的这些 Actor 实例。
你可以在自托管模式或 Kubernetes 上使用命名空间。
在自托管模式下,你可以通过设置 NAMESPACE 环境变量来指定 Dapr 实例的命名空间。
在 Kubernetes 上,你可以在部署 Actor 应用程序时创建和配置命名空间。例如,从以下 kubectl 命令开始:
kubectl create namespace namespace-actorA
kubectl config set-context --current --namespace=namespace-actorA
然后,将你的 Actor 应用程序部署到此命名空间中(本例中为 namespace-actorA)。
每个命名空间 Actor 部署必须使用各自独立的状态存储。虽然你可以为每个 Actor 命名空间使用不同的物理数据库,但某些状态存储组件提供了通过表、前缀、集合等方式进行逻辑隔离的方法。这允许你使用相同的物理数据库来支持多个命名空间,只要你在 Dapr 组件定义中提供逻辑隔离即可。
以下是一些示例。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.etcd
version: v2
metadata:
- name: endpoints
value: localhost:2379
- name: keyPrefixPath
value: namespace-actorA
- name: actorStateStore
value: "true"
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.sqlite
version: v1
metadata:
- name: connectionString
value: "data.db"
- name: tableName
value: "namespace-actorA"
- name: actorStateStore
value: "true"
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: actorStateStore
value: "true"
- name: redisDB
value: "1"
- name: redisPassword
secretKeyRef:
name: redis-secret
key: redis-password
- name: actorStateStore
value: "true"
- name: redisDB
value: "1"
auth:
secretStore: <SECRET_STORE_NAME>
请查看你的状态存储组件规范以了解其提供的功能。
Actor 可以通过注册定时器或提醒来调度自身的周期性工作。
定时器和提醒的功能非常相似。主要区别在于,Dapr actor 运行时在停用后不保留关于定时器的任何信息,而使用 Dapr Scheduler 持久化关于提醒的信息。
这种区别允许用户在轻量级但无状态的定时器与资源消耗更大但有状态的提醒之间进行权衡。
定时器和提醒的调度配置总结如下:
data 是一个可选参数,包含在调用时传递给提醒回调方法的数据。
dueTime 是一个可选参数,设置首次调用回调的时间或时间间隔。如果省略 dueTime,回调将在定时器/提醒注册后立即被调用。
支持的格式:
2020-10-02T15:00:00Z2h30mPT2H30Mperiod 是一个可选参数,设置两次连续回调调用之间的时间间隔。当以 ISO 8601-1 duration 格式指定时,你还可以配置重复次数以限制回调调用的总次数。
如果省略 period,回调将只被调用一次。
支持的格式:
2h30m、500msPT2H30M、R5/PT1M30Sttl 是一个可选参数,设置定时器/提醒到期和删除的时间或时间间隔。如果省略 ttl,则不应用任何限制。
支持的格式:
2020-10-02T15:00:00Z2h30mPT2H30M仅适用于提醒。
overwrite 是一个可选布尔参数,指示是否覆盖同名的现有提醒。
如果 overwrite 设置为 true,任何同名的现有提醒将被新配置替换。
如果 overwrite 设置为 false 或省略,并且已存在同名的提醒,操作将失败并返回已存在的错误。
请注意,覆盖现有提醒将重置其状态,包括调用次数和下次触发时间,就像创建新提醒一样。
仅适用于提醒。
failurePolicy 是一个可选参数,定义提醒调用失败时的行为。
支持的失败策略包括:
drop:丢弃失败的调用,提醒继续按计划进行下一次调用,就好像失败没有发生一样。constant:提醒将以固定的间隔重试失败的调用指定次数。interval:每次重试尝试之间的时间间隔。如果未指定,间隔为 “0s”,意味着立即尝试重试。maxRetries:重试尝试的最大次数。如果未指定,调用将以指定间隔无限期重试,直到成功。如果未指定失败策略,将应用默认的失败策略:重试 3 次,间隔为 1 秒。
actor 运行时会验证调度配置的正确性,并在输入无效时返回错误。
当你在 period 中指定重复次数以及 ttl 时,定时器/提醒将在任一条件满足时停止,以先发生者为准。
你可以为 actor 注册一个基于定时器执行的回调。
Dapr actor 运行时确保回调方法遵守基于轮次的并发保证。这意味着在此回调完成执行之前,不会有其他 actor 方法或定时器/提醒回调正在进行。
Dapr actor 运行时在回调完成时保存对 actor 状态所做的更改。如果在保存状态时发生错误,该 actor 对象将被停用,并将激活一个新实例。
当 actor 作为垃圾回收的一部分被停用时,所有定时器都会停止。之后不会再调用定时器回调。此外,Dapr actor 运行时不保留关于停用前正在运行的定时器的任何信息。由 actor 在将来重新激活时注册其所需的任何定时器。
你可以通过调用对 Dapr 的 HTTP/gRPC 请求来为 actor 创建定时器,如下所示,或通过 Dapr SDK。
POST/PUT http://localhost:3500/v1.0/actors/<actorType>/<actorId>/timers/<name>
定时器参数在请求体中指定。
以下请求体配置了一个定时器,dueTime 为 9 秒,period 为 3 秒。这意味着它将在 9 秒后首次触发,然后每 3 秒触发一次。
{
"dueTime":"0h0m9s0ms",
"period":"0h0m3s0ms"
}
以下请求体配置了一个定时器,period 为 3 秒(ISO 8601 duration 格式)。它还将调用次数限制为 10 次。这意味着它将触发 10 次:首先在注册后立即触发,然后每 3 秒触发一次。
{
"period":"R10/PT3S",
}
以下请求体配置了一个定时器,period 为 3 秒(ISO 8601 duration 格式),ttl 为 20 秒。这意味着它在注册后立即触发,然后在 20 秒的持续时间内每 3 秒触发一次。
{
"period":"PT3S",
"ttl":"20s"
}
以下请求体配置了一个定时器,dueTime 为 10 秒,period 为 3 秒,ttl 为 10 秒。它还将调用次数限制为 4 次。这意味着它将在 10 秒后首次触发,然后在 10 秒的持续时间内每 3 秒触发一次,但总共不超过 4 次。
{
"dueTime":"10s",
"period":"R4/PT3S",
"ttl":"10s"
}
你可以通过调用以下命令来删除 actor 定时器
DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/timers/<name>
有关更多详细信息,请参阅 API 规范。
提醒是一种在指定时间在 actor 上触发持久回调的机制。它们的功能类似于定时器。但与定时器不同,提醒在所有情况下都会被触发,直到 actor 显式注销它们或调用次数用尽。具体来说,提醒会在 actor 停用和故障转移期间触发,因为 Dapr actor 运行时使用 Dapr Scheduler 服务 持久化关于 actor 提醒的信息。
你可以通过调用对 Dapr 的 HTTP/gRPC 请求为 actor 创建持久提醒,如下所示,或通过 Dapr SDK。
POST/PUT http://localhost:3500/v1.0/actors/<actorType>/<actorId>/reminders/<name>
与定时器不同,提醒支持 overwrite 选项,允许你替换同名的现有提醒,以及 failurePolicy 选项,定义提醒调用失败时的行为。
你可以通过调用以下命令来检索 actor 提醒
GET http://localhost:3500/v1.0/actors/<actorType>/<actorId>/reminders/<name>
你可以通过调用以下命令来删除 actor 提醒
DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/reminders/<name>
如果触发 actor 提醒且应用未向运行时返回 2** 代码(例如,由于连接问题),actor 提醒将根据其失败策略进行重试,默认情况下是三次,每次尝试之间的退避间隔为 1 秒。 根据任何可选应用的 actor 弹性策略,可能会尝试额外的重试。
有关更多详细信息,请参阅 API 规范。
当 actor 的方法成功完成时,运行时将继续按照指定的定时器或提醒计划调用该方法。 为了允许 actor 从故障中恢复并在崩溃或重启后重试,你可以通过配置状态存储(例如 Redis 或 Azure Cosmos DB)来持久化 actor 的状态。
如果方法的调用失败,定时器不会被删除。定时器仅在以下情况下被删除:
Actor 提醒持久化在 Scheduler 中。 你可以使用 dapr scheduler CLI 命令管理它们。
dapr scheduler list --filter actor
NAME BEGIN COUNT LAST TRIGGER
actor/MyActorType/actorid1/test1 -3.89s 1 2025-10-03T16:58:55Z
actor/MyActorType/actorid2/test2 -3.89s 1 2025-10-03T16:58:55Z
获取提醒详细信息
dapr scheduler get actor/MyActorType/actorid1/test1 -o yaml
删除单个提醒:
dapr scheduler delete actor/MyActorType/actorid1/test1
删除给定 actor 类型的所有提醒:
dapr scheduler delete-all actor/MyActorType
Actor 提醒持久化在 Scheduler 中。 你可以使用 dapr scheduler CLI 命令管理它们。
dapr scheduler list --filter actor
NAME BEGIN COUNT LAST TRIGGER
actor/MyActorType/actorid1/test1 -3.89s 1 2025-10-03T16:58:55Z
actor/MyActorType/actorid2/test2 -3.89s 1 2025-10-03T16:58:55Z
获取提醒详细信息
dapr scheduler get actor/MyActorType/actorid1/test1 -o yaml
删除单个提醒:
dapr scheduler delete actor/MyActorType/actorid1/test1
删除给定 actor 类型的所有提醒:
dapr scheduler delete-all actor/MyActorType
删除特定 actor 实例的所有提醒:
dapr scheduler delete-all actor/MyActorType/actorid1
删除特定 actor 类型的所有提醒:
dapr scheduler delete-all actor/MyActorType
删除所有提醒
dapr scheduler delete-all actor
导出所有提醒:
dapr scheduler export -o reminders-backup.bin
从备份文件恢复:
dapr scheduler import -f reminders-backup.bin
dapr scheduler CLI 管理现有提醒(列出、获取、删除、备份/恢复)。学习如何通过调用 HTTP/gRPC 端点来使用虚拟 actor。
您可以通过调用 HTTP/gRPC 端点与 Dapr 交互来调用 actor 方法。
POST/GET/PUT/DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/method/<method>
在请求正文中提供 actor 方法的数据。请求的响应(来自 actor 方法调用的数据)位于响应正文中。
有关更多详细信息,请参阅 Actor API 规范。
您可以通过 HTTP/gRPC 端点与 Dapr 交互,利用 Dapr actor 状态管理功能可靠地保存状态。
要使用 actor,您的状态存储必须支持多项目事务。这意味着您的状态存储组件必须实现 TransactionalStore 接口。
查看支持事务/actor 的组件列表。所有 actor 只能使用单个状态存储组件作为状态存储。
虚拟 Actor 模式 的一个核心原则是 Actor 执行的单线程特性。在没有重入的情况下,Dapr 运行时会对所有 Actor 请求进行加锁。第二个请求无法在第一个请求完成之前开始。这意味着 Actor 无法调用自身,或者让另一个 Actor 调用它,即使它是同一调用链的一部分。
重入通过允许来自同一链或上下文的请求重新进入已锁定的 Actor 来解决这个问题。这在以下场景中非常有用:
重入所允许的调用链示例如下:
Actor A -> Actor A
ActorA -> Actor B -> Actor A
通过重入,你可以执行更复杂的 Actor 调用,而不会牺牲虚拟 Actor 的单线程行为。

maxStackDepth 参数设置一个值,用于控制可以对同一 Actor 进行多少次重入调用。默认情况下,此值设置为 32,这在大多数情况下绰绰有余。
可重入的 Actor 必须提供适当的配置。这是通过 Actor 的端点 GET /dapr/config 完成的,类似于其他 Actor 配置元素。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<BankService>();
services.AddActors(options =>
{
options.Actors.RegisterActor<DemoActor>();
options.ReentrancyConfig = new Dapr.Actors.ActorReentrancyConfig()
{
Enabled = true,
MaxStackDepth = 32,
};
});
}
}
import { CommunicationProtocolEnum, DaprClient, DaprServer } from "@dapr/dapr";
// 使用 DaprClientOptions 配置 actor 运行时。
const clientOptions = {
actor: {
reentrancy: {
enabled: true,
maxStackDepth: 32,
},
},
};
from fastapi import FastAPI
from dapr.ext.fastapi import DaprActor
from dapr.actor.runtime.config import ActorRuntimeConfig, ActorReentrancyConfig
from dapr.actor.runtime.runtime import ActorRuntime
from demo_actor import DemoActor
reentrancyConfig = ActorReentrancyConfig(enabled=True)
config = ActorRuntimeConfig(reentrancy=reentrancyConfig)
ActorRuntime.set_actor_config(config)
app = FastAPI(title=f'{DemoActor.__name__}Service')
actor = DaprActor(app)
@app.on_event("startup")
async def startup_event():
# 注册 DemoActor
await actor.register_actor(DemoActor)
@app.get("/MakeExampleReentrantCall")
def do_something_reentrant():
# 在此处调用另一个 actor,重入将自动处理
return
下面是用 Golang 编写的 Actor 片段,它通过 HTTP API 提供重入配置。Go SDK 尚未包含重入功能。
type daprConfig struct {
Entities []string `json:"entities,omitempty"`
ActorIdleTimeout string `json:"actorIdleTimeout,omitempty"`
ActorScanInterval string `json:"actorScanInterval,omitempty"`
DrainOngoingCallTimeout string `json:"drainOngoingCallTimeout,omitempty"`
DrainRebalancedActors bool `json:"drainRebalancedActors,omitempty"`
Reentrancy config.ReentrancyConfig `json:"reentrancy,omitempty"`
}
var daprConfigResponse = daprConfig{
[]string{defaultActorType},
actorIdleTimeout,
actorScanInterval,
drainOngoingCallTimeout,
drainRebalancedActors,
config.ReentrancyConfig{Enabled: true, MaxStackDepth: &maxStackDepth},
}
func configHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(daprConfigResponse)
}
重入请求的关键是 Dapr-Reentrancy-Id 请求头。此请求头的值用于将请求与其调用链进行匹配,并允许它们绕过 Actor 的锁。
此请求头由 Dapr 运行时为任何指定了重入配置的 Actor 请求生成。一旦生成,它就会被用来锁定 Actor,并且必须传递给所有后续请求。下面是 Actor 处理重入请求的示例:
func reentrantCallHandler(w http.ResponseWriter, r *http.Request) {
/*
* 省略。
*/
req, _ := http.NewRequest("PUT", url, bytes.NewReader(nextBody))
reentrancyID := r.Header.Get("Dapr-Reentrancy-Id")
req.Header.Add("Dapr-Reentrancy-Id", reentrancyID)
client := http.Client{}
resp, err := client.Do(req)
/*
* 省略。
*/
}
观看此视频,了解如何使用 Actor 重入。
应用程序通常使用专用的密钥存储来存储敏感信息。例如,你使用存储在密钥存储(如 AWS Secrets Manager、Azure Key Vault、Hashicorp Vault 等)中的连接字符串、密钥、令牌和其他应用级密钥来对数据库、服务和外部系统进行身份验证。
要访问这些密钥存储,应用程序需要导入密钥存储 SDK,这通常需要大量无关的样板代码。在多云场景中,这会带来更大的挑战,因为可能会使用不同供应商特定的密钥存储。
Dapr 专用的密钥构建块 API 使开发者更容易从密钥存储中消费应用密钥。要使用 Dapr 的密钥存储构建块,你需要:
以下概览视频和演示展示了 Dapr 密钥管理的工作原理。
密钥管理 API 构建块为你的应用程序带来了多项功能。
你可以在应用代码中调用密钥 API 来检索和使用来自 Dapr 支持的密钥存储的密钥。观看此视频,了解如何在你的应用程序中使用密钥管理 API 的示例。
例如,下图显示了一个应用程序从名为"vault"的密钥存储中请求名为"mysecret"的密钥,该密钥存储来自已配置的云密钥存储。

应用程序还可以使用密钥 API 访问 Kubernetes 密钥存储中的密钥。默认情况下,Dapr 在 Kubernetes 模式下启用内置的 Kubernetes 密钥存储,通过以下方式部署:
dapr init -k如果你使用其他密钥存储,可以通过在 deployment.yaml 文件中添加注解 dapr.io/disable-builtin-k8s-secret-store: "true" 来禁用(不配置)Dapr Kubernetes 密钥存储。默认值为 false。
在下面的示例中,应用程序从 Kubernetes 密钥存储中检索相同的密钥"mysecret"。

在 Azure 中,你可以配置 Dapr 使用托管身份通过 Azure Key Vault 进行身份验证来检索密钥。在下面的示例中:

在上面的示例中,应用程序代码无需更改即可获取相同的密钥。Dapr 通过密钥管理构建块 API 使用密钥管理组件。
使用我们的快速入门或教程之一试用密钥 API。
在配置 Dapr 组件(如状态存储)时,通常需要在组件文件中包含凭证。或者,你可以将凭证放在 Dapr 支持的密钥存储中,并在 Dapr 组件中引用该密钥。这是首选方法,也是推荐的最佳实践,尤其是在生产环境中。
有关更多信息,请阅读在组件中引用密钥存储。
为了对密钥访问提供更精细的控制,Dapr 提供了定义作用域和限制访问权限的功能。了解有关使用密钥作用域的更多信息。
想要测试 Dapr 密钥管理 API?通过以下快速入门和教程了解 Dapr 密钥的实际应用:
| 快速入门/教程 | 描述 |
|---|---|
| 密钥管理快速入门 | 使用密钥管理 API 从已配置的密钥存储在应用代码中检索密钥。 |
| 密钥存储教程 | 演示如何使用 Dapr 密钥 API 访问密钥存储。 |
想跳过快速入门?没问题。你可以直接在你的应用程序中试用密钥管理构建块来检索和管理密钥。在安装 Dapr后,你可以从密钥操作指南开始使用密钥管理 API。
既然您已经了解了 Dapr 密钥构建块提供什么功能,了解它如何在您的服务中工作。本指南演示如何调用密钥 API 并从配置的密钥存储中检索应用代码中的密钥。

在应用的代码中检索密钥之前,您必须配置一个密钥存储组件。本示例配置了一个使用本地 JSON 文件存储密钥的密钥存储。
在项目目录中,创建一个名为 secrets.json 的文件,包含以下内容:
{
"secret": "Order Processing pass key"
}
创建一个名为 components 的新目录。导航到该目录并创建一个名为 local-secret-store.yaml 的组件文件,包含以下内容:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: localsecretstore
spec:
type: secretstores.local.file
version: v1
metadata:
- name: secretsFile
value: secrets.json #path to secrets file
- name: nestedSeparator
value: ":"
dapr run 的位置。更多信息:
通过使用密钥 API 调用 Dapr 边车来获取密钥:
curl http://localhost:3601/v1.0/secrets/localsecretstore/secret
查看完整 API 参考。
现在您已经设置了本地密钥存储,调用 Dapr 从应用代码中获取密钥。以下是利用 Dapr SDK 获取密钥的代码示例。
using System;
using System.Threading.Tasks;
using Dapr.Client;
namespace EventService;
const string SECRET_STORE_NAME = "localsecretstore";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
//Resolve a DaprClient from DI
var daprClient = app.Services.GetRequiredService<DaprClient>();
//Use the Dapr SDK to get a secret
var secret = await daprClient.GetSecretAsync(SECRET_STORE_NAME, "secret");
Console.WriteLine($"Result: {string.Join(", ", secret)}");
//dependencies
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
//code
@SpringBootApplication
public class OrderProcessingServiceApplication {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingServiceApplication.class);
private static final ObjectMapper JSON_SERIALIZER = new ObjectMapper();
private static final String SECRET_STORE_NAME = "localsecretstore";
public static void main(String[] args) throws InterruptedException, JsonProcessingException {
DaprClient client = new DaprClientBuilder().build();
//Using Dapr SDK to get a secret
Map<String, String> secret = client.getSecret(SECRET_STORE_NAME, "secret").block();
log.info("Result: " + JSON_SERIALIZER.writeValueAsString(secret));
}
}
#dependencies
import random
from time import sleep
import requests
import logging
from dapr.clients import DaprClient
from dapr.clients.grpc._state import StateItem
from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType
#code
logging.basicConfig(level = logging.INFO)
DAPR_STORE_NAME = "localsecretstore"
key = 'secret'
with DaprClient() as client:
#Using Dapr SDK to get a secret
secret = client.get_secret(store_name=DAPR_STORE_NAME, key=key)
logging.info('Result: ')
logging.info(secret.secret)
#Using Dapr SDK to get bulk secrets
secret = client.get_bulk_secret(store_name=DAPR_STORE_NAME)
logging.info('Result for bulk secret: ')
logging.info(sorted(secret.secrets.items()))
//dependencies
import (
"context"
"log"
dapr "github.com/dapr/go-sdk/client"
)
//code
func main() {
client, err := dapr.NewClient()
SECRET_STORE_NAME := "localsecretstore"
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
//Using Dapr SDK to get a secret
secret, err := client.GetSecret(ctx, SECRET_STORE_NAME, "secret", nil)
if secret != nil {
log.Println("Result : ")
log.Println(secret)
}
//Using Dapr SDK to get bulk secrets
secretBulk, err := client.GetBulkSecret(ctx, SECRET_STORE_NAME, nil)
if secret != nil {
log.Println("Result for bulk: ")
log.Println(secretBulk)
}
}
//dependencies
import { DaprClient, HttpMethod, CommunicationProtocolEnum } from '@dapr/dapr';
//code
const daprHost = "127.0.0.1";
async function main() {
const client = new DaprClient({
daprHost,
daprPort: process.env.DAPR_HTTP_PORT,
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
const SECRET_STORE_NAME = "localsecretstore";
//Using Dapr SDK to get a secret
var secret = await client.secret.get(SECRET_STORE_NAME, "secret");
console.log("Result: " + secret);
//Using Dapr SDK to get bulk secrets
secret = await client.secret.getBulk(SECRET_STORE_NAME);
console.log("Result for bulk: " + secret);
}
main();
一旦你为应用配置了密钥存储,该存储中定义的任何密钥默认都可从 Dapr 应用访问。
你可以通过定义密钥作用域来限制 Dapr 应用对特定密钥的访问。只需向应用配置添加具有限制性权限的密钥作用域策略。
密钥作用域策略适用于任何密钥存储,包括:
观看此视频了解如何将密钥作用域与应用结合使用的演示。
在此示例中,将拒绝在 Kubernetes 集群上运行的应用访问所有密钥,该集群已配置名为 mycustomsecretstore 的Kubernetes 密钥存储。除了用户定义的自定义存储外,该示例还配置 Kubernetes 默认存储(名为 kubernetes)以确保拒绝所有密钥的访问。了解更多关于 Kubernetes 默认密钥存储的信息。
定义以下 appconfig.yaml 配置并使用命令 kubectl apply -f appconfig.yaml 将其应用到 Kubernetes 集群。
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
secrets:
scopes:
- storeName: kubernetes
defaultAccess: deny
- storeName: mycustomsecreststore
defaultAccess: deny
对于需要被拒绝访问 Kubernetes 密钥存储的应用,遵循这些说明,并向应用 Pod 添加以下注解:
dapr.io/config: appconfig
定义后,应用将无法再访问 Kubernetes 密钥存储中的任何密钥。
此示例使用名为 vault 的密钥存储。这可以是为应用设置的 Hashicorp 密钥存储组件。要允许 Dapr 应用仅访问 vault 密钥存储中的 secret1 和 secret2,定义以下 appconfig.yaml:
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
secrets:
scopes:
- storeName: vault
defaultAccess: deny
allowedSecrets: ["secret1", "secret2"]
对 vault 密钥存储的默认访问权限为 deny,而基于 allowedSecrets 列表,应用可以访问某些密钥。了解如何将配置应用到边车。
定义以下 config.yaml:
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
secrets:
scopes:
- storeName: vault
defaultAccess: allow # 此为默认值,可省略该行
deniedSecrets: ["secret1", "secret2"]
此示例配置明确拒绝访问名为 vault 的密钥存储中的 secret1 和 secret2,同时允许访问所有其他密钥。了解如何将配置应用到边车。
allowedSecrets 和 deniedSecrets 列表值优先于 defaultAccess 策略。
| 场景 | defaultAccess | allowedSecrets | deniedSecrets | 权限 |
|---|---|---|---|---|
| 1 - 仅默认访问 | deny/allow | 空 | 空 | deny/allow |
| 2 - 默认拒绝,包含允许列表 | deny | [“s1”] | 空 | 仅能访问 “s1” |
| 3 - 默认允许,包含拒绝列表 | allow | 空 | [“s1”] | 仅不能访问 “s1” |
| 4 - 默认允许,包含允许列表 | allow | [“s1”] | 空 | 仅能访问 “s1” |
| 5 - 默认拒绝,包含拒绝列表 | deny | 空 | [“s1”] | deny |
| 6 - 默认拒绝/允许,包含两个列表 | deny/allow | [“s1”] | [“s2”] | 仅能访问 “s1” |
了解更多关于如何使用 Dapr Configuration 的信息:
在编写应用程序时,消费应用程序配置是一项常见任务。配置存储通常用于管理这些配置数据。配置项通常是动态的,并且与消费它的应用程序的需求紧密耦合。
例如,应用程序配置可能包括:
通常,配置项以键/值对的形式存储在状态存储或数据库中。开发人员或运维人员可以在运行时更改配置存储中的应用程序配置。更改完成后,会通知服务加载新配置。
从应用程序 API 的角度来看,配置数据是只读的,配置存储的更新通过运维工具完成。使用 Dapr 的 Configuration API,您可以:

想测试 Dapr configuration API 吗?请参阅以下快速入门,了解配置 API 的实际应用:
| 快速入门 | 描述 |
|---|---|
| Configuration 快速入门 | 使用 configuration API 获取配置项或订阅配置更改。 |
想跳过快速入门吗?没问题。您可以在应用程序中直接试用 configuration 构建块来读取和管理配置数据。安装 Dapr 后,您可以开始使用 configuration API,从 configuration 操作指南 开始。
按照这些指南操作:
此示例使用 Redis 配置存储组件来演示如何检索配置项。

/dapr/config 端点的自动 HTTP 调用,以减少日志噪音。在使用 dapr run 时使用 --disable-init-endpoints config 标志,或在 Kubernetes 中使用 dapr.io/disable-init-endpoints: "config" 注解。了解有关禁用初始化端点的更多信息。在支持的配置存储中创建一个配置项。这可以是一个简单的键值项,键可以由您自行选择。如前所述,此示例使用 Redis 配置存储组件。
docker run --name my-redis -p 6379:6379 -d redis:6
使用 Redis CLI 连接到 Redis 实例:
redis-cli -p 6379
保存一个配置项:
MSET orderId1 "101||1" orderId2 "102||1"
将以下组件文件保存到您机器上的默认组件文件夹中。您可以使用此文件作为 Dapr 组件 YAML:
kubectl 的 Kubernetes 环境。statestore.yaml 组件相同,如果您已经有 Redis statestore.yaml,可以简单地复制/更改 Redis 状态存储组件类型。apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: configstore
spec:
type: configuration.redis
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: <REDIS_PASSWORD>
以下示例展示如何使用 Dapr Configuration API 获取已保存的配置项。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Dapr.Client;
const string CONFIG_STORE_NAME = "configstore";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
using var client = app.Services.GetRequiredServices<DaprClient>();
var configuration = await client.GetConfiguration(CONFIG_STORE_NAME, [ "orderId1", "orderId2" ]);
Console.WriteLine($"Got key=\n{configuration[0].Key} -> {configuration[0].Value}\n{configuration[1].Key} -> {configuration[1].Value}");
//dependencies
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprClient;
import io.dapr.client.domain.ConfigurationItem;
import io.dapr.client.domain.GetConfigurationRequest;
import io.dapr.client.domain.SubscribeConfigurationRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
//code
private static final String CONFIG_STORE_NAME = "configstore";
public static void main(String[] args) throws Exception {
try (DaprClient client = (new DaprClientBuilder()).build()) {
List<String> keys = new ArrayList<>();
keys.add("orderId1");
keys.add("orderId2");
GetConfigurationRequest req = new GetConfigurationRequest(CONFIG_STORE_NAME, keys);
try {
Mono<List<ConfigurationItem>> items = client.getConfiguration(req);
items.block().forEach(ConfigurationClient::print);
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
}
}
#dependencies
from dapr.clients import DaprClient
#code
with DaprClient() as d:
CONFIG_STORE_NAME = 'configstore'
keys = ['orderId1', 'orderId2']
#Startup time for dapr
d.wait(20)
configuration = d.get_configuration(store_name=CONFIG_STORE_NAME, keys=[keys], config_metadata={})
print(f"Got key={configuration.items[0].key} value={configuration.items[0].value} version={configuration.items[0].version}")
package main
import (
"context"
"fmt"
dapr "github.com/dapr/go-sdk/client"
)
func main() {
ctx := context.Background()
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
items, err := client.GetConfigurationItems(ctx, "configstore", ["orderId1","orderId2"])
if err != nil {
panic(err)
}
for key, item := range items {
fmt.Printf("get config: key = %s value = %s version = %s",key,(*item).Value, (*item).Version)
}
}
import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";
// JS SDK does not support Configuration API over HTTP protocol yet
const protocol = CommunicationProtocolEnum.GRPC;
const host = process.env.DAPR_HOST ?? "localhost";
const port = process.env.DAPR_GRPC_PORT ?? 3500;
const DAPR_CONFIGURATION_STORE = "configstore";
const CONFIGURATION_ITEMS = ["orderId1", "orderId2"];
async function main() {
const client = new DaprClient(host, port, protocol);
// Get config items from the config store
try {
const config = await client.configuration.get(DAPR_CONFIGURATION_STORE, CONFIGURATION_ITEMS);
Object.keys(config.items).forEach((key) => {
console.log("Configuration for " + key + ":", JSON.stringify(config.items[key]));
});
} catch (error) {
console.log("Could not get config item, err:" + error);
process.exit(1);
}
}
main().catch((e) => console.error(e));
Launch a dapr sidecar:
dapr run --app-id orderprocessing --dapr-http-port 3601
在另一个终端中,获取之前保存的配置项:
curl http://localhost:3601/v1.0/configuration/configstore?key=orderId1
启动 Dapr 边车:
dapr run --app-id orderprocessing --dapr-http-port 3601
在另一个终端中,获取之前保存的配置项:
Invoke-RestMethod -Uri 'http://localhost:3601/v1.0/configuration/configstore?key=orderId1'
以下是使用 SDK 订阅使用 configstore 存储组件的键 [orderId1, orderId2] 的代码示例。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Dapr.Client;
using System.Text.Json;
const string DAPR_CONFIGURATION_STORE = "configstore";
var CONFIGURATION_ITEMS = new List<string> { "orderId1", "orderId2" };
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
var client = app.Services.GetRequiredService<DaprClient>();
// Subscribe for configuration changes
var subscribe = await client.SubscribeConfiguration(DAPR_CONFIGURATION_STORE, CONFIGURATION_ITEMS);
// Print configuration changes
await foreach (var items in subscribe.Source)
{
// First invocation when app subscribes to config changes only returns subscription id
if (items.Keys.Count == 0)
{
Console.WriteLine("App subscribed to config changes with subscription id: " + subscribe.Id);
subscriptionId = subscribe.Id;
continue;
}
var cfg = JsonSerializer.Serialize(items);
Console.WriteLine("Configuration update " + cfg);
}
导航到包含上述代码的目录,然后运行以下命令启动 Dapr 边车和订阅者应用程序:
dapr run --app-id orderprocessing -- dotnet run
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Dapr.Client;
using Dapr.Extensions.Configuration;
using System.Collections.Generic;
using System.Threading;
Console.WriteLine("Starting application.");
var builder = WebApplication.CreateBuilder(args);
// Unlike most other situations, we build a `DaprClient` here using its factory because we cannot rely on `IConfiguration`
// or other injected services to configure it because we haven't yet built the DI container.
var client = new DaprClientBuilder().Build();
// In a real-world application, you'd also add the following line to register the `DaprClient` with the DI container so
// it can be injected into other services. In this demonstration, it's not necessary as we're not injecting it anywhere.
// builder.Services.AddDaprClient();
// Get the initial value and continue to watch it for changes
builder.Configuration.AddDaprConfigurationStore("configstore", new List<string>() { "orderId1","orderId2" }, client, TimeSpan.FromSeconds(20));
builder.Configuration.AddStreamingDaprConfigurationStore("configstore", new List<string>() { "orderId1","orderId2" }, client, TimeSpan.FromSeconds(20));
await builder.Build().RunAsync();
Console.WriteLine("Closing application.");
导航到包含上述代码的目录,然后运行以下命令启动 Dapr 边车和订阅者应用程序:
dapr run --app-id orderprocessing -- dotnet run
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprClient;
import io.dapr.client.domain.ConfigurationItem;
import io.dapr.client.domain.GetConfigurationRequest;
import io.dapr.client.domain.SubscribeConfigurationRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
//code
private static final String CONFIG_STORE_NAME = "configstore";
private static String subscriptionId = null;
public static void main(String[] args) throws Exception {
try (DaprClient client = (new DaprClientBuilder()).build()) {
// Subscribe for config changes
List<String> keys = new ArrayList<>();
keys.add("orderId1");
keys.add("orderId2");
Flux<SubscribeConfigurationResponse> subscription = client.subscribeConfiguration(DAPR_CONFIGURATON_STORE,keys);
// Read config changes for 20 seconds
subscription.subscribe((response) -> {
// First ever response contains the subscription id
if (response.getItems() == null || response.getItems().isEmpty()) {
subscriptionId = response.getSubscriptionId();
System.out.println("App subscribed to config changes with subscription id: " + subscriptionId);
} else {
response.getItems().forEach((k, v) -> {
System.out.println("Configuration update for " + k + ": {'value':'" + v.getValue() + "'}");
});
}
});
Thread.sleep(20000);
}
}
导航到包含上述代码的目录,然后运行以下命令启动 Dapr 边车和订阅者应用程序:
dapr run --app-id orderprocessing -- -- mvn spring-boot:run
#dependencies
from dapr.clients import DaprClient
#code
def handler(id: str, resp: ConfigurationResponse):
for key in resp.items:
print(f"Subscribed item received key={key} value={resp.items[key].value} "
f"version={resp.items[key].version} "
f"metadata={resp.items[key].metadata}", flush=True)
def executeConfiguration():
with DaprClient() as d:
storeName = 'configurationstore'
keys = ['orderId1', 'orderId2']
id = d.subscribe_configuration(store_name=storeName, keys=keys,
handler=handler, config_metadata={})
print("Subscription ID is", id, flush=True)
sleep(20)
executeConfiguration()
导航到包含上述代码的目录,然后运行以下命令启动 Dapr 边车和订阅者应用程序:
dapr run --app-id orderprocessing -- python3 OrderProcessingService.py
package main
import (
"context"
"fmt"
"time"
dapr "github.com/dapr/go-sdk/client"
)
func main() {
ctx := context.Background()
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
subscribeID, err := client.SubscribeConfigurationItems(ctx, "configstore", []string{"orderId1", "orderId2"}, func(id string, items map[string]*dapr.ConfigurationItem) {
for k, v := range items {
fmt.Printf("get updated config key = %s, value = %s version = %s \n", k, v.Value, v.Version)
}
})
if err != nil {
panic(err)
}
time.Sleep(20*time.Second)
}
导航到包含上述代码的目录,然后运行以下命令启动 Dapr 边车和订阅者应用程序:
dapr run --app-id orderprocessing -- go run main.go
import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";
// JS SDK does not support Configuration API over HTTP protocol yet
const protocol = CommunicationProtocolEnum.GRPC;
const host = process.env.DAPR_HOST ?? "localhost";
const port = process.env.DAPR_GRPC_PORT ?? 3500;
const DAPR_CONFIGURATION_STORE = "configstore";
const CONFIGURATION_ITEMS = ["orderId1", "orderId2"];
async function main() {
const client = new DaprClient(host, port, protocol);
// Subscribe to config updates
try {
const stream = await client.configuration.subscribeWithKeys(
DAPR_CONFIGURATION_STORE,
CONFIGURATION_ITEMS,
(config) => {
console.log("Configuration update", JSON.stringify(config.items));
}
);
// Unsubscribe to config updates and exit app after 20 seconds
setTimeout(() => {
stream.stop();
console.log("App unsubscribed to config changes");
process.exit(0);
}, 20000);
} catch (error) {
console.log("Error subscribing to config updates, err:" + error);
process.exit(1);
}
}
main().catch((e) => console.error(e));
导航到包含上述代码的目录,然后运行以下命令启动 Dapr 边车和订阅者应用程序:
dapr run --app-id orderprocessing --app-protocol grpc --dapr-grpc-port 3500 -- node index.js
订阅监视配置项后,您将收到所有订阅键的更新。要停止接收更新,您需要显式调用取消订阅 API。
以下是展示如何使用取消订阅 API 取消订阅配置更新的代码示例。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Dapr.Client;
var builder = WebApplication.CreateBuilder();
builder.Services.AddDaprClient();
var app = builder.Build();
const string DAPR_CONFIGURATION_STORE = "configstore";
const string SubscriptionId = "abc123"; //Replace with the subscription identifier to unsubscribe from
var client = app.Services.GetRequiredService<DaprClient>();
await client.UnsubscribeConfiguration(DAPR_CONFIGURATION_STORE, SubscriptionId);
Console.WriteLine("App unsubscribed from config changes");
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprClient;
import io.dapr.client.domain.ConfigurationItem;
import io.dapr.client.domain.GetConfigurationRequest;
import io.dapr.client.domain.SubscribeConfigurationRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
//code
private static final String CONFIG_STORE_NAME = "configstore";
private static String subscriptionId = null;
public static void main(String[] args) throws Exception {
try (DaprClient client = (new DaprClientBuilder()).build()) {
// Unsubscribe from config changes
UnsubscribeConfigurationResponse unsubscribe = client
.unsubscribeConfiguration(subscriptionId, DAPR_CONFIGURATON_STORE).block();
if (unsubscribe.getIsUnsubscribed()) {
System.out.println("App unsubscribed to config changes");
} else {
System.out.println("Error unsubscribing to config updates, err:" + unsubscribe.getMessage());
}
} catch (Exception e) {
System.out.println("Error unsubscribing to config updates," + e.getMessage());
System.exit(1);
}
}
import asyncio
import time
import logging
from dapr.clients import DaprClient
subscriptionID = ""
with DaprClient() as d:
isSuccess = d.unsubscribe_configuration(store_name='configstore', id=subscriptionID)
print(f"Unsubscribed successfully? {isSuccess}", flush=True)
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"time"
dapr "github.com/dapr/go-sdk/client"
)
var DAPR_CONFIGURATION_STORE = "configstore"
var subscriptionID = ""
func main() {
client, err := dapr.NewClient()
if err != nil {
log.Panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := client.UnsubscribeConfigurationItems(ctx, DAPR_CONFIGURATION_STORE , subscriptionID); err != nil {
panic(err)
}
}
import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";
// JS SDK does not support Configuration API over HTTP protocol yet
const protocol = CommunicationProtocolEnum.GRPC;
const host = process.env.DAPR_HOST ?? "localhost";
const port = process.env.DAPR_GRPC_PORT ?? 3500;
const DAPR_CONFIGURATION_STORE = "configstore";
const CONFIGURATION_ITEMS = ["orderId1", "orderId2"];
async function main() {
const client = new DaprClient(host, port, protocol);
try {
const stream = await client.configuration.subscribeWithKeys(
DAPR_CONFIGURATION_STORE,
CONFIGURATION_ITEMS,
(config) => {
console.log("Configuration update", JSON.stringify(config.items));
}
);
setTimeout(() => {
// Unsubscribe to config updates
stream.stop();
console.log("App unsubscribed to config changes");
process.exit(0);
}, 20000);
} catch (error) {
console.log("Error subscribing to config updates, err:" + error);
process.exit(1);
}
}
main().catch((e) => console.error(e));
curl 'http://localhost:<DAPR_HTTP_PORT>/v1.0/configuration/configstore/<subscription-id>/unsubscribe'
Invoke-RestMethod -Uri 'http://localhost:<DAPR_HTTP_PORT>/v1.0/configuration/configstore/<subscription-id>/unsubscribe'
锁用于提供对资源的互斥访问。例如,您可以使用锁来:
任何发生更新的共享资源都可以成为锁的目标。锁通常用于改变状态的操作,而不是读取操作。
每个锁都有一个名称。应用程序决定命名锁访问的资源。通常,同一应用程序的多个实例使用此命名锁来独占访问资源并执行更新。
例如,在竞争消费者模式中,应用程序的多个实例访问一个队列。您可以决定在应用程序运行其业务逻辑时锁定队列。
在下图中,同一应用程序 App1 的两个实例使用 Redis 锁组件 对共享资源进行加锁。

*此 API 目前处于 Alpha 状态。
在任何给定时刻,只有一个应用程序实例可以持有命名锁。锁的作用域限定为 Dapr app-id。
Dapr 分布式锁使用基于租约的锁定机制。如果应用程序获取锁后遇到异常而无法释放锁,则锁会在一段时间后通过租约自动释放。这可以防止应用程序失败时的资源死锁。
遵循以下指南:
现在你已经了解了 Dapr 分布式锁 API 构建块所提供的功能,接下来学习它如何在你的服务中工作。在本指南中,一个示例应用程序使用 Redis 锁组件获取锁,以演示如何锁定资源。有关支持的锁存储列表,请参阅此参考页面。
在下图中,同一应用程序的两个实例尝试获取锁,其中一个实例成功,另一个被拒绝。

下图显示同一应用程序的两个实例,其中一个实例释放锁后,另一个实例能够获取该锁。

下图显示两个不同应用程序的实例,在同一资源上获取不同的锁。

将以下组件文件保存到你机器上的默认组件文件夹中。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: lockstore
spec:
type: lock.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: <PASSWORD>
curl -X POST http://localhost:3500/v1.0-alpha1/lock/lockstore
-H 'Content-Type: application/json'
-d '{"resourceId":"my_file_name", "lockOwner":"random_id_abc123", "expiryInSeconds": 60}'
using System;
using Dapr.Client;
namespace LockService
{
class Program
{
[Obsolete("Distributed Lock API is in Alpha, this can be removed once it is stable.")]
static async Task Main(string[] args)
{
string DAPR_LOCK_NAME = "lockstore";
string fileName = "my_file_name";
var client = new DaprClientBuilder().Build();
await using (var fileLock = await client.Lock(DAPR_LOCK_NAME, fileName, "random_id_abc123", 60))
{
if (fileLock.Success)
{
Console.WriteLine("Success");
}
else
{
Console.WriteLine($"Failed to lock {fileName}.");
}
}
}
}
}
package main
import (
"fmt"
dapr "github.com/dapr/go-sdk/client"
)
func main() {
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
resp, err := client.TryLockAlpha1(ctx, "lockstore", &dapr.LockRequest{
LockOwner: "random_id_abc123",
ResourceID: "my_file_name",
ExpiryInSeconds: 60,
})
fmt.Println(resp.Success)
}
curl -X POST http://localhost:3500/v1.0-alpha1/unlock/lockstore
-H 'Content-Type: application/json'
-d '{"resourceId":"my_file_name", "lockOwner":"random_id_abc123"}'
using System;
using Dapr.Client;
namespace LockService
{
class Program
{
static async Task Main(string[] args)
{
string DAPR_LOCK_NAME = "lockstore";
var client = new DaprClientBuilder().Build();
var response = await client.Unlock(DAPR_LOCK_NAME, "my_file_name", "random_id_abc123"));
Console.WriteLine(response.status);
}
}
}
package main
import (
"fmt"
dapr "github.com/dapr/go-sdk/client"
)
func main() {
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer client.Close()
resp, err := client.UnlockAlpha1(ctx, "lockstore", &UnlockRequest{
LockOwner: "random_id_abc123",
ResourceID: "my_file_name",
})
fmt.Println(resp.Status)
}
阅读分布式锁 API 概述了解更多内容。
通过 cryptography 构建块,您可以以安全且一致的方式使用加密功能。Dapr 公开的 API 允许您在密钥保管库或 Dapr 边车中执行加密和解密等操作,而不会将加密密钥暴露给您的应用程序。
应用程序广泛使用加密技术,如果实施正确,即使数据泄露,也能使解决方案更加安全。在某些情况下,您可能需要使用加密来满足行业法规(例如金融领域)或法律要求(包括 GDPR 等隐私法规)。
然而,正确使用加密技术可能很困难。您需要:
安全的一个重要要求是限制对加密密钥的访问,这通常被称为"原始密钥材料"。Dapr 可以与密钥保管库(如 Azure Key Vault,未来的版本将支持更多组件)集成,这些保管库在安全飞地中存储密钥并在保管库内执行加密操作,而不会将密钥暴露给您的应用程序或 Dapr。
或者,您可以配置 Dapr 为您管理加密密钥,在边车内执行操作,同样不会将原始密钥材料暴露给您的应用程序。
使用 Dapr,您可以执行加密操作而不会将加密密钥暴露给您的应用程序。

通过使用 cryptography 构建块,您可以:
Dapr cryptography 构建块包含两类组件:
允许与管理服务或保管库(“密钥保管库”)交互的组件。
类似于 Dapr 如何在各种密钥存储或状态存储之上提供"抽象层",这些组件允许与各种密钥保管库(如 Azure Key Vault,未来 Dapr 版本将支持更多)交互。使用这些组件时,对私钥的加密操作在保管库内执行,Dapr 永远不会看到您的私钥。
基于 Dapr 自身加密引擎的组件。
当密钥保管库不可用时,您可以利用基于 Dapr 自身加密引擎的组件。这些名称中包含 .dapr. 的组件在 Dapr 边车内执行加密操作,密钥存储在文件、Kubernetes Secret 或其他来源中。虽然 Dapr 知道私钥,但您的应用程序仍然无法访问它们。
两类组件(无论是利用密钥保管库还是使用 Dapr 中的加密引擎)都提供相同的抽象层。这允许您的解决方案根据需要在各种保管库和/或加密组件之间切换。例如,您可以在开发期间使用本地存储的密钥,在生产环境中使用云保管库。
Cryptography API 允许使用 Dapr Crypto Scheme v1 加密和解密数据。这是一种固执己见的加密方案,旨在使用现代、安全的加密标准,并将数据(包括大文件)作为流高效处理。
想测试 Dapr Cryptography API?请参阅以下快速入门和教程,了解 cryptography 的实际应用:
| 快速入门/教程 | 描述 |
|---|---|
| Cryptography 快速入门 | 使用 RSA 和 AES 密钥通过 cryptography API 加密和解密消息和大文件。 |
想跳过快速入门?没问题。您可以直接在应用程序中试用 cryptography 构建块来加密和解密您的应用程序。安装 Dapr 后,您可以开始使用 cryptography API,从 cryptography 操作指南 开始。
观看 Dapr Community Call #83 中 Cryptography API 的演示视频:
现在您已经阅读了 Cryptography 作为 Dapr 构建块 的相关内容,让我们通过 SDK 来演练如何使用 cryptography API。
在您的项目中使用 Dapr SDK(通过 gRPC API),您可以加密数据流,例如文件或字符串:
# 当传递数据(缓冲区或字符串)时,`encrypt` 返回包含加密消息的 Buffer
def encrypt_decrypt_string(dapr: DaprClient):
message = 'The secret is "passw0rd"'
# 加密消息
resp = dapr.encrypt(
data=message.encode(),
options=EncryptOptions(
# Cryptography 组件的名称(必需)
component_name=CRYPTO_COMPONENT_NAME,
# 存储在 cryptography 组件中的密钥(必需)
key_name=RSA_KEY_NAME,
# 用于包装密钥的算法,必须由上述命名的密钥支持。
# 选项包括:"RSA"、"AES"
key_wrap_algorithm='RSA',
),
)
# 该方法返回一个可读流,我们在内存中完整读取它
encrypt_bytes = resp.read()
print(f'Encrypted the message, got {len(encrypt_bytes)} bytes')
在您的项目中使用 Dapr SDK(通过 gRPC API),您可以加密缓冲区或字符串中的数据:
// 当传递数据(缓冲区或字符串)时,`encrypt` 返回包含加密消息的 Buffer
const ciphertext = await client.crypto.encrypt(plaintext, {
// Dapr 组件的名称(必需)
componentName: "mycryptocomponent",
// 存储在组件中的密钥名称(必需)
keyName: "mykey",
// 用于包装密钥的算法,必须由上述命名的密钥支持。
// 选项包括:"RSA"、"AES"
keyWrapAlgorithm: "RSA",
});
这些 API 也可以与流一起使用,以便在数据来自流时更高效地加密数据。下面的示例使用流加密文件,并将其写入另一个文件:
// `encrypt` 可以用作 Duplex 流
await pipeline(
fs.createReadStream("plaintext.txt"),
await client.crypto.encrypt({
// Dapr 组件的名称(必需)
componentName: "mycryptocomponent",
// 存储在组件中的密钥名称(必需)
keyName: "mykey",
// 用于包装密钥的算法,必须由上述命名的密钥支持。
// 选项包括:"RSA"、"AES"
keyWrapAlgorithm: "RSA",
}),
fs.createWriteStream("ciphertext.out"),
);
在您的项目中使用 Dapr SDK(通过 gRPC API),您可以加密字符串或字节数组中的数据:
using var client = new DaprClientBuilder().Build();
const string componentName = "azurekeyvault"; // 更改此项以匹配您的 cryptography 组件
const string keyName = "myKey"; // 更改此项以匹配您的加密存储中密钥的名称
const string plainText = "This is the value we're going to encrypt today";
// 将字符串编码为 UTF-8 字节数组并加密
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
var encryptedBytesResult = await client.EncryptAsync(componentName, plaintextBytes, keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa));
在您的项目中使用 Dapr SDK,您可以加密数据流,例如文件。
out, err := sdkClient.Encrypt(context.Background(), rf, dapr.EncryptOptions{
// Dapr 组件的名称(必需)
ComponentName: "mycryptocomponent",
// 存储在组件中的密钥名称(必需)
KeyName: "mykey",
// 用于包装密钥的算法,必须由上述命名的密钥支持。
// 选项包括:"RSA"、"AES"
Algorithm: "RSA",
})
下面的示例将 Encrypt API 放在上下文中,代码读取文件、加密文件,然后将结果存储在另一个文件中。
// 输入文件,明文
rf, err := os.Open("input")
if err != nil {
panic(err)
}
defer rf.Close()
// 输出文件,已加密
wf, err := os.Create("output.enc")
if err != nil {
panic(err)
}
defer wf.Close()
// 使用 Dapr 加密数据
out, err := sdkClient.Encrypt(context.Background(), rf, dapr.EncryptOptions{
// 这是 3 个必需参数
ComponentName: "mycryptocomponent",
KeyName: "mykey",
Algorithm: "RSA",
})
if err != nil {
panic(err)
}
// 读取流并将其复制到输出文件
n, err := io.Copy(wf, out)
if err != nil {
panic(err)
}
fmt.Println("Written", n, "bytes")
下面的示例使用 Encrypt API 来加密字符串。
// 输入字符串
rf := strings.NewReader("Amor, ch'a nullo amato amar perdona, mi prese del costui piacer sì forte, che, come vedi, ancor non m'abbandona")
// 使用 Dapr 加密数据
enc, err := sdkClient.Encrypt(context.Background(), rf, dapr.EncryptOptions{
ComponentName: "mycryptocomponent",
KeyName: "mykey",
Algorithm: "RSA",
})
if err != nil {
panic(err)
}
// 将加密数据读入字节切片
enc, err := io.ReadAll(enc)
if err != nil {
panic(err)
}
要解密数据流,请使用 decrypt。
def encrypt_decrypt_string(dapr: DaprClient):
message = 'The secret is "passw0rd"'
# ...
# 解密加密的数据
resp = dapr.decrypt(
data=encrypt_bytes,
options=DecryptOptions(
# Cryptography 组件的名称(必需)
component_name=CRYPTO_COMPONENT_NAME,
# 存储在 cryptography 组件中的密钥(必需)
key_name=RSA_KEY_NAME,
),
)
# 该方法返回一个可读流,我们在内存中完整读取它
decrypt_bytes = resp.read()
print(f'Decrypted the message, got {len(decrypt_bytes)} bytes')
print(decrypt_bytes.decode())
assert message == decrypt_bytes.decode()
使用 Dapr SDK,您可以解密缓冲区中的数据或使用流。
// 当将数据作为缓冲区传递时,`decrypt` 返回包含解密消息的 Buffer
const plaintext = await client.crypto.decrypt(ciphertext, {
// 唯一必需的选项是组件名称
componentName: "mycryptocomponent",
});
// `decrypt` 也可以用作 Duplex 流
await pipeline(
fs.createReadStream("ciphertext.out"),
await client.crypto.decrypt({
// 唯一必需的选项是组件名称
componentName: "mycryptocomponent",
}),
fs.createWriteStream("plaintext.out"),
);
要解密字符串,请在您的项目中使用 ‘DecryptAsync’ gRPC API。
在下面的示例中,我们将获取一个字节数组(例如来自上面的示例)并将其解密为 UTF-8 编码的字符串。
public async Task<string> DecryptBytesAsync(byte[] encryptedBytes)
{
using var client = new DaprClientBuilder().Build();
const string componentName = "azurekeyvault"; // 更改此项以匹配您的 cryptography 组件
const string keyName = "myKey"; // 更改此项以匹配您的加密存储中密钥的名称
var decryptedBytes = await client.DecryptAsync(componentName, encryptedBytes, keyName);
var decryptedString = Encoding.UTF8.GetString(decryptedBytes.ToArray());
return decryptedString;
}
要解密文件,请在您的项目中使用 Decrypt gRPC API。
在下面的示例中,out 是一个可以写入文件或在内存中读取的流,如上面的示例所示。
out, err := sdkClient.Decrypt(context.Background(), rf, dapr.EncryptOptions{
// 唯一必需的选项是组件名称
ComponentName: "mycryptocomponent",
})
许多应用程序需要作业调度,或者需要在将来执行某个操作。Jobs API 是一个编排器,用于调度这些未来的作业,可以在特定时间或特定间隔执行。
Jobs API 不仅帮助你调度作业,而且在内部,Dapr 使用 Scheduler 服务来调度 actor reminders。
Dapr 中的 Jobs 由以下部分组成:

Jobs API 是一个作业调度器,而不是运行作业的执行器。该设计保证至少一次作业执行,优先考虑持久化和水平扩展而非精确性。这意味着:
所有计划作业的作业详细信息和用户关联数据都存储在 Scheduler 服务中的嵌入式 Etcd 数据库中。
你可以使用 jobs 来:
作业调度在以下场景中可能很有用:
自动化数据库备份: 确保数据库每天备份以防止数据丢失。安排备份脚本每天凌晨 2 点运行,这将创建数据库备份并将其存储在安全位置。
定期数据处理和 ETL(提取、转换、加载): 处理和转换来自各种来源的原始数据并将其加载到数据仓库中。安排 ETL 作业在特定时间运行(例如:每小时、每天)以获取新数据、处理它,并用最新信息更新数据仓库。
电子邮件通知和报告: 通过电子邮件接收每日销售报告和每周绩效摘要。安排一个作业生成所需的报告,并通过电子邮件发送,每天报告在每天早上 6 点发送,每周摘要在每周一早上 8 点发送。
维护任务和系统更新: 执行定期维护任务,如清除临时文件、更新软件和检查系统运行状况。安排各种维护脚本在非高峰时段运行,例如周末或深夜,以最大程度减少对用户的干扰。
金融交易的批处理: 处理大量需要批处理并在每个工作日结束时进行结算的交易。安排批处理作业在每个工作日下午 5 点运行,汇总当天的交易并执行必要的结算和对账。
Dapr 的 jobs API 确保这些场景中代表的任务始终如一且可靠地执行,无需手动干预,从而提高效率并降低错误风险。
Jobs API 的主要功能允许你创建、检索和删除计划作业。默认情况下,当你创建一个名称已存在的作业时,操作会失败,除非你显式地将 overwrite 标志设置为 true。这确保现有作业不会被意外修改或覆盖。
当你创建一个作业时,它不会替换同名的现有作业,除非你显式设置 overwrite 标志。这意味着每次创建作业时,它都会重置计数,并且只在该作业的嵌入式 etcd 中保留 1 条记录。因此,你不必担心创建和触发多个作业——只有最新的作业被记录和执行,即使你的所有应用程序在启动时都调度相同的作业。
Scheduler 服务使作业的调度能够跨多个副本扩展,同时保证作业仅由 1 个 Scheduler 服务实例触发。
你可以在你的应用程序中试用 jobs API。在安装 Dapr后,你就可以开始使用 jobs API,从操作指南:调度作业指南开始。
现在你已经大致了解了 jobs 构建块 ,让我们深入了解 Dapr Jobs 和各种 SDK 包含的特性和概念。Dapr Jobs:
500ms)。实际触发精度可能因运行时而异;基于 Cron 的调度仅支持秒级精度。所有作业都使用区分大小写的作业名称进行注册。这些名称在整个与 Dapr 运行时交互的服务中应该是唯一的。名称用作创建和修改作业时的标识符,也用于指示触发的调用与哪个作业相关联。
任意时刻一个名称只能关联一个作业。默认情况下,尝试创建与现有作业同名的作业会报错。然而,如果 overwrite 标志设置为 true,则新作业会覆盖同名作业。
可以通过以下方式调度作业:
对于所有基于时间的调度,如果通过 RFC3339 规范提供了带时区的时间戳,则使用该时区。如果未提供,则使用运行 Dapr 的服务器所在的时区。换句话说,除非在调度作业时另有指定,否则不要假设时间以 UTC 时区运行。
使用 Cron 表达式调度作业按特定间隔执行时,表达式使用 6 个字段,字段值范围见下表:
| seconds | minutes | hours | day of month | month | day of week |
|---|---|---|---|---|---|
| 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-6/sun-sat |
"0 30 * * * *" 每小时的半点触发。
"0 15 3 * * *" 每天 03:15 触发。
可以使用 Go 持续时间字符串 调度作业,其中字符串由一个(可能有符号的)十进制数字序列组成,每个数字可带有可选的小数部分和单位后缀。有效的时间单位为 "ns"、"us"、"ms"、"s"、"m" 或 "h"。
"2h45m" 每 2 小时 45 分钟触发一次。
"37m25s" 每 37 分钟 25 秒触发一次。
支持以下周期表达式。"@every" 表达式也接受 Go 持续时间字符串。
| 表达式 | 描述 | 等效 Cron 表达式 |
|---|---|---|
| @every | 每隔指定时间运行(例如 “@every 1h30m”) | 不适用 |
| @yearly(或 @annually) | 每年一次,1 月 1 日午夜 | 0 0 0 1 1 * |
| @monthly | 每月一次,月初午夜 | 0 0 0 1 * * |
| @weekly | 每周一次,周日午夜 | 0 0 0 * * 0 |
| @daily 或 @midnight | 每天午夜运行一次 | 0 0 0 * * * |
| @hourly | 每小时整点运行一次 | 0 0 * * * * |
也可以通过使用 RFC3339 规范 提供日期,将作业调度到特定时间点执行。
"2025-12-09T16:09:53+00:00" 表示作业应在 2025 年 12 月 9 日 UTC 时间下午 4:09:53 执行。
当预定的 Dapr 作业被触发时,运行时根据服务启动时向 Dapr 注册的方式,通过 HTTP 或 gRPC 向调度该作业的服务发送消息。
当作业到达预定的触发时间时,触发的作业通过以下回调函数发送回应用程序:
注意: 以下示例使用 Go 语言,但适用于任何支持 gRPC 的编程语言。
import rtv1 "github.com/dapr/dapr/pkg/proto/runtime/v1"
...
func (s *JobService) OnJobEventAlpha1(ctx context.Context, in *rtv1.JobEventRequest) (*rtv1.JobEventResponse, error) {
// Handle the triggered job
}
此函数在 gRPC 服务器的上下文中处理触发的作业。设置服务器时,请确保注册回调服务器,该服务器在作业被触发时调用此函数:
...
js := &JobService{}
rtv1.RegisterAppCallbackAlphaServer(server, js)
在此设置下,你可以完全控制如何接收和处理触发的作业,因为它们通过此 gRPC 方法直接路由。
如果应用程序启动时未向 Dapr 注册 gRPC 服务器,则 Dapr 会通过向端点 /job/<job-name> 发送 POST 请求来触发作业。请求体包含以下作业信息:
Schedule:作业触发的时间RepeatCount:可选值,指示作业应重复的次数DueTime:可选的时间点,表示作业应执行的单次时间(如果非重复),或者调度生效的最早开始时间Ttl:可选值,指示作业何时过期Payload:包含作业调度时原始存储数据的字节集合Overwrite:标志,允许请求的作业覆盖已存在的同名作业FailurePolicy:作业的可选失败策略DueTime 和 Ttl 字段将反映 RFC3339 时间戳值,反映作业最初调度时提供的时区。如果未提供时区,这些值表示运行 Dapr 的服务器使用的时区。
虽然作业通过 API 调用创建,但你可以通过 dapr scheduler CLI 命令管理(列出、检查、删除、备份和恢复)作业。
dapr scheduler list --filter app
NAME BEGIN COUNT LAST TRIGGER
app/my-app/my-job -3.89s 1 2025-10-03T16:58:55Z
app/my-app/another-job -3.89s 1 2025-10-03T16:58:55Z
dapr scheduler list -o wide
NAMESPACE NAME BEGIN EXPIRATION SCHEDULE DUE TIME TTL REPEATS COUNT LAST TRIGGER
default app/my-app/my-job 2025-10-03T16:58:55Z @every 5s 2025-10-03T17:58:55+01:00 100 1 2025-10-03T16:58:55Z
dapr scheduler get app/my-app/my-job -o yaml
删除指定作业:
dapr scheduler delete app/my-app/my-job
删除应用的所有作业:
dapr scheduler delete-all app/my-app
导出所有作业:
dapr scheduler export -o jobs-backup.bin
之后导入:
dapr scheduler import -f jobs-backup.bin
既然你已经了解了作业构建块提供了什么,让我们来看一个如何使用该 API 的示例。下面的代码示例描述了一个为数据库备份应用程序调度作业并在触发时间处理它们的应用程序,触发时间也称为作业因其到达 dueTime 而被发送回应用程序的时间。
当你在自托管模式或 Kubernetes 上运行 dapr init时,Dapr Scheduler 服务会自动启动。
在你的代码中,设置和调度应用程序内的作业。
下面的 .NET SDK 代码示例调度名为 prod-db-backup 的作业。作业数据包含有关你将定期备份的数据库的信息。在本示例的整个过程中,你将:
在下面的示例中,你将创建记录,这些记录将与作业一起序列化和注册,以便在将来作业被触发时信息可用:
db-backup)Metadata,包括:DBName)BackupLocation)创建一个 ASP.NET Core 项目并从 NuGet 添加最新版本的 Dapr.Jobs。
注意: 虽然你的项目不必严格使用
Microsoft.NET.Sdk.WebSDK 来创建作业,但在编写本文档时,只有调度作业的服务才会接收其触发调用。由于这些调用期望有一个可以处理作业触发的端点,并且需要Microsoft.NET.Sdk.WebSDK,因此建议你为此目的使用 ASP.NET Core 项目。
首先定义类型以持久化我们的备份作业数据,并对属性应用我们自己的 JSON 属性名称属性,使其与其他语言示例保持一致。
//Define the types that we'll represent the job data with
internal sealed record BackupJobData([property: JsonPropertyName("task")] string Task, [property: JsonPropertyName("metadata")] BackupMetadata Metadata);
internal sealed record BackupMetadata([property: JsonPropertyName("DBName")]string DatabaseName, [property: JsonPropertyName("BackupLocation")] string BackupLocation);
接下来,作为应用程序设置的一部分,设置一个处理程序,该处理程序将在任何时候应用程序上触发作业时被调用。该处理程序负责根据提供的作业名称确定应如何处理作业。
这是通过在 /job/<job-name> 处向 ASP.NET Core 注册一个处理程序来实现的,其中 <job-name> 是参数化的,并传递给此处理程序委托,这满足了 Dapr 期望有一个端点来处理触发的命名作业的要求。
使用以下内容填充你的 Program.cs 文件:
using System.Text;
using System.Text.Json;
using Dapr.Jobs;
using Dapr.Jobs.Extensions;
using Dapr.Jobs.Models;
using Dapr.Jobs.Models.Responses;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient();
var app = builder.Build();
//Registers an endpoint to receive and process triggered jobs
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
app.MapDaprScheduledJobHandler((string jobName, ReadOnlyMemory<byte> jobPayload, ILogger logger, CancellationToken cancellationToken) => {
logger?.LogInformation("Received trigger invocation for job '{jobName}'", jobName);
switch (jobName)
{
case "prod-db-backup":
// Deserialize the job payload metadata
var jobData = JsonSerializer.Deserialize<BackupJobData>(jobPayload);
// Process the backup operation - we assume this is implemented elsewhere in your code
await BackupDatabaseAsync(jobData, cancellationToken);
break;
}
}, cancellationTokenSource.Token);
await app.RunAsync();
最后,作业本身需要向 Dapr 注册,以便它可以在以后的时间点被触发。你可以通过将 DaprJobsClient 注入到类中并作为应用程序的入站操作的一部分来执行此操作,但为了本示例的目的,它将放在你上面开始使用的 Program.cs 文件的底部。因为你将使用通过依赖注入注册的 DaprJobsClient,所以首先创建一个作用域以便你可以访问它。
//Create a scope so we can access the registered DaprJobsClient
await using scope = app.Services.CreateAsyncScope();
var daprJobsClient = scope.ServiceProvider.GetRequiredService<DaprJobsClient>();
//Create the payload we wish to present alongside our future job triggers
var jobData = new BackupJobData("db-backup", new BackupMetadata("my-prod-db", "/backup-dir"));
//Serialize our payload to UTF-8 bytes
var serializedJobData = JsonSerializer.SerializeToUtf8Bytes(jobData);
//Schedule our backup job to run every minute, but only repeat 10 times
await daprJobsClient.ScheduleJobAsync("prod-db-backup", DaprJobSchedule.FromDuration(TimeSpan.FromMinutes(1)),
serializedJobData, repeats: 10);
下面的 Go SDK 代码示例调度名为 prod-db-backup 的作业。作业数据位于备份数据库("my-prod-db")中,并使用 ScheduleJobAlpha1 进行调度。这提供了 jobData,包括:
Task 名称Metadata,包括:DBName)BackupLocation)package main
import (
//...
daprc "github.com/dapr/go-sdk/client"
"github.com/dapr/go-sdk/examples/dist-scheduler/api"
"github.com/dapr/go-sdk/service/common"
daprs "github.com/dapr/go-sdk/service/grpc"
)
func main() {
// Initialize the server
server, err := daprs.NewService(":50070")
// ...
if err = server.AddJobEventHandler("prod-db-backup", prodDBBackupHandler); err != nil {
log.Fatalf("failed to register job event handler: %v", err)
}
log.Println("starting server")
go func() {
if err = server.Start(); err != nil {
log.Fatalf("failed to start server: %v", err)
}
}()
// ...
// Set up backup location
jobData, err := json.Marshal(&api.DBBackup{
Task: "db-backup",
Metadata: api.Metadata{
DBName: "my-prod-db",
BackupLocation: "/backup-dir",
},
},
)
// ...
}
作业通过设置的 Schedule 和所需的 Repeats 数量进行调度。这些设置确定作业应被触发并发送回应用程序的最大次数。
在此示例中,在触发时间(根据 Schedule 为 @every 1s),此作业被触发并发送回应用程序,直到达到最大 Repeats(10)。
// ...
// Set up the job
job := daprc.Job{
Name: "prod-db-backup",
Schedule: "@every 1s",
Repeats: 10,
Data: &anypb.Any{
Value: jobData,
},
}
当作业被触发时,Dapr 会自动将作业路由到你在服务器初始化期间设置的事件处理程序。例如,在 Go 中,你会像这样注册事件处理程序:
...
if err = server.AddJobEventHandler("prod-db-backup", prodDBBackupHandler); err != nil {
log.Fatalf("failed to register job event handler: %v", err)
}
Dapr 处理底层路由。当作业被触发时,会使用触发的作业数据调用你的 prodDBBackupHandler 函数。以下是处理触发的作业的示例:
// ...
// At job trigger time this function is called
func prodDBBackupHandler(ctx context.Context, job *common.JobEvent) error {
var jobData common.Job
if err := json.Unmarshal(job.Data, &jobData); err != nil {
// ...
}
var jobPayload api.DBBackup
if err := json.Unmarshal(job.Data, &jobPayload); err != nil {
// ...
}
fmt.Printf("job %d received:\n type: %v \n typeurl: %v\n value: %v\n extracted payload: %v\n", jobCount, job.JobType, jobData.TypeURL, jobData.Value, jobPayload)
jobCount++
return nil
}
一旦你在应用程序中设置了 Jobs API,在终端窗口中使用以下命令运行 Dapr 边车。
dapr run --app-id=distributed-scheduler \
--metrics-port=9091 \
--dapr-grpc-port 50001 \
--app-port 50070 \
--app-protocol grpc \
--log-level debug \
go run ./main.go
Dapr 的对话 API 降低了大规模安全可靠地与大型语言模型 (LLM) 交互的复杂性。无论您是缺乏必要原生 SDK 的开发者,还是只想专注于 LLM 交互的提示工程的多语言团队,对话 API 都提供了一个一致的 API 入口点来与底层 LLM 提供商通信。

除了启用关键的性能和安全功能(如缓存和 PII 清理),对话 API 还提供:
您还可以将对话 API 与 Dapr 功能结合使用,例如:
以下功能是所有支持的对话组件 开箱即用的。
对话 API 支持两种缓存:
promptCacheRetention 参数在每个请求中启用此功能(例如,24h 用于 OpenAI)。有关请求级选项,请参阅对话 API 参考。支持情况取决于提供商。responseCacheTTL(例如 10m)时,Dapr 会根据请求(提示和选项)缓存响应。重复的相同请求将从缓存提供,无需调用 LLM,从而降低延迟和成本。此缓存在内存中且每个边车独立。请在您的对话组件 spec 中配置。您可以通过在请求中传递 responseFormat(JSON Schema)来请求模型的结构化输出。支持 Deepseek、Google AI、Hugging Face、OpenAI 和 Anthropic。请参阅对话 API 参考。
响应可以包含对话的令牌使用量(promptTokens、completionTokens、totalTokens)。请参阅 API 参考中的响应内容。
PII 混淆功能可识别并清除对话响应中任何形式的敏感用户信息。只需在输入和输出数据上启用 PII 混淆即可保护您的隐私,清除可能被用来识别个人的敏感细节。
PII 清理器会混淆以下用户信息:
对话 API 支持高级工具调用能力,允许 LLM 与外部函数和 API 交互。这使您能够构建复杂的 AI 应用,这些应用可以:
工具调用遵循 OpenAI 的函数调用格式,便于与现有 AI 开发工作流和工具集成。
观看在 Diagrid 的 Dapr v1.15 庆祝活动 上进行的演示,了解如何使用 .NET SDK 了解对话 API 的工作原理。
想让 Dapr 对话 API 接受测试吗?通过以下快速入门和教程了解实际效果:
| 快速入门/教程 | 描述 |
|---|---|
| 对话快速入门 | 了解如何使用对话 API 与大型语言模型 (LLM) 交互。 |
想跳过快速入门吗?没问题。您可以直接在应用程序中试用对话构建块。在安装 Dapr 后,您可以开始使用对话 API,从操作指南开始。
让我们开始使用对话 API。在本指南中,您将学习如何:
dapr run 运行连接。创建一个名为 conversation.yaml 的新配置文件,并保存到应用程序目录下的 components 或 config 子文件夹中。
选择您的首选对话组件规范用于您的 conversation.yaml 文件。
对于此场景,我们使用简单的 echo 组件。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: echo
spec:
type: conversation.echo
version: v1
要与真实的 LLM 交互,请使用其他支持的对话组件,包括 OpenAI、Hugging Face、Anthropic、DeepSeek 等。
例如,要将 echo 模拟组件替换为 OpenAI 组件,请使用以下内容替换 conversation.yaml 文件。您需要将 API 密钥复制到组件文件中。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: openai
spec:
type: conversation.openai
metadata:
- name: key
value: <REPLACE_WITH_YOUR_KEY>
- name: model
value: gpt-4-turbo
以下示例使用 Dapr SDK 客户端与 LLM 进行交互。
using Dapr.AI.Conversation;
using Dapr.AI.Conversation.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprConversationClient();
var app = builder.Build();
var conversationClient = app.Services.GetRequiredService<DaprConversationClient>();
var response = await conversationClient.ConverseAsync("conversation",
new List<DaprConversationInput>
{
new DaprConversationInput(
"Please write a witty haiku about the Dapr distributed programming framework at dapr.io",
DaprConversationRole.Generic)
});
Console.WriteLine("conversation output: ");
foreach (var resp in response.Outputs)
{
Console.WriteLine($"\t{resp.Result}");
}
//dependencies
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprPreviewClient;
import io.dapr.client.domain.ConversationInput;
import io.dapr.client.domain.ConversationRequest;
import io.dapr.client.domain.ConversationResponse;
import reactor.core.publisher.Mono;
import java.util.List;
public class Conversation {
public static void main(String[] args) {
String prompt = "Please write a witty haiku about the Dapr distributed programming framework at dapr.io";
try (DaprPreviewClient client = new DaprClientBuilder().buildPreviewClient()) {
System.out.println("Input: " + prompt);
ConversationInput daprConversationInput = new ConversationInput(prompt);
// Component name is the name provided in the metadata block of the conversation.yaml file.
Mono<ConversationResponse> responseMono = client.converse(new ConversationRequest("echo",
List.of(daprConversationInput))
.setContextId("contextId")
.setScrubPii(true).setTemperature(1.1d));
ConversationResponse response = responseMono.block();
System.out.printf("conversation output: %s", response.getConversationOutputs().get(0).getResult());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
#dependencies
from dapr.clients import DaprClient
from dapr.clients.grpc._request import ConversationInput
#code
with DaprClient() as d:
inputs = [
ConversationInput(content="Please write a witty haiku about the Dapr distributed programming framework at dapr.io", role='user', scrub_pii=True),
]
metadata = {
'model': 'modelname',
'key': 'authKey',
'responseCacheTTL': '10m',
}
response = d.converse_alpha1(
name='echo', inputs=inputs, temperature=0.7, context_id='chat-123', metadata=metadata
)
for output in response.outputs:
print(f'conversation output: {output.result}')
package main
import (
"context"
"fmt"
dapr "github.com/dapr/go-sdk/client"
"log"
)
func main() {
client, err := dapr.NewClient()
if err != nil {
panic(err)
}
input := dapr.ConversationInput{
Content: "Please write a witty haiku about the Dapr distributed programming framework at dapr.io",
// Role: "", // Optional
// ScrubPII: false, // Optional
}
fmt.Printf("conversation input: %s\n", input.Content)
var conversationComponent = "echo"
request := dapr.NewConversationRequest(conversationComponent, []dapr.ConversationInput{input})
resp, err := client.ConverseAlpha1(context.Background(), request)
if err != nil {
log.Fatalf("err: %v", err)
}
fmt.Printf("conversation output: %s\n", resp.Outputs[0].Result)
}
use dapr::client::{ConversationInputBuilder, ConversationRequestBuilder};
use std::thread;
use std::time::Duration;
type DaprClient = dapr::Client<dapr::client::TonicClient>;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Sleep to allow for the server to become available
thread::sleep(Duration::from_secs(5));
// Set the Dapr address
let address = "https://127.0.0.1".to_string();
let mut client = DaprClient::connect(address).await?;
let input = ConversationInputBuilder::new("Please write a witty haiku about the Dapr distributed programming framework at dapr.io").build();
let conversation_component = "echo";
let request =
ConversationRequestBuilder::new(conversation_component, vec![input.clone()]).build();
println!("conversation input: {:?}", input.content);
let response = client.converse_alpha1(request).await?;
println!("conversation output: {:?}", response.outputs[0].result);
Ok(())
}
使用 dapr run 命令启动连接。例如,对于此场景,我们在一个 app ID 为 conversation 的应用程序上运行 dapr run,并指向 ./config 目录中的对话 YAML 文件。
dapr run --app-id conversation --dapr-grpc-port 50001 --log-level debug --resources-path ./config -- dotnet run
dapr run --app-id conversation --dapr-grpc-port 50001 --log-level debug --resources-path ./config -- mvn spring-boot:run
dapr run --app-id conversation --dapr-grpc-port 50001 --log-level debug --resources-path ./config -- python3 app.py
dapr run --app-id conversation --dapr-grpc-port 50001 --log-level debug --resources-path ./config -- go run ./main.go
dapr run --app-id=conversation --resources-path ./config --dapr-grpc-port 3500 -- cargo run --example conversation
预期输出
- '== APP == conversation output: Please write a witty haiku about the Dapr distributed programming framework at dapr.io'
对话 API 支持以下功能:
提示缓存: 允许开发者在 Dapr 中缓存提示,从而获得更快的响应时间,并降低 LLM 提供商缓存插入提示的出口成本。
PII 清理: 允许对进入和离开 LLM 的数据进行混淆处理。
工具调用: 允许 LLM 与外部函数和 API 进行交互。
要了解如何启用这些功能,请参阅对话 API 参考指南。
使用支持 SDK 仓库中提供的完整示例来体验对话 API。