
ET ActorLocation 包开发指南Location 路由、分布式锁与 MessageLocationSender 的架构与实战【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET导读本文面向 ET9Unity3D Client And C# Server Framework的服务器开发者系统讲解cn.etetet.actorlocation包的职责边界与开发规范。你将理解 Actor Location 路由如何持久化到 DB、Location 分布式锁为何必须采用 token 闭环LockWithToken 带lockToken的UnLock、代理层LocationProxyComponent如何在主备切换与跟随者拒绝时重试以及MessageLocationSender如何缓存 ActorId 并按 LocationType 高效投递消息。读完本文你可以安全地在本包之上扩展 Actor 路由功能并掌握该包配套的测试运行方式。包职责总览cn.etetet.actorlocation在 ET9 中承担的核心职责有三块见 AGENTS.mdActor Location 路由维护「逻辑 Idlong key LocationType → ActorId」的映射关系支持 Add / Remove / Get 基本操作Location 锁支持对某个路由项加锁LockWithToken、解锁带lockToken的UnLock并内建锁超时自动失效机制代理重试与MessageLocationSender客户端进程通过LocationProxyComponent向主 Location 服务发起请求并自动重试MessageLocationSender缓存目标 ActorId避免每次发送都查一次 Location。此外文档特别强调一条持久化红线Location 路由状态会持久化到 DB修改持久化结构之前必须设计迁移方案。这是因为 Location 路由是跨进程、跨重启的关键状态若直接变更存储结构而不迁移将导致旧数据无法读取或读取到错误路由。对应到仓库源码这个职责由以下核心文件承载模型层LocationComponent.cs、LocationProxyComponent.cs、MessageLocationSender.cs热更逻辑层LocationComponentSystem.cs、LocationProxyComponentSystem.cs、MessageLocationSenderComponentSystem.cs协议层ActorLocation_S_20600.proto对象路由协议、ActorLocation_C_10900.proto测试用客户端协议Location 路由的数据结构与持久化路由模型每个逻辑对象在 Location 服务中对应一条LocationInfoLocationComponent.cs它以对象 Id 作为实体 Id内部用Dictionaryint, LocationTypeState TypeStates按 LocationType 保存多份路由状态[ChildOf(typeof(LocationComponent))] public class LocationInfo: Entity, IAwake, IDestroy { [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)] public Dictionaryint, LocationTypeState TypeStates new(); } public struct LocationTypeState { public ActorId ActorId; // 目标 ActorId public long LockToken; // 非 0 表示该路由被加锁 public long LockExpireTime; // 锁超时时间ServerNow 时间戳0 表示永不过期 }注意LocationTypeState是一个 struct锁的「是否有效」由LockToken ! 0判定超时则由LockExpireTime 0 ServerNow LockExpireTime判定LocationComponentSystem.cs。DB 持久化常量持久化相关的常量集中定义在LocationPersistenceConstpublic static class LocationPersistenceConst { public const string DBName Share; // 数据库名 public const string RouteCollection LocationInfo; // 集合名 }LocationComponent通过GetDBComponent()从DBManagerComponent按 Zone 拿到DBComponent所有 Add / Remove / Lock / UnLock 在修改内存缓存后都会通过SaveInfoToDB写入集合LocationInfo当某个 key 的所有路由都被移除时会调用RemoveInfoFromDB删除整条记录LocationComponentSystem.cs。所有写库操作都包裹在CoroutineLockType.LocationPersistence协程锁下并以 key 作为锁粒度确保同一对象的并发写不会互相覆盖。同时写库失败时会基于写前快照RestoreStates回滚内存状态保证内存与 DB 的一致性如 LocationComponentSystem.cs。主备 Location 服务与 DB 共享LocationComponent上带有IsPrimaryLocation与PrimaryLocationSceneName两个字段用于标识当前 Scene 是否为该 Zone 的 Location 主服务。系统在收到服务变更事件OnServiceChangeAddService/OnServiceChangeRemoveService时调用RefreshPrimaryState()通过ServiceDiscoveryProxy.GetBySceneType(SceneType.Location)获取全部 Location 服务并用LocationPrimaryHelper.SelectStablePrimarySceneName选出稳定的主服务LocationComponentSystem.cs。主备选择的核心算法在 LocationPrimaryHelper.cs每个 Location 服务通过ServiceMetaKey.PriorityId元数据上报优先级优先选择priorityId最小者优先级相同则按SceneName字典序最小者保证结果确定且稳定同一批服务永远选同一个主主服务崩溃后剩余服务中会重新选出一个新的主旧主恢复时由于优先级相同则按名字稳定回切。因为所有 Location 服务读写同一个 DBShare库的LocationInfo集合备机无需同步即可在主机挂掉后立即接管。一旦节点从主降级为备RefreshPrimaryState会调用ClearAllCache()清空内存缓存避免缓存与 DB 不一致LocationComponentSystem.cs。Location 锁token 闭环与超时机制为什么需要 token 闭环Location 锁用于防止「对象正在迁移/转移所有权」期间其他进程把消息发到旧 ActorId。如果只有「加锁/解锁」而没有 token 校验可能出现以下竞态A 进程加锁随后崩溃解锁消息永远不来B 进程在锁被系统清理后解锁误把新 owner 的状态清掉迟到的旧解锁请求与新加锁请求相互覆盖。因此本包采用token 闭环设计见 AGENTS.md 开发规则调用方通过LockWithToken获取 tokenlong之后必须调用带lockToken参数的UnLock完成解锁解锁时校验lockToken与当前持锁 token 一致才允许执行。LockWithToken / UnLock 的源码语义LocationProxyComponent提供的接口LocationProxyComponentSystem.cspublic static async ETTasklong LockWithToken(this LocationProxyComponent self, int type, long key, ActorId actorId, int time 60000); public static async ETTask UnLock(this LocationProxyComponent self, int type, long key, ActorId oldActorId, ActorId newActorId, long lockToken);LockWithToken默认超时time 60000毫秒即锁默认 60 秒自动过期传0表示永不过期。UnLock在成功后会顺便完成「所有权转移」把该路由的 ActorId 从oldActorId替换为newActorId并清除 token。在 LocationComponentSystem.cs 的锁实现中LocationComponent.Lock会执行以下校验链若已存在锁且已超时IsExpiredLock先清掉旧锁再继续若锁仍有效且state.ActorId actorId视为幂等加锁直接返回已有 token若锁仍有效但 owner 不同抛出ERR_LocationAlreadyLocked若路由已存在且 owner 与请求者不同抛出ERR_LocationLockOwnerMismatch通过后生成IdGenerater.GenerateId()作为LockToken计算过期时间并持久化。UnLock的校验链LocationComponentSystem.cs锁不存在或已过期 →ERR_LocationLockNotFoundlockToken 0或与当前持锁 token 不一致 →ERR_LocationLockTokenMismatcholdActorId与当前 owner 不一致 →ERR_LocationLockOwnerMismatch全部通过后才将ActorId换成newActorId、清空LockToken与LockExpireTime并落库。旧无 token API 的处理策略按照开发规则不要新增无 token 解锁路径旧无 token API 只保留为编译期阻断入口。仓库中的具体做法LocationProxyComponentSystem.cs[Obsolete(Use LockWithToken and pass the returned token to UnLock., true)] public static async ETTask Lock(...) { await self.LockWithToken(type, key, actorId, time); } [Obsolete(Use UnLock overload with lockToken., true)] public static ETTask UnLock(...) // 直接抛出 ERR_LocationLockTokenMismatchLock用[Obsolete(..., true)]标记并转发到LockWithToken任何新代码引用旧Lock都会在编译期报错无 token 的UnLock直接在运行期抛异常从行为上杜绝绕过 token 校验的可能。LocationComponent层同样只保留带 token 的UnLock重载LocationComponentSystem.cs将「锁必须 token 闭环」从 API 设计层面强制落实。锁超时自动失效与解锁补偿超时自动失效IsExpiredLock在所有读路径Add / Get / Lock / UnLock上生效锁超时后路由状态会被自动清理避免因持锁进程崩溃导致路由永久不可用。解锁补偿UnLockWithRetryLocationProxyComponentSystem.cs当UnLock抛出ERR_LocationLockNotFound/ERR_LocationLockOwnerMismatch/ERR_LocationLockTokenMismatch时代理会先Get当前路由若已经指向目标newActorId则视为补偿成功直接返回否则按重试次数递增的间隔继续重试最多locationRequestRetryTimes次。这两个机制共同保证了即使解锁消息在网络上丢失、或主服务发生了切换最终也能收敛到正确状态仓库对应测试见 Actorlocation_UnlockHistory_Compensation_Test.cs、Actorlocation_LockTimeout_AutoUnlock_Test.cs。LocationProxyComponent主备切换与重试LocationProxyComponent是所有业务进程访问 Location 服务的入口。它自己不存路由数据而是把请求转发给当前 Zone 的主 Location 服务并在失败时自动重试。关键配置项LocationProxyComponent.cs 定义了三个可调字段其默认值在 LocationProxyComponentSystem.cs 中初始化字段默认值含义primaryLocationSceneName空当前缓存的主 Location 服务 Scene 名primaryLocationPriorityId0当前缓存的主服务优先级 IdlocationRequestRetryTimes20请求失败的最大重试次数locationRequestRetryIntervalMs100每次重试之间的基础等待间隔毫秒主服务选择与切换GetPrimaryLocationSceneName()每次请求前都会用LocationPrimaryHelper重新计算稳定的主服务若与本地缓存一致则直接返回否则更新缓存并记录切换日志LocationProxyComponentSystem.cs。当收到OnServiceChangeRemoveService事件且被移除的正是当前主服务时代理会清空primaryLocationSceneName与primaryLocationPriorityId强制下一次请求重新选择主服务LocationProxyComponentSystem.cs。请求重试机制所有路由请求Add / Get / Lock / UnLock / Remove都经由CallPrimaryWithRetryLocationProxyComponentSystem.cs发送解析当前主服务 ActorId用MessageSender.Call发送请求若响应为ERR_LocationFollowerRejected或ERR_LocationPrimaryUnavailable说明打到了备机或主已失效清空主缓存并重试若调用抛异常如主服务不可用同样清空主缓存并重试每轮重试前TimerComponent.WaitAsync(locationRequestRetryIntervalMs)等待 100ms超过locationRequestRetryTimes默认 20 次才放弃。此外Get还针对ERR_LocationGetRetry目标正被加锁、路由暂不可读做退避重试等待间隔为locationRequestRetryIntervalMs * retryCount逐次放大LocationProxyComponentSystem.cs。跟随者拒绝与主不可用主 Location 服务通过EnsurePrimaryLocationComponentSystem.cs校验自己是否仍是主是主 → 放行请求不是主但知道自己应跟随的主场景名 → 返回ERR_LocationFollowerRejectedlocation is follower完全找不到主 → 返回ERR_LocationPrimaryUnavailable。这样备机不会误写路由而客户端代理收到这两个错误码后会自动切换主并重试从而实现无感知的主备切换。MessageLocationSender按 LocationType 缓存与投递消息MessageLocationSender解决的核心问题是业务进程不能每次发 Actor 消息都查一次 Location。它把「目标 ActorId」缓存在本地只有缓存失效时才去 Location 服务查询。三层结构MessageLocationSenderComponent挂在 Scene 上按 LocationType 管理多个子组件MessageLocationSenderComponentSystem.csMessageLocationSenderOneType以 LocationType 为 Id 的子实体负责某一种 LocationType 的发送队列与过期清理MessageLocationSender以对象 Id 为 Id 的叶子实体持有ActorId与LastSendOrRecvTimeMessageLocationSender.cs。[ChildOf(typeof(MessageLocationSenderOneType))] public class MessageLocationSender: Entity, IAwake, IDestroy { public ActorId ActorId; // 缓存的最近目标 ActorId public long LastSendOrRecvTime; // 最近收发消息的时间用于过期回收 } [ChildOf(typeof(MessageLocationSenderComponent))] public class MessageLocationSenderOneType: Entity, IAwake, IDestroy { public const long TIMEOUT_TIME 60 * 1000; // 60 秒无收发视为过期 public long CheckTimer; }缓存失效与回收MessageLocationSenderOneType.Awake中注册了一个每 10 秒执行一次的重复定时器MessageLocationSenderComponentSystem.cs。Check()扫描所有子MessageLocationSender若ServerNow LastSendOrRecvTime TIMEOUT_TIME即 60 秒无收发则将其回收Dispose下次发送时重新查 Location 获取最新 ActorId。这对应了 AGENTS.md 中「代理重试与 MessageLocationSender」的职责——既能自动回收因进程崩溃等异常导致的悬挂缓存又能在对象迁移后及时刷新路由。三种发送方式MessageLocationSenderOneType对外提供以下方法MessageLocationSenderComponentSystem.cs方法语义适用场景Send(entityId, IMessage)/SendAsync普通单向消息不阻塞发送队列发给不会改变位置的对象性能更高Send(entityId, ILocationMessage)单向 Location 消息走Call路径并带重试目标可能迁移、需要重定向Call(entityId, IRequest)同步 RPC 请求失败自动重试请求-响应模式目标可能迁移其内部流程按CoroutineLockType.MessageLocationSender协程锁串行化同一对象的发送若缓存的ActorId default通过LocationProxyComponent.Get查询路由并缓存普通Send直接MessageSender.SendCall走CallInner循环响应ERR_NotFoundActor→ 说明目标已迁移/销毁清空缓存ActorId default按failTimes * failTimes * 10指数退避等待后重试最多 10 次响应ERR_MessageTimeout→ 直接抛异常其他需要抛异常的 Rpc 错误 → 包装成RpcException抛出每次收发都会刷新LastSendOrRecvTime防止缓存被误回收。ILocationRequest / ILocationMessage 协议与处理器消息协议定义Location 相关协议在 ActorLocation_S_20600.proto 中定义包括五个服务端 RPCObjectAddRequest/ResponseType Key ActorId注册路由ObjectLockRequest/ResponseType Key ActorId Time → LockToken加锁ObjectUnLockRequest/ResponseType Key OldActorId NewActorId LockToken解锁并转移ObjectRemoveRequest/ResponseType Key ExpectedActorId移除路由ObjectGetRequest/ResponseType Key → ActorId查询路由。客户端测试协议定义在 ActorLocation_C_10900.protoC2M_TestRequest标记为ILocationRequestM2C_TestResponse为响应。消息处理器基类MessageLocationHandlerMessageLocationHandler.cs提供两个基类MessageLocationHandlerE, Message处理单向ILocationMessage收到后先Reply空MessageResponse再异步执行RunMessageLocationHandlerE, Request, Response处理ILocationRequest请求-响应Run中抛出的RpcException会写入response.Error其余异常统一转ERR_RpcFail最后经ProcessInnerSender.Reply返回。注释中还保留了关键设计说明Location 消息发送路径可能因需要查 Location 而进入新协程为避免 response 先于 handler 完成发出最终统一使用ActorId直接发送从而可以去掉发送队列的协程锁MessageLocationHandler.cs 中的历史注释。错误码速查本包的错误码定义在 ErrorCode.cs分为「不抛异常仅设置响应 Error」与「抛异常」两类错误码数值触发场景ERR_LocationAlreadyLocked200003001路由已被他人加锁时 Add/Remove/LockERR_LocationLockNotFound200003002解锁时锁不存在或已过期ERR_LocationLockOwnerMismatch200003003加锁/解锁时 owner 与请求者不一致ERR_LocationLockTokenMismatch200003004解锁 token 缺失或不匹配ERR_LocationPrimaryUnavailable200003005找不到主 Location 服务ERR_LocationGetRetry200003006Get 时目标正被加锁需重试ERR_LocationAlreadyExists200003007路由已存在ERR_LocationFollowerRejected200003008请求打到了备机被主服务拒绝ERR_ActorLocationSenderTimeout2/3/4100003004/5/6Sender 发送路径上的各类超时开发规则务必遵守在修改cn.etetet.actorlocation包前必须先遵守以下规则原文见 AGENTS.md先读根目录规范修改本包代码前先遵守根目录 AGENTS.md 与 Packages/cn.etetet.harness/AGENTS.md所有命令必须使用pwsh仓库的构建、测试脚本均以 PowerShell 为准await后使用EntityRefT涉及ETTask/await后继续访问 Entity 时必须使用EntityRefT重新获取防止 Entity 被回收后访问悬空引用源码中大量EntityRefLocationComponent selfRef self; ... self selfRef;正是这一规则的落地Location 锁 token 闭环调用方使用LockWithToken获取 token并调用带lockToken的UnLock不新增无 token 解锁路径旧无 token API 只保留为编译期阻断入口不手工生成.meta、不手工修改.csproj资源与工程文件统一由工具链维护。测试入口与运行方式本包自带完整的测试套件位于 Scripts/Hotfix/Test覆盖了主备缓存、持久化 CRUD、Get 锁重试、锁超时自动解锁、锁的幂等性、多 LocationType 存储、解锁补偿等场景。运行方式如下dotnet build ET.sln构建成功后运行测试Test --NameActorlocation_.* | dotnet ./Bin/ET.App.dll --SceneNameTest--NameActorlocation_.*是正则过滤只运行以Actorlocation_开头的测试用例--SceneNameTest指定进入测试场景。从测试代码可以看到典型的用法模式例如 Actorlocation_LocationProxy_And_Sender_Test.cs注册两个 mock Location 服务不同优先级→ 断言代理选中优先级更小的a_location_mock→Add写路由 →Get读回 →LockWithToken加锁 → 直接UnLock→ 再走UnLockWithRetry验证补偿路径。这为我们编写新的 Location 功能测试提供了可复制的模板。小结cn.etetet.actorlocation是 ET9 服务器框架中 Actor 消息路由的基石包LocationComponent负责路由数据的内存缓存与 DB 持久化LocationProxyComponent负责主备发现、跟随者拒绝与请求重试MessageLocationSender负责按 LocationType 缓存目标 ActorId 并提供可靠的 Location 消息投递。三者配合配合 token 闭环的分布式锁构成了一个支持对象跨进程迁移、进程崩溃自愈、主备无缝切换的完整路由体系。任何在此包之上的扩展都必须守住「持久化结构变更需迁移方案」与「锁必须 token 闭环」两条底线。【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考