perf(api): bound reverse geocode requests
This commit is contained in:
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"container/list"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
@@ -238,6 +239,112 @@ type mapReverseGeocodeResponse struct {
|
||||
Adcode string `json:"adcode,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
reverseGeocodeCacheTTL = time.Hour
|
||||
reverseGeocodeCacheMaxEntries = 4096
|
||||
)
|
||||
|
||||
type reverseGeocodeCacheEntry struct {
|
||||
key string
|
||||
value mapReverseGeocodeResponse
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type reverseGeocodeKeyLock struct {
|
||||
mu sync.Mutex
|
||||
refs int
|
||||
}
|
||||
|
||||
type reverseGeocodeCache struct {
|
||||
mu sync.Mutex
|
||||
entries map[string]*list.Element
|
||||
recency *list.List
|
||||
keyLocks map[string]*reverseGeocodeKeyLock
|
||||
maxEntries int
|
||||
ttl time.Duration
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func newReverseGeocodeCache(maxEntries int, ttl time.Duration) *reverseGeocodeCache {
|
||||
return &reverseGeocodeCache{
|
||||
entries: make(map[string]*list.Element),
|
||||
recency: list.New(),
|
||||
keyLocks: make(map[string]*reverseGeocodeKeyLock),
|
||||
maxEntries: maxEntries,
|
||||
ttl: ttl,
|
||||
now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
func reverseGeocodeCacheKey(longitude, latitude float64) string {
|
||||
return fmt.Sprintf("%.5f,%.5f", longitude, latitude)
|
||||
}
|
||||
|
||||
func (cache *reverseGeocodeCache) get(key string) (mapReverseGeocodeResponse, bool) {
|
||||
cache.mu.Lock()
|
||||
defer cache.mu.Unlock()
|
||||
element, ok := cache.entries[key]
|
||||
if !ok {
|
||||
return mapReverseGeocodeResponse{}, false
|
||||
}
|
||||
entry := element.Value.(*reverseGeocodeCacheEntry)
|
||||
if !entry.expiresAt.After(cache.now()) {
|
||||
cache.remove(element)
|
||||
return mapReverseGeocodeResponse{}, false
|
||||
}
|
||||
cache.recency.MoveToFront(element)
|
||||
return entry.value, true
|
||||
}
|
||||
|
||||
func (cache *reverseGeocodeCache) put(key string, value mapReverseGeocodeResponse) {
|
||||
if cache.maxEntries <= 0 || cache.ttl <= 0 {
|
||||
return
|
||||
}
|
||||
cache.mu.Lock()
|
||||
defer cache.mu.Unlock()
|
||||
if element, ok := cache.entries[key]; ok {
|
||||
entry := element.Value.(*reverseGeocodeCacheEntry)
|
||||
entry.value = value
|
||||
entry.expiresAt = cache.now().Add(cache.ttl)
|
||||
cache.recency.MoveToFront(element)
|
||||
return
|
||||
}
|
||||
for len(cache.entries) >= cache.maxEntries {
|
||||
cache.remove(cache.recency.Back())
|
||||
}
|
||||
entry := &reverseGeocodeCacheEntry{key: key, value: value, expiresAt: cache.now().Add(cache.ttl)}
|
||||
cache.entries[key] = cache.recency.PushFront(entry)
|
||||
}
|
||||
|
||||
func (cache *reverseGeocodeCache) remove(element *list.Element) {
|
||||
if element == nil {
|
||||
return
|
||||
}
|
||||
delete(cache.entries, element.Value.(*reverseGeocodeCacheEntry).key)
|
||||
cache.recency.Remove(element)
|
||||
}
|
||||
|
||||
func (cache *reverseGeocodeCache) lockKey(key string) func() {
|
||||
cache.mu.Lock()
|
||||
keyLock := cache.keyLocks[key]
|
||||
if keyLock == nil {
|
||||
keyLock = &reverseGeocodeKeyLock{}
|
||||
cache.keyLocks[key] = keyLock
|
||||
}
|
||||
keyLock.refs++
|
||||
cache.mu.Unlock()
|
||||
keyLock.mu.Lock()
|
||||
return func() {
|
||||
keyLock.mu.Unlock()
|
||||
cache.mu.Lock()
|
||||
keyLock.refs--
|
||||
if keyLock.refs == 0 {
|
||||
delete(cache.keyLocks, key)
|
||||
}
|
||||
cache.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func withAMapReverseGeocodeAPI(next http.Handler, cfg config.Config, upstream string, client *http.Client) http.Handler {
|
||||
apiKey := strings.TrimSpace(cfg.AMapAPIKey)
|
||||
if client == nil {
|
||||
@@ -247,6 +354,7 @@ func withAMapReverseGeocodeAPI(next http.Handler, cfg config.Config, upstream st
|
||||
if upstream == "" {
|
||||
upstream = "https://restapi.amap.com"
|
||||
}
|
||||
cache := newReverseGeocodeCache(reverseGeocodeCacheMaxEntries, reverseGeocodeCacheTTL)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/map/reverse-geocode" {
|
||||
next.ServeHTTP(w, r)
|
||||
@@ -265,6 +373,17 @@ func withAMapReverseGeocodeAPI(next http.Handler, cfg config.Config, upstream st
|
||||
httpx.WriteError(w, http.StatusBadRequest, "BAD_COORDINATE", "经纬度参数无效", err.Error(), requestTraceID(r))
|
||||
return
|
||||
}
|
||||
cacheKey := reverseGeocodeCacheKey(longitude, latitude)
|
||||
unlock := cache.lockKey(cacheKey)
|
||||
defer unlock()
|
||||
if cached, ok := cache.get(cacheKey); ok {
|
||||
cached.Longitude = longitude
|
||||
cached.Latitude = latitude
|
||||
w.Header().Set("X-Reverse-Geocode-Cache", "HIT")
|
||||
httpx.WriteOK(w, requestTraceID(r), cached)
|
||||
return
|
||||
}
|
||||
w.Header().Set("X-Reverse-Geocode-Cache", "MISS")
|
||||
target, err := url.Parse(upstream + "/v3/geocode/regeo")
|
||||
if err != nil {
|
||||
httpx.WriteError(w, http.StatusBadGateway, "AMAP_REVERSE_GEOCODE_URL_INVALID", "高德逆地理编码地址无效", err.Error(), requestTraceID(r))
|
||||
@@ -331,6 +450,7 @@ func withAMapReverseGeocodeAPI(next http.Handler, cfg config.Config, upstream st
|
||||
Township: amapJSONText(body.Regeocode.AddressComponent.Township),
|
||||
Adcode: amapJSONText(body.Regeocode.AddressComponent.Adcode),
|
||||
}
|
||||
cache.put(cacheKey, result)
|
||||
httpx.WriteOK(w, requestTraceID(r), result)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user