第4章 令牌化系统架构设计
说实话,很多人一上来就写代码实现令牌化,结果系统上线不久就崩了——为什么?因为架构没想清楚。我踩过这个坑,所以这一章咱们把架构设计讲透。
4.1 整体架构:三层两总线
令牌化系统不是单点服务,它得扛住高并发、保证数据一致性,还要能审计追查。我个人习惯把它拆成三层:接入层、核心服务层、存储层。层与层之间通过两条总线通信——一条是同步的API总线,一条是异步的事件总线。
| 层次 | 职责 | 常用技术栈 |
|---|---|---|
| 接入层 | 负载均衡、限流、鉴权、请求路由 | Nginx / API Gateway |
| 核心服务层 | 令牌生成、验证、撤销、策略执行、仲裁 | Java / Go + gRPC |
| 存储层 | 持久化令牌、策略规则、审计日志 | Redis + MySQL / TiDB |
为什么存储层要混用?因为令牌查询需要极低的延迟,Redis能扛;但审计和回溯必须可靠持久化,所以MySQL也得上。这里有个坑——千万别用Redis做主库,否则宕机丢数据你会哭的。我在早期的项目里就因为这个被DBA骂过。
4.2 模块划分:拆而不散
核心服务层我习惯拆成5个模块,每个模块只做一件事:
- 令牌生成器 — 生成唯一且不可预测的令牌。我用的是UUID v4 + 时间戳哈希,但生产环境中建议用Snowflake变体,避免碰撞。
- 令牌存储 — 负责读写缓存与数据库,写时双写(先写Redis再异步写MySQL),读时缓存穿透保护。
- 策略引擎 — 根据租户定义规则(如有效期、请求来源、次数限制)判断令牌是否可用。这玩意儿容易变成“上帝模块”,记得把规则表达式抽成独立DSL。
- 仲裁器 — 多副本场景下保证令牌状态的最终一致性。我们用的是Raft变体,但如果你业务没那么强一致性,用乐观锁+版本号也行。
- 审计追踪 — 记录每一次令牌操作。别小看它,合规审查全靠它撑场子。
我的建议:每个模块独立部署,用事件总线解耦。令牌生成器只负责生成,不关心后续存储是否成功——即使Redis挂了,事件还能补偿写入数据库。这招我在一个金融项目里用过,线上跑了两年没出过大乱子。
4.3 数据流:三条主链路
系统里最核心的数据流就三条,你得记住它们:
- 令牌创建流
客户端 → 接入层(验证签名)→ 令牌生成器(生成令牌)→ 令牌存储(写入Redis+异步写DB)→ 仲裁器(广播通知其他副本)→ 返回给客户端。 - 令牌验证流
客户端 → 接入层(解析令牌)→ 策略引擎(检查有效期、黑白名单、频次)→ 令牌存储(读取缓存)→ 返回验证结果。如果缓存miss,需要回源到DB并缓存回填,但这里要小心“惊群效应”——我遇到过瞬间1000个请求把DB打崩的事,后来加了分布式锁+本地缓存才解决。 - 令牌撤销流
客户端/管理后台 → 接入层 → 令牌存储(标记失效)→ 仲裁器(同步到所有副本)→ 审计追踪(记录撤销原因)。
注意:撤销流的时效性很敏感。如果你的系统要求毫秒级失效,那必须走Redis的Key过期通知+拉模式双保险。别问我怎么知道的——我曾经因为只用了Redis过期通知,结果网络抖动导致漏通知,令牌被重复使用了三分钟。
4.4 仲裁逻辑:一致性博弈
单节点当然没有仲裁问题,但生产环境一定是多副本的。仲裁器解决的问题就是:当两个副本对同一个令牌的状态产生分歧时,听谁的?
我们用了“时间戳+版本号”的Lamport时钟变体。具体逻辑:
- 每个令牌对象维护
version和last_updated_at。 - 写操作必须带版本号,仲裁器比较当前版本号与请求携带的版本号,只有
req_version == cur_version时才允许写入,否则返回冲突。 - 冲突发生时,仲裁器调用“版本合并策略”:默认以最近时间戳为准,但允许业务方自定义策略(如“拒绝所有冲突”、“回退到上一版本”、“人工介入”)。
其实说白了,仲裁就是“乐观锁 + 重试机制”。你想想看,如果业务允许短暂不一致,用ZooKeeper做分布式协调反而更重。我现在的团队用的是Redis RedLock + 本地版本缓存,成本低且够用。
下面是这一章的核心架构图——别嫌丑,能帮你建立整体感:
你看,架构图里核心服务层的五个模块互相协作,但又不直接依赖。我最喜欢的设计是那条“事件总线”——有了它,即使某个模块挂了,其他模块还能继续工作,最多牺牲一点实时性。
对了,还有一件事:如果你想深入了解仲裁器的具体实现,可以去看Raft或ZooKeeper的ZAB协议。但说实话,90%的业务场景用乐观锁+补偿就够了。我一向信奉“能简单就别复杂”。