概述
在 Orleans 流系统中,subscriptionId
是用于标识流订阅的唯一标识符。它有两种类型:显式订阅 和隐式订阅,每种类型都有不同的生成机制。
1. 订阅类型分类
显式订阅 (Explicit Subscription)
- 通过代码显式调用
SubscribeAsync()
方法创建的订阅 - 使用随机生成的 GUID
- 在 GUID 的最后字节的高位设置为 0
隐式订阅 (Implicit Subscription)
- 基于 Grain 类型和流命名空间自动匹配的订阅
- 使用确定性算法生成 GUID
- 在 GUID 的最后字节的高位设置为 1
2. 生成流程
2.1 显式订阅 ID 生成
csharp
// 在 GrainBasedPubSubRuntime.cs 中
public GuidId CreateSubscriptionId(QualifiedStreamId streamId, GrainId streamConsumer)
{
// 1. 生成随机 GUID
Guid subscriptionId = Guid.NewGuid();
// 2. 标记为显式订阅(清除最后字节的高位)
subscriptionId = SubscriptionMarker.MarkAsExplicitSubscriptionId(subscriptionId);
// 3. 包装为 GuidId
return GuidId.GetGuidId(subscriptionId);
}
2.2 隐式订阅 ID 生成
csharp
// 在 ImplicitStreamSubscriberTable.cs 中
private Guid MakeSubscriptionGuid(GrainType grainType, QualifiedStreamId streamId)
{
Span<byte> bytes = stackalloc byte[16];
// 1. 使用 Grain 类型的哈希码
BinaryPrimitives.WriteUInt32LittleEndian(bytes, grainType.GetUniformHashCode());
// 2. 使用 StreamId 的哈希码
BinaryPrimitives.WriteUInt32LittleEndian(bytes[4..], streamId.StreamId.GetUniformHashCode());
// 3. 使用 StreamId 的键索引
BinaryPrimitives.WriteUInt32LittleEndian(bytes[8..], streamId.StreamId.GetKeyIndex());
// 4. 使用 Provider 名称的稳定哈希
BinaryPrimitives.WriteUInt32LittleEndian(bytes[12..], StableHash.ComputeHash(streamId.ProviderName));
// 5. 标记为隐式订阅(设置最后字节的高位)
return SubscriptionMarker.MarkAsImplictSubscriptionId(new(bytes));
}
3. 订阅标记机制
3.1 SubscriptionMarker 类
csharp
internal static class SubscriptionMarker
{
// 标记为显式订阅:清除最后字节的高位 (0x7F)
internal static Guid MarkAsExplicitSubscriptionId(Guid subscriptionGuid)
{
return MarkSubscriptionGuid(subscriptionGuid, false);
}
// 标记为隐式订阅:设置最后字节的高位 (0x80)
internal static Guid MarkAsImplictSubscriptionId(Guid subscriptionGuid)
{
return MarkSubscriptionGuid(subscriptionGuid, true);
}
// 检查是否为隐式订阅
internal static bool IsImplicitSubscription(Guid subscriptionGuid)
{
Span<byte> guidBytes = stackalloc byte[16];
subscriptionGuid.TryWriteBytes(guidBytes);
// 检查最后字节的高位是否设置
return (guidBytes[15] & 0x80) != 0;
}
}
4. 完整生成流程
4.1 显式订阅流程
-
用户调用订阅方法
csharpawait stream.SubscribeAsync(observer);
-
StreamConsumer 创建订阅 ID
csharp// 在 StreamConsumer.cs 中 GuidId subscriptionId = pubSub.CreateSubscriptionId(stream.InternalStreamId, myGrainReference.GetGrainId());
-
GrainBasedPubSubRuntime 生成 ID
csharpGuid subscriptionId = SubscriptionMarker.MarkAsExplicitSubscriptionId(Guid.NewGuid());
-
设置观察者
csharpvar subscriptionHandle = myExtension.SetObserver(subscriptionId, stream, observer, batchObserver, token, filterData);
4.2 隐式订阅流程
-
系统检查隐式订阅资格
csharp// 在 ImplicitStreamPubSub.cs 中 if (!implicitTable.TryGetImplicitSubscriptionGuid(grainId, streamId, out subscriptionGuid)) { throw new ArgumentOutOfRangeException(streamId.ToString(), "Only implicit subscriptions are supported."); }
-
ImplicitStreamSubscriberTable 生成确定性 ID
csharp// 基于 Grain 类型、StreamId 和 Provider 名称生成确定性 GUID subscriptionId = MakeSubscriptionGuid(grainId.Type, streamId);
-
标记为隐式订阅
csharpreturn SubscriptionMarker.MarkAsImplictSubscriptionId(new(bytes));
5. 关键特性
5.1 唯一性保证
- 显式订阅 :使用
Guid.NewGuid()
保证全局唯一性 - 隐式订阅:使用确定性算法,相同输入产生相同输出
5.2 类型识别
- 通过 GUID 最后字节的高位区分订阅类型
- 0x80 = 隐式订阅,0x7F = 显式订阅
5.3 性能优化
- 隐式订阅使用确定性算法,避免重复计算
- 使用
Span<byte>
和stackalloc
减少内存分配
6. 使用场景
6.1 显式订阅
- 用户主动订阅特定流
- 需要精确控制订阅行为
- 支持取消订阅
6.2 隐式订阅
- 基于命名空间自动匹配
- 简化开发者的订阅逻辑
- 适用于广播场景
7. 总结
Orleans 的 subscriptionId 生成机制设计精巧,通过不同的生成策略和标记机制,既保证了唯一性,又支持了显式和隐式两种订阅模式。这种设计在保证性能的同时,提供了灵活的流订阅管理能力。