项目概述
本项目实现带缓存与限流的地址解析代理:把地址查询转发到付费提供商,缓存命中与未命中,并按调用方配额限速,使单个客户无法耗尽共享套餐。负结果缓存是常被忽略但往往更有价值的一半:重复的未命中与重复的命中一样昂贵。
架构与关键决策
查询先规范化成规范键,再查缓存,未命中时在全局令牌桶下调用提供商;结果按命中与否设置不同 TTL,负结果用更短的生命周期,因为今天不存在的地址明天可能存在。全局桶限制总花费,按调用方计数限制每个客户,两者回答不同问题且都需要。提供商响应在边界处映射为稳定内部形状,使提供商更换不波及调用方。
实施与测试
同一地址调用两次断言只触发一次提供商调用;未知地址调用两次断言负缓存生效;超出某调用方配额断言 429 而其他调用方不受影响;并发下断言全局桶真正限速。
限制与后续
- 缓存键为规范化文本,两个同名不同地会共享条目。
- 无到第二提供商的自动故障转移。
- 配额为固定窗口,边界处仍可能突发。
代码示例
pnpm init
pnpm add ioredis undici express
pnpm add -D typescript tsx vitest
geocoding-proxy/
├── src/normalize.ts
├── src/provider.ts
├── src/cache.ts
├── src/quota.ts
├── tests/geocode.test.ts
└── package.json
const key = 'geo:' + sha1(normalize(address));
await redis.set(key, JSON.stringify(result ?? null), 'EX', result ? 86400 : 600);
if (!globalBucket.take()) return res.status(429).end();
const used = await redis.incr('quota:' + apiKey + ':' + day);
return { lat: r.geometry.lat, lng: r.geometry.lng, label: r.formatted };
pnpm vitest run