身份服务
为多租户平台提供账号、会话与多级合作伙伴层级
项目背景
平台上的其他所有服务都依赖身份信息:用户是谁、属于哪个租户、在最深可达十级的合作伙伴层级中处于什么位置。它必须能快速回答“X 的所有下级”,保证会话可吊销,并在账号创建跨越多个服务时保持一致。
架构设计
网关 → 身份服务(go-zero gRPC)→ 通过由可组合 GORM scope 构成的 db/ 层访问 MySQL;Redis 位于 cache/ 层之后,由 cacheSync/ 层编排读穿透与异步回写。账号变更连同变更前后快照和 trace 头一起发布到 Kafka,供 CRM 读模型和客户分群服务跟进。
核心技术亮点
基于闭包表(祖先、后代、深度)实现 10 级合作伙伴网络层级,在创建账号的同一事务中写入
三层数据访问:MySQL → Redis → 异步缓存同步,配合扇出 worker、singleflight 和 TTL 随机抖动
使用 Redis 布隆过滤器做账号去重;JWT 会话支持滑动续期与服务端吊销
拆分为独立的 gRPC 微服务(拆分时共 92 个 RPC);此后又新增约 20 个 RPC,包括基于闭包表的层级查询
跨服务账号创建的补偿回滚;只有在所有步骤成功后才发布注册事件
缓存层加固:带队列满指标的有界异步 worker 池、会话吊销的同步兜底,以及缓存未命中时的批量加载
核心实现
基于闭包表的合作伙伴层级
存储每一对祖先–后代关系及其深度,与创建账号在同一事务中写入,并在此基础上构建子树、下级成员和按租户的目录查询。
Why: 子树查询(“该合作伙伴下的所有人”)是最常见的读操作。闭包表用一次索引查找即可回答,无需递归查询;深度字段也让 10 级上限易于强制执行。
带异步同步的三层缓存
缓存未命中时,读请求经 singleflight 访问数据库。缓存回写与失效在带 TTL 随机抖动的有界异步 worker 池中执行。2025 年我对其进行了加固:修正未命中处理逻辑,将 singleflight 限定在数据库读取环节,增加单次调用超时和队列满计数器,并在 worker 池饱和时让会话吊销降级为同步执行。
Why: 身份服务位于每条请求链路上。合并并发未命中、分散过期时间、绝不丢失任何一次吊销,在这里比其他任何地方都更重要。
Result: 逐用户的缓存未命中查询被替换为一次批量查询加一次 pipeline 写入。
跨服务账号创建的一致性
创建账号时还会在其他服务中创建钱包和设置。每个流程都拆分为校验、准备和初始化步骤;若后续步骤失败,则在独立且有超时保护的 context 上执行补偿删除。
Why: 否则,下游失败会留下创建了一半的用户,下游消费者还可能收到一个已不存在账号的注册事件。
技术决策
| 技术决策 | 选择 | 替代方案 | 原因 |
|---|---|---|---|
| 层级存储 | 闭包表 | 邻接表 + 递归查询 | 读取次数远多于层级变更,且几乎每次读取都是子树查询。插入时多写一些行,换来子树查询只需一次查找。 |
| 会话吊销 | 在 JWT 之外,于 Redis 中为每个用户保存一个有效 token | 纯无状态 JWT | 管理员必须能够立即禁用账号并强制下线;无状态 token 在过期前无法吊销。 |