Dapr 状态管理构建基块
分布式应用程序由独立服务组成。 虽然每个服务都应是无状态的,但某些服务必须跟踪状态才能完成业务操作。 请考虑电子商务网站的购物篮服务。 如果服务无法跟踪状态,则客户可能因为离开网站丢失购物篮内容,从而导致销售损失和不愉快的客户体验。 对于这些情况,需要将状态持久保存在分布式状态存储中。 Dapr 状态管理构建基块简化了状态跟踪,并跨各种数据存储提供高级功能。
若要试用状态管理构建基块,请参阅第 3 章中的计数器应用程序示例。
它的用途
跟踪分布式应用程序中的状态可能很有挑战性。 例如:
- 应用程序可能需要不同类型的数据存储。
- 访问和更新数据可能需要不同的一致性级别。
- 多个用户可以同时更新数据,这需要解决冲突。
- 服务必须重试与数据存储交互时发生的任何短期暂时性错误。
Dapr 状态管理构建基块解决了这些难题。 它简化了跟踪状态,没有依赖关系或第三方存储 SDK 学习曲线。
重要
Dapr 状态管理提供密钥/值 API。 该功能不支持关系数据存储或图形数据存储。
工作原理
应用程序与 Dapr sidecar 进行交互,以存储和检索键/值数据。 在后台,sidecar API 使用可配置的状态存储组件来持久保存数据。 开发人员可以从不断增长的受支持状态存储集合中选择,这些存储包括 Azure Cosmos DB、SQL Server 和 Cassandra。
可使用 HTTP 或 gRPC 调用 API。 使用以下 URL 调用 HTTP API:
http://localhost:<dapr-port>/v1.0/state/<store-name>/
<dapr-port>
:Dapr 侦听的 HTTP 端口。<store-name>
:要使用的状态存储组件的名称。
图 5-1 显示已启用 Dapr 的购物篮服务如何使用名为 statestore
的 Dapr 状态存储组件存储键/值对。
图 5-1。 在 Dapr 状态存储中存储键/值对。
请注意上图中的步骤:
- 购物篮服务在 Dapr sidecar 上调用状态管理 API。 请求正文包含一个 JSON 数组,该数组可以包含多个键/值对。
- Dapr sidecar 根据组件配置文件确定状态存储。 在本例中,它是 Redis 缓存状态存储。
- sidecar 将数据持久保存到 Redis 缓存。
检索存储的数据是类似的 API 调用。 在以下示例中,curl 命令通过调用 Dapr sidecar API 检索数据:
curl http://localhost:3500/v1.0/state/statestore/basket1
该命令返回响应正文中的存储状态:
{
"items": [
{
"itemId": "DaprHoodie",
"quantity": 1
}
],
"customerId": 1
}
以下部分介绍如何使用状态管理构建基块的更高级功能。
一致性
CAP 定理是一组适用于存储状态的分布式系统的原则。 图 5-2 显示了 CAP 定理的三个属性。
图 5-2。 CAP 定理。
该定理指出,分布式数据系统将在一致性、可用性和分区容错之间做出权衡。 而且,任何数据存储只能保证三个属性中的两个:
一致性 (C)。 群集中的每个节点都会使用最新数据进行响应(即使系统必须阻止请求),直到所有副本都更新。 如果你向“一致性系统”查询当前正在更新的项,直到所有副本都成功更新,才会获得响应。 不过,你将始终收到最新的数据。
可用性 (A)。 每个节点都会返回即时响应,即使该响应不是最新数据。 如果你向“可用系统”查询正在更新的项,将获得服务此时可以提供的最佳答案。
分区容错 (P)。 保证系统继续运行,即使复制的数据节点发生故障或者与其他复制的数据节点断开连接。
分布式应用程序必须处理 P 属性。 当服务与网络调用相互通信时,会发生网络中断 (P)。 因此,分布式应用程序必须是 AP 或 CP。
AP 应用程序选择可用性,而不选择一致性。 Dapr 通过最终一致性策略支持此选择。 请考虑基础数据存储(例如 Azure CosmosDB),它在多个副本上存储冗余数据。 借助最终一致性,状态存储会将更新写入副本,并完成客户端的写入请求。 之后,存储将异步更新其副本。 读取请求可以从任何副本返回数据(包括尚未收到最新更新的副本)。
CP 应用程序选择一致性,而不选择可用性。 Dapr 通过其强一致性策略支持此选择。 在此方案中,状态存储将在完成写入请求前同步更新所有必需副本(或者,在某些情况下,达到仲裁)。 读取操作将跨副本一致地返回最新数据。
通过向操作附加一致性提示来指定状态操作的一致性级别。 以下 curl 命令使用强一致性提示将 键/值对写入状态存储:
curl -X POST http://localhost:3500/v1.0/state/<store-name> \
-H "Content-Type: application/json" \
-d '[
{
"key": "Hello",
"value": "World",
"options": {
"consistency": "strong"
}
}
]'
重要
由 Dapr 状态存储组件完成附加到该操作的一致性提示。 并非所有数据存储都支持这两种一致性级别。 如果未设置一致性提示,则默认行为是最终一致性。
并发
在多用户应用程序中,多个用户可能(同时)更新同一数据。 Dapr 支持使用乐观并发控制 (OCC) 来管理冲突。 OCC 基于以下假设:更新冲突不常见,因为用户处理数据的不同部分。 更有效的做法是假设更新会成功,如果失败则重试。 实现悲观锁定的替代方法可能会影响性能,长时间运行的锁定会导致数据争用。
Dapr 支持使用 ETag 进行乐观并发控制 (OCC)。 ETag 是一个与存储的键/值对的特定版本关联的值。 每次键/值对更新时,ETag 值也会更新。 当客户端检索键/值对时,响应包括当前的 ETag 值。 客户端更新或删除键/值对时,必须将该 ETag 值发送回请求正文中。 如果另一个客户端同时更新了数据,则 ETag 不匹配,请求将失败。 此时,客户端必须检索已更新的数据,再次进行更改,然后重新提交更新。 此策略称为“先写入优先”。
Dapr 还支持“后写入优先”策略。 通过此方法,客户端不会将 ETag 附加到写入请求。 状态存储组件将始终允许更新(即使基础值在会话期间已更改)。 后写入优先非常适用于低数据争用的高吞吐量写入方案。 也可以容忍覆盖偶尔的用户更新。
事务
Dapr 可以将多项更改写入数据存储,作为事务实现的单个操作。 此功能仅适用于支持 ACID 事务的数据存储。 撰写本文时,这些存储包括 Redis、MongoDB、PostgreSQL、SQL Server 和 Azure CosmosDB。
在以下示例中,多项操作将发送到单个事务中的状态存储。 所有操作都必须成功,事务才能提交。 如果一个或多个操作失败,则回退整个事务。
curl -X POST http://localhost:3500/v1.0/state/<store-name>/transaction \
-H "Content-Type: application/json" \
-d '{
"operations": [
{
"operation": "upsert",
"request": { "key": "Key1", "value": "Value1"
}
},
{
"operation": "delete",
"request": { "key": "Key2" }
}
]
}'
对于不支持事务的数据存储,仍可将多个密钥作为单个请求发送。 以下示例演示批量写入操作:
curl -X POST http://localhost:3500/v1.0/state/<store-name> \
-H "Content-Type: application/json" \
-d '[
{ "key": "Key1", "value": "Value1" },
{ "key": "Key2", "value": "Value2" }
]'
对于批量操作,Dapr 将每个键/值对更新作为单独的请求提交到数据存储。
使用 Dapr .NET SDK
Dapr .NET SDK 为 .NET 平台提供特定于语言的支持。 开发人员可以使用DaprClient
中引入的 DaprClient
类来读取和写入数据。 以下示例演示了如何使用 DaprClient.GetStateAsync<TValue>
方法从缓存中检索数据。 方法需要存储名称 statestore
和密钥 AMS
作为参数:
var weatherForecast = await daprClient.GetStateAsync<WeatherForecast>("statestore", "AMS");
如果状态存储不包含密钥 AMS
的数据,则结果为 default(WeatherForecast)
。
若要将数据写入数据存储,请使用 DaprClient.SaveStateAsync<TValue>
方法:
daprClient.SaveStateAsync("statestore", "AMS", weatherForecast);
该示例使用“后写入优先”策略,因为 ETag 值不会传递给状态存储组件。 若要将乐观并发控制 (OCC) 与“先写入优先”策略一起使用,请首先使用 方法检索当前的 ETag。 然后,写入更新的值,并使用 DaprClient.TrySaveStateAsync
方法传递检索到的 ETag。
var (weatherForecast, etag) = await daprClient.GetStateAndETagAsync<WeatherForecast>("statestore", city);
// ... make some changes to the retrieved weather forecast
var result = await daprClient.TrySaveStateAsync("statestore", city, weatherForecast, etag);
检索数据后,数据(以及关联的 ETag)在状态存储中被更改时,DaprClient.TrySaveStateAsync
方法将失败。 该方法返回一个布尔值,以指示调用是否成功。 处理失败的一种策略是,仅需从状态存储重新加载更新后的数据,再次进行更改,然后重新提交更新。
如果始终希望写入成功,而不考虑对数据的其他更改,请使用“后写入优先”策略。
SDK 提供其他方法用于批量检索数据、删除数据和执行事务。 有关详细信息,请参阅 Dapr .NET SDK 存储库。
ASP.NET Core 集成
Dapr 还支持 ASP.NET Core,它是构建基于云的新式 Web 应用程序的跨平台框架。 Dapr SDK 将状态管理功能直接集成到 ASP.NET Core 模型绑定功能中。 配置很简单。 在 Program.cs
文件中,在 WebApplication
生成器上调用以下扩展方法:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers().AddDapr();
配置后,Dapr 可以将键/值对直接注入使用 ASP.NET Core FromState
属性的控制器操作。 不再需要引用 DaprClient
对象。 下一个示例演示返回给定城市天气预报的 Web API:
[HttpGet("{city}")]
public ActionResult<WeatherForecast> Get([FromState("statestore", "city")] StateEntry<WeatherForecast> forecast)
{
if (forecast.Value == null)
{
return NotFound();
}
return forecast.Value;
}
在示例中,控制器使用 FromState
属性加载天气预报。 第一个属性参数是状态存储 statestore
。 第二个属性参数 city
是获取状态密钥的city
变量的名称。 如果省略第二个参数,则将使用绑定方法参数 (forecast
) 的名称来查找路由模板变量。
StateEntry
类包含为单个键/值对检索到的各种属性的所有信息:StoreName
、Key
、Value
和 ETag
。 ETag 可用于实现乐观并发控制 (OCC) 策略。 该类还提供无需 DaprClient
实例即可删除或更新检索到的键/值数据的方法。 在下一个示例中,TrySaveAsync
方法用于使用 OCC 更新检索到的天气预报。
[HttpPut("{city}")]
public async Task Put(WeatherForecast updatedForecast, [FromState("statestore", "city")] StateEntry<WeatherForecast> currentForecast)
{
// update cached current forecast with updated forecast passed into service endpoint
currentForecast.Value = updatedForecast;
// update state store
var success = await currentForecast.TrySaveAsync();
// ... check result
}
状态存储组件
在撰写本文时,Dapr 支持以下事务状态存储:
- Azure CosmosDB
- Azure SQL Server
- CockroachDB
- 内存中
- MongoDB
- MySQL
- Oracle 数据库
- PostgreSQL
- Redis
- RethinkDB
Dapr 还包括对支持 CRUD 操作但不支持事务功能的状态存储的支持:
- Aerospike
- Apache Cassandra
- AWS DynamoDB
- Azure Blob 存储
- Azure 表存储
- Couchbase
- GCP Firestore
- Hashicorp Consul
- Hazelcast
- JetStream KV
- Memcached
- Oracle 对象存储
- Zookeeper
配置
初始化本地自承载开发时,Dapr 将 Redis 注册为默认状态存储。 以下是默认状态存储配置的示例。 记住默认名称 statestore
:
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"
注意
许多状态存储可以注册到单个应用程序,每个应用程序具有不同的名称。
Redis 状态存储需要 redisHost
和 redisPassword
元数据来连接到 Redis 实例。 在以上示例中,Redis 密码(默认情况下为空字符串)存储为纯字符串形式。 最佳做法是避免使用纯文本字符串,并始终使用机密引用。 若要详细了解机密管理,请参阅第 10 章。
另一个元数据字段 actorStateStore
指示执行组件构建基块是否可以使用状态存储。
密钥前缀策略
状态存储组件支持不同的策略在基础存储中存储键/值对。 回想一下前面有关购物篮服务的示例,该服务存储客户想要购买的项目:
curl -X POST http://localhost:3500/v1.0/state/statestore \
-H "Content-Type: application/json" \
-d '[{
"key": "basket1",
"value": {
"customerId": 1,
"items": [
{ "itemId": "DaprHoodie", "quantity": 1 }
]
}
}]'
使用 Redis 控制台工具,在 Redis 缓存中查看 Redis 状态存储组件如何持久保存数据:
127.0.0.1:6379> KEYS *
1) "basketservice||basket1"
127.0.0.1:6379> HGETALL basketservice||basket1
1) "data"
2) "{\"items\":[{\"itemId\":\"DaprHoodie\",\"quantity\":1}],\"customerId\":1}"
3) "version"
4) "1"
输出将数据的完整 Redis 密钥显示为 。 默认情况下,Dapr 使用 Dapr 实例 (basketservice
) 的 application id
作为密钥的前缀。 此命名约定使多个 Dapr 实例可以共享相同的数据存储,而不会发生密钥名称冲突。 对于开发人员,在使用 Dapr 运行应用程序时,必须始终指定相同的 application id
。 如果省略,Dapr 将生成唯一的应用程序 ID。 如果更改 application id
,应用程序将无法再访问使用上一个密钥前缀存储的状态。
也就是说,在状态存储组件文件的 元数据字段中,可以配置密钥前缀的常量值。 请考虑以下示例:
spec:
metadata:
- name: keyPrefix
- value: MyPrefix
常量密钥前缀允许跨多个 Dapr 应用程序访问状态存储。 此外,将 keyPrefix
设置为 none
完全省略前缀。
示例应用程序:Dapr 流量控制
在 Dapr 流量控制示例应用中,TrafficControl 服务使用 Dapr 状态管理构建基块来保存每辆过往车辆的进出时间戳。 图 5-3 显示了 Dapr 流量控制示例应用程序的概念体系结构。 Dapr 状态管理构建基块在图中标记有数字 3 的流中使用:
图 5-3。 Dapr 流量控制示例应用程序的概念体系结构。
入口和出口事件逻辑由 TrafficController
类(一个普通的 ASP.NET 控制器)处理。 TrafficController.VehicleEntry
方法接受传入 VehicleRegistered
消息并保存封闭车辆状态:
// store vehicle state
var vehicleState = new VehicleState
{
LicenseNumber = msg.LicenseNumber,
EntryTimestamp = msg.Timestamp
};
await _vehicleStateRepository.SaveVehicleStateAsync(vehicleState);
在以上代码片段中,抽象 _vehicleStateRepository
负责将状态保存到数据存储。 具体实现 DaprVehicleStateRepository
如下所示:
public class DaprVehicleStateRepository : IVehicleStateRepository
{
private const string DAPR_STORE_NAME = "statestore";
private readonly DaprClient _daprClient;
public DaprVehicleStateRepository(DaprClient daprClient)
{
_daprClient = daprClient;
}
public async Task SaveVehicleStateAsync(VehicleState vehicleState)
{
await _daprClient.SaveStateAsync<VehicleState>(
DAPR_STORE_NAME, vehicleState.LicenseNumber, vehicleState);
}
public async Task<VehicleState> GetVehicleStateAsync(string licenseNumber)
{
return await _daprClient.GetStateAsync<VehicleState>(
DAPR_STORE_NAME, licenseNumber);
}
}
如前面的代码片段所示,实现 DaprVehicleStateRepository
类非常简单。 SaveVehicleStateAsync
方法使用注入的 DaprClient
对象将状态保存到配置的 Dapr 状态存储。 它使用车辆许可证号作为密钥。 应用程序可以通过调用 GetVehicleStateAsync
方法检索保存的状态。
TrafficControl 服务使用 Redis 作为其基础数据存储。 只查看代码,你永远不会了解它。 使用 Dapr 状态管理构建基块的服务不会直接引用任何状态组件。 而 Dapr 组件配置文件指定存储:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
namespace: dapr-trafficcontrol
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
secretKeyRef:
name: state.redisPassword
key: state.redisPassword
scopes:
- trafficcontrolservice
注意
组件配置文件包含元素 secretKeyRef
。 应用程序使用它从 Dapr 机密构建块引用 Redis 密码值。 请参阅第 10 章,详细了解如何通过 Dapr 管理机密。
配置中的 type
元素 state.redis
指示构建基块使用 Dapr Redis 组件管理状态。
配置中的 scopes
元素限制应用程序对状态存储组件的访问。 只有 TrafficControl 服务可以访问状态存储。
总结
Dapr 状态管理构建块提供了一个 API,用于在各种数据存储区中存储键/值数据。 API 支持以下内容:
- 批量操作
- 强一致性和最终一致性
- 乐观并发控制
- 多项事务
.NET SDK 为 .NET 和 ASP.NET Core 提供特定于语言的支持。 模型绑定集成简化了从 ASP.NET Core 控制器操作方法访问和更新状态。
在 Dapr 流量控制示例应用程序中,使用 Dapr 状态管理的好处显而易见:
- 它还消除了使用第三方 SDK(例如
StackExchange.Redis
)的复杂性。 - 将基础 Redis 缓存替换为不同类型的数据存储只需要更改组件配置文件。