为了省地图 API 费用,我们把缓存做到极致,最后还是重构了整个位置服务
一个月后,我们发现 Redis 比地图服务还忙
去年做本地生活项目的时候。
有段时间地图服务账单涨得特别快。
第一反应当然不是换服务商。
而是优化。
毕竟程序员的本能就是:
能靠技术解决的问题,不要靠采购解决。
于是我们开始疯狂加缓存。
第一版缓存
最简单。
逆地址解析结果直接缓存。
const key = `geo:${lat}:${lng}` const cache = await redis.get(key) if (cache) { return JSON.parse(cache) } const data = await reverseGeocode(lat, lng) await redis.set( key, JSON.stringify(data), 'EX', 86400 ) return data上线第一天效果不错。
调用量明显下降。
大家都觉得找到了正确方向。
很快发现缓存命中率低得离谱
原因很简单。
GPS 坐标不是整数。
骑手位置:
30.658231 104.072891下一秒:
30.658236 104.072895肉眼看几乎没区别。
系统看来却是两个 Key。
缓存根本命不中。
第二版缓存
坐标网格化。
保留四位小数。
const lat = Number(rawLat.toFixed(4)) const lng = Number(rawLng.toFixed(4))效果确实提升不少。
命中率从个位数来到 40% 左右。
但还是不够。
第三个问题出现了
POI 搜索。
用户搜索:
奶茶下一位用户搜索:
奶茶店再下一位:
附近奶茶结果其实差不多。
但缓存看来完全不同。
为了缓存,我们写了很多奇怪逻辑
同义词归一化。
关键词预处理。
区域缓存。
热门词缓存。
甚至搞了搜索结果预热。
代码越来越复杂。
某天我看监控发现
Redis CPU:
75%地图接口调用:
下降30%但账单:
下降不到20%那一刻突然意识到:
我们可能在优化错误的问题。
真正的问题
后来把调用链全部梳理了一遍。
发现大量请求根本不应该发生。
例如:
用户打开订单页。
调一次逆解析。
进入详情页。
再调一次。
刷新页面。
再调一次。
客服后台打开订单。
又调一次。
同一组坐标。
一天能被解析十几次。
调整思路
不再研究怎么缓存。
而是研究怎么减少调用。
例如:
位置上报时直接完成解析。
await save({ lat, lng, address })查询直接读库。
SELECT address FROM order_location整个链路简单很多。
顺手重新评估地图能力
既然都在改架构。
干脆把地图服务也重新看了一遍。
我们需要的其实很少:
- POI 搜索
- 正逆地址解析
- 坐标转换
并不需要全家桶。
于是开始测试不同方案。
一个很现实的问题
很多团队选地图服务时只看:
功能多不多实际上应该看:
我真正用了多少很多接口一年都调不了几次。
真正花钱的永远是那几个高频接口。
后来的架构
客户端 ↓ 位置服务层 ↓ POI 逆解析 坐标转换 ↓ LTS统一封装。
业务侧不再直接调用第三方接口。
以后换服务商只改一层。
最终结果
这次优化没有什么黑科技。
做的事情很朴素:
- 删除重复调用
- 调整数据流向
- 重构位置服务
- 重新评估供应商
最终:
- 调用量下降约 70%
- Redis 压力下降
- 地图成本明显降低
- 不再需要研究各种缓存骚操作
写在最后
很多时候技术人容易陷入一个误区:
出现成本问题,就开始研究缓存。
出现性能问题,就开始研究并发。
出现账单问题,就开始研究限流。
但真正应该问的是:
这个请求真的有必要发生吗?
当年我们花了很长时间研究缓存策略。
最后发现最值钱的一行代码其实是:
删除一次不必要的调用如果你也在做位置服务、地图服务或者 POI 检索系统,可以先看看调用链,而不是先看 Redis。
