
地图接口一天被刷爆 3 万次之后我把多厂商切换 配额限流做成了这样导读项目里接了高德地理编码上线一周就被线上业务刷到日配额告警后台又没做限流月底账单直接翻倍。后来我把地图通道重构成 SPI 多厂商热切换顺手把每天的调用配额也收进了 Redis。这篇就讲讲这套怎么落地。先说现象。我们的系统里有发岗定位、简历地址转坐标、POI 搜索这些场景一开始图省事代码里到处new高德的 clientkey 写死在配置里。结果某天运营搞活动简历批量补全地址一个定时任务把 geocode 接口一天调了 3 万多次高德日配额直接超限后面所有定位全部报错。排查的时候更气人——因为 key 是写死的想切到百度救急得改代码重新发版。一个第三方接口的切换成本高到要发版这本身就不对。先把通道抽象成 SPI核心思路就一条业务只依赖一个接口具体用高德还是百度后台配置说了算。先定义一个MapClientpublicinterfaceMapClient{// 厂商标识amap / baidu / tencentStringprovider();// 地址 - 坐标MapGeoResultgeocode(Stringaddress);// 坐标 - 地址MapGeoResultregeocode(doublelng,doublelat);// POI 关键词搜索ListsearchPoi(Stringkeyword,Stringcity);}高德实现就一行 HTTP 的事Hutool 的 HttpUtil 直接调ComponentRequiredArgsConstructorpublicclassAmapMapClientimplementsMapClient{privatefinalSysConfigReaderconfigReader;OverridepublicStringprovider(){returnamap;}OverridepublicMapGeoResultgeocode(Stringaddress){JSONObjectjsonget(https://restapi.amap.com/v3/geocode/geo,Map.of(address,address,key,requireKey()));assertOk(json);JSONArraygeosjson.getJSONArray(geocodes);if(geosnull||geos.isEmpty()){thrownewQklBizException(ErrorCode.MAP_API_FAILED,未解析到坐标);}JSONObjectggeos.getJSONObject(0);String[]locStrUtil.splitToArray(g.getStr(location),,);returnMapGeoResult.builder().lng(parseD(loc,0)).lat(parseD(loc,1)).province(g.getStr(province)).city(stringify(g.get(city))).adcode(g.getStr(adcode)).build();}}Spring 会把这个实现自动收进容器注册表构造时一次收集ComponentpublicclassMapClientRegistry{privatefinalMapclients;publicMapClientRegistry(Listlist){this.clientslist.stream().collect(Collectors.toMap(c-c.provider().toLowerCase(Locale.ROOT),Function.identity(),(a,b)-a));}publicMapClientrequire(Stringprovider){MapClientclientclients.get(providernull?:provider.toLowerCase(Locale.ROOT));if(clientnull){thrownewQklBizException(ErrorCode.MAP_PROVIDER_UNAVAILABLE,不支持的地图厂商: provider);}returnclient;}}以后加腾讯地图只要再写一个TencentMapClient实现零改动接进去。厂商切换走后台配置publicStringcurrentProvider(){returnconfigReader.get(map,provider,amap);}后台热切换前端无感知厂商切换做成了后台通道配置页保存即生效不用重启服务。saveChannel把开关、当前厂商、各厂商 key、各接口日配额全部写进配置中心密钥打码展示Transactional(rollbackForException.class)publicvoidsaveChannel(ChannelBodies.MapChannelSaveBodybody){configWriter.upsert(map,enabled,StringUtils.hasText(body.getEnabled())?body.getEnabled():false,地图通道开关,false,switch);configWriter.upsert(map,provider,body.getProvider(),默认地图厂商,false,text);configWriter.upsert(map,amap.key,nullToEmpty(body.getAmapKey()),高德 Key,true,text);configWriter.upsert(map,baidu.ak,nullToEmpty(body.getBaiduAk()),百度 AK,true,text);configWriter.upsert(map,tencent.key,nullToEmpty(body.getTencentKey()),腾讯地图 Key,true,text);configWriter.upsert(map,quota.geocodeDaily,normalizeQuota(body.getQuotaGeocodeDaily()),地理编码日限额,false,number);}注意这里密钥那行true是加密存储标记配置中心的密钥不能明文落库展示时打码。业务侧每次调用走activeClient()它先判断开关、再拿当前厂商、最后从注册表取实现一条链下来publicMapClientactiveClient(){if(!isEnabled()){thrownewQklBizException(ErrorCode.MAP_MODULE_DISABLED,地图通道未开启);}returnregistry.require(currentProvider());}配额限流用 Redis Hash 按天计数高德、百度这些厂商的免费配额是按每日调用次数给的光能切换还不够得知道今天用了多少、超没超。我用 Redis Hash 做了一版自建计量key 按厂商 日期隔离publicvoidconsume(Stringprovider,MapApiTypeapi){StringpnormalizeProvider(provider);longlimitdailyLimit(api);StringkeydailyKey(p);LongusedredisTemplate.opsForHash().increment(key,api.code(),1L);touchExpire(key);if(limit0used!nullusedlimit){// 超限了把刚才加的次数回滚然后抛业务异常redisTemplate.opsForHash().increment(key,api.code(),-1L);thrownewQklBizException(ErrorCode.MAP_QUOTA_EXCEEDED,api.label()今日调用额度已用完上限 limit 次);}}几个细节提一下计数和校验是两段超限要先回滚再抛错不然一次超限请求就把计数打爆后面全挂key 里带日期touchExpire给 Hash 续一天过期时间第二天自动清零不用定时任务接口维度拆开geocode / regeocode / poi / ping配额互不干扰后台可以单独配。踩坑记录切厂商切出个半吊子问题现象后台把 provider 从 amap 切到 baidu 后列表接口正常了但发岗页的地址联想一直空白。排查过程先看网关日志MapChannelService.geocode()走的是 baidu client接口 200。再看前端地址联想调的是searchPoi而这个方法在百度实现里当时还没写直接 return 了空 list。定位思路说白了就是 SPI 只统一了方法签名没统一行为契约。切厂商只切了主链路POI 这种边角接口在新厂商里是 stub业务侧又没感知。最终解决给每个厂商实现补了连通性自检ping()后台厂商视图里展示各厂商 ping 状态同时把 POI 空结果当异常抛不让静默吞掉。从此切厂商前先看 ping 面板再也不敢盲切。还有一个细节值得提配额视图。后台通道页把当日已用/限额/百分比实时展示出来运营在活动前就能看到 geocode 快超了提前切厂商或者调配额而不是等线上报错。overview()从 Redis Hash 里把当天计数读出来逐接口算剩余量for(MapApiTypeapi:MapApiType.values()){longusedparseLong(raw.get(api.code()));longlimitdailyLimit(api);longremaininglimit0?-1:Math.max(0,limit-used);intpercent0;if(limit0){anyLimitedtrue;totalLimitlimit;percent(int)Math.min(100,Math.round(used*100.0/limit));}totalUsedused;// row: api / label / used / limit / remaining / percent}这套东西上线后再没出现过月底账单翻倍这种事。厂商切换从改代码发版变成后台点一下配额从看不到变成实时可见第三方接入终于从碰运气变成了可控能力。可复用清单通道类第三方接入先抽象 SPI再谈对接切换成本才会可控Redis Hash 计数 超限回滚是轻量配额里成本最低的写法不用引限流框架key 按厂商 日期拆分配touchExpire天然按天重置每个厂商实现要有连通性自检别让 stub 接口静默返回空那是线上事故的温床。这套地图通道现在是 qkl-boot 脚手架里的标准能力短信、邮件也沿用了同一套 SPI 配额的思路。项目源码https://gitee.com/gzqkl/qkl-boot