Mục lục
- Mục Tiêu Bài Học
- Bài Toán: Tìm Tài Xế / Cửa Hàng Gần Nhất
- GEO Là Gì — Internal Geohash 52-bit
- GEOADD — Thêm & Cập Nhật Tọa Độ
- GEOPOS & GEODIST — Lấy Tọa Độ & Khoảng Cách
- GEOSEARCH (Redis 6.2+) — Thay Thế GEORADIUS
- GEOSEARCHSTORE — Lưu Kết Quả Vào ZSet Mới
- Use Case 1: Ride-Hailing — Tìm Tài Xế Gần
- Use Case 2: Store Locator
- Use Case 3: Geofencing
- Stale Location & Cleanup Pattern
- Performance & Big-O
- Anti-patterns & Best Practices
- Tổng Kết & Quiz
Mục Tiêu Bài Học
- Hiểu GEO là gì trong Redis và tại sao nó thực chất là Sorted Set với score = geohash 52-bit.
- Nắm thứ tự tham số đúng: longitude trước, latitude sau — ngược chiều với convention địa lý thông thường.
- Dùng được GEOADD, GEOPOS, GEODIST, GEOSEARCH, GEOSEARCHSTORE với các option đầy đủ.
- Biết GEOSEARCH (Redis 6.2+) thay thế GEORADIUS / GEORADIUSBYMEMBER đã deprecated.
- Áp dụng GEO vào ba use case thực chiến: ride-hailing, store locator, geofencing.
- Nhận diện vấn đề stale location và chọn đúng cleanup pattern.
- Phân biệt khi nào GEO phù hợp và khi nào cần routing engine (OSRM, Google Maps API).
Bài Toán: Tìm Tài Xế / Cửa Hàng Gần Nhất
Một số bài toán location phổ biến trong backend:
- Ride-hailing: user đặt xe, server cần tìm 10 tài xế đang online gần nhất trong vòng 3km, sắp theo khoảng cách.
- Store locator: user mở app food delivery hoặc bản đồ, server liệt kê cửa hàng trong bán kính 2km kèm khoảng cách.
- Social nearby: hiển thị bạn bè hoặc người dùng đang ở gần vị trí hiện tại.
- Geofencing: gửi notification khi user bước vào hoặc ra khỏi một vùng địa lý cụ thể.
Với SQL thuần, query kiểu "tìm theo bán kính" cần spatial index (PostGIS trên PostgreSQL, hoặc MySQL spatial). Mỗi query phải tính khoảng cách theo công thức Haversine rồi lọc. Đây không phải là không làm được — PostGIS cho phép query phức tạp và join với các bảng khác rất tốt. Nhưng khi số điểm thay đổi liên tục (tài xế cập nhật vị trí mỗi vài giây), ghi vào SQL có overhead transaction; khi số query "tìm gần" lớn (hàng nghìn request/s), latency phụ thuộc vào disk IO.
Redis GEO giải quyết trường hợp này bằng cách lưu tọa độ trong memory, xử lý tìm kiếm theo bán kính bằng geohash trực tiếp trên cấu trúc Sorted Set đã có sẵn. Không cần extension, không cần spatial index riêng. Đánh đổi: không join được với dữ liệu khác, không routing đường thực, không persistence chắc chắn như RDBMS.
GEO Là Gì — Internal Geohash 52-bit
Redis GEO không phải một data type mới. Về mặt kỹ thuật, khi bạn GEOADD, Redis tạo hoặc cập nhật một Sorted Set bình thường — với mỗi member là tên điểm (vd "driver:1"), và score là một số nguyên 52-bit được tính từ tọa độ.
Cách tính score: Redis dùng thuật toán geohash nhưng theo phương pháp interleaved (xen kẽ bit kinh độ và bit vĩ độ). Cụ thể:
- Kinh độ (longitude) trong khoảng [-180, 180] được chia nhị phân 26 lần → 26 bit.
- Vĩ độ (latitude) trong khoảng [-85.05, 85.05] được chia nhị phân 26 lần → 26 bit.
- Hai chuỗi bit này được xen kẽ với nhau → 52 bit tổng.
Kết quả 52-bit này được lưu làm score trong ZSet. Vì geohash có đặc tính locality (các điểm gần nhau trong không gian địa lý cũng có score gần nhau trên trục số), Redis có thể tìm kiếm các điểm trong một vùng bằng cách tính range score và dùng ZRANGEBYSCORE — tức là tận dụng lại hoàn toàn tập lệnh ZSet đã có.
Precision của geohash 52-bit: sai số tối đa khoảng 0.6m so với tọa độ thực. Đủ cho hầu hết ứng dụng ride-hailing và store locator. Bạn có thể xác minh bằng lệnh OBJECT ENCODING <geo_key> — Redis trả về listpack hoặc skiplist, đúng như ZSet thông thường.
Hệ quả thực tế: mọi lệnh ZSet đều chạy được trên GEO key. Ví dụ ZCARD drivers cho biết số tài xế đang trong GEO key, ZREM drivers "driver:1" xóa một tài xế khỏi GEO. Điều này vừa hữu ích vừa là bẫy nếu bạn vô tình dùng lệnh ZSet mà nhầm tưởng score có nghĩa địa lý.
GEOADD — Thêm & Cập Nhật Tọa Độ
Cú pháp GEOADD (Redis CLI):
GEOADD key [NX | XX] [CH] longitude latitude member [longitude latitude member ...]
Thứ tự tham số: longitude (kinh độ) TRƯỚC, latitude (vĩ độ) SAU. Đây là điểm dễ nhầm nhất — ngược với cách đọc thông thường "lat, lon" trong địa lý và Google Maps. Redis theo chuẩn GeoJSON (RFC 7946) đặt longitude trước.
# Thêm điểm tại TP.HCM (Bến Thành): lon=106.7009, lat=10.7769
GEOADD stores 106.7009 10.7769 "store:saigon-center"
# Thêm điểm tại Hà Nội (Hồ Gươm): lon=105.8542, lat=21.0285
GEOADD stores 105.8542 21.0285 "store:hanoi"
# Thêm nhiều điểm cùng lúc (batch)
GEOADD drivers
106.7009 10.7769 "driver:1"
106.7050 10.7800 "driver:2"
106.6900 10.7700 "driver:3"
Nếu member đã tồn tại, GEOADD cập nhật tọa độ (tức cập nhật score ZSet) và trả về 0 (không tăng count). Đây là cách tài xế cập nhật vị trí liên tục — gọi GEOADD lại cùng member name với tọa độ mới.
Options:
NX: chỉ thêm member mới, không update member đã có.XX: chỉ update member đã có, không thêm mới.CH: thay đổi return value từ "số member thêm mới" sang "số member bị thêm hoặc update".
Giới hạn tọa độ: latitude phải trong khoảng [-85.05112878, 85.05112878] (không phải ±90°). Đây là giới hạn của phép chiếu Mercator mà geohash sử dụng — vùng cực bị cắt bỏ. Với mọi ứng dụng thực tế ở Việt Nam và phần lớn thế giới, giới hạn này không ảnh hưởng gì.
GEOPOS & GEODIST — Lấy Tọa Độ & Khoảng Cách
GEOPOS
Trả về tọa độ (longitude, latitude) của một hoặc nhiều member:
GEOPOS stores "store:saigon-center"
# → [[106.70089721679687500, 10.77690006200868]]
GEOPOS stores "store:saigon-center" "store:hanoi" "nonexistent"
# → [[106.70089...], [105.85419...], [nil]]
Lưu ý: tọa độ trả về không khớp chính xác 100% với tọa độ đã nhập vì quá trình encode sang geohash 52-bit và decode ngược lại gây sai số ~0.6m. Đây là hành vi đã được tài liệu hoá và chấp nhận được với độ chính xác thực tế.
GEODIST
Tính khoảng cách giữa hai member trong cùng một GEO key:
GEODIST stores "store:saigon-center" "store:hanoi" km
# → "1726.8899" (km)
GEODIST drivers "driver:1" "driver:2" m
# → "567.3412" (mét)
Unit hợp lệ: m (mét, mặc định), km, mi (dặm Anh), ft (feet). Nếu một trong hai member không tồn tại, trả về nil.
GEODIST tính khoảng cách đường chim bay (straight-line, công thức WGS84). Không phải khoảng cách đường thực — nếu cần routing đường bộ, phải dùng OSRM hoặc Google Maps Distance Matrix API.
GEOSEARCH (Redis 6.2+) — Thay Thế GEORADIUS
GEORADIUS và GEORADIUSBYMEMBER đã bị deprecated từ Redis 6.2 và bị xoá khỏi Redis 7.0+ (chỉ còn là alias). Lệnh thay thế là GEOSEARCH.
Cú pháp đầy đủ:
GEOSEARCH key
FROMMEMBER member | FROMLONLAT longitude latitude
BYRADIUS radius m|km|mi|ft | BYBOX width height m|km|mi|ft
ASC | DESC
[COUNT count [ANY]]
[WITHCOORD] [WITHDIST] [WITHHASH]
Tìm kiếm theo bán kính từ tọa độ cố định:
# Tìm tài xế trong bán kính 3km tính từ vị trí user
# lon=106.70, lat=10.77 — sắp tăng dần theo khoảng cách, lấy tối đa 10
GEOSEARCH drivers
FROMLONLAT 106.70 10.77
BYRADIUS 3 km
ASC COUNT 10
WITHCOORD WITHDIST
Kết quả (mỗi entry gồm member + distance + coordinates nếu có WITHCOORD/WITHDIST):
1) 1) "driver:2"
2) "0.4231" -- km
3) 1) "106.70499938726425171"
2) "10.77999842911413"
2) 1) "driver:1"
2) "0.8812"
3) ...
Tìm kiếm theo hình chữ nhật (box):
# Hình chữ nhật 10km x 10km tại tâm
GEOSEARCH drivers
FROMLONLAT 106.70 10.77
BYBOX 10 10 km
ASC
Tìm kiếm từ một member đã có trong key:
# Tìm tài xế trong bán kính 2km tính từ vị trí driver:1
GEOSEARCH drivers FROMMEMBER "driver:1" BYRADIUS 2 km ASC
Options chi tiết:
WITHCOORD: trả về tọa độ (longitude, latitude) của mỗi kết quả.WITHDIST: trả về khoảng cách từ điểm tìm đến mỗi kết quả, theo unit đã chỉ định.WITHHASH: trả về geohash score thô (52-bit integer) — ít dùng trong production.COUNT N: giới hạn số kết quả. Luôn nên đặt COUNT để tránh trả về triệu kết quả khi bán kính lớn.COUNT N ANY: dừng sớm khi tìm đủ N kết quả, không đảm bảo N gần nhất nhưng nhanh hơn.ASC/DESC: sắp theo khoảng cách tăng / giảm dần.
GEOSEARCHSTORE — Lưu Kết Quả Vào ZSet Mới
GEOSEARCHSTORE có cú pháp giống GEOSEARCH nhưng lưu kết quả vào một ZSet mới thay vì trả về trực tiếp:
GEOSEARCHSTORE destination source
FROMLONLAT longitude latitude
BYRADIUS radius km
ASC COUNT 20
STOREDIST
Với STOREDIST: score của ZSet đích là khoảng cách (float), không phải geohash. Điều này cho phép sau đó dùng ZRANGE, ZSCORE, ZRANK lên key destination để lấy thêm thông tin hoặc join logic.
Không có STOREDIST: score vẫn là geohash 52-bit, key đích vẫn dùng được như GEO key bình thường.
Dùng GEOSEARCHSTORE khi muốn tính toán danh sách tài xế gần nhất một lần, lưu lại và phục vụ nhiều request đọc tiếp theo (cache kết quả geo search), thay vì tính lại mỗi lần.
Use Case 1: Ride-Hailing — Tìm Tài Xế Gần
Luồng điển hình trong ứng dụng ride-hailing:
- Tài xế cập nhật vị trí GPS mỗi 5 giây.
- User đặt xe → server tìm 10 tài xế gần nhất trong bán kính 3km.
- Trả danh sách tài xế kèm khoảng cách để hiển thị trên map.
import redis
from datetime import datetime
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
GEO_KEY = "drivers:active"
def update_driver_location(driver_id: str, lon: float, lat: float) -> None:
"""Tài xế cập nhật vị trí — gọi mỗi vài giây."""
# GEOADD tự update nếu driver_id đã có
r.geoadd(GEO_KEY, (lon, lat, driver_id))
# Ghi timestamp riêng để phát hiện tài xế offline
r.hset("drivers:last_seen", driver_id, int(datetime.now().timestamp()))
def find_nearby_drivers(
user_lon: float,
user_lat: float,
radius_km: float = 3.0,
count: int = 10,
) -> list[dict]:
"""Tìm tài xế trong bán kính, sắp theo khoảng cách."""
results = r.geosearch(
GEO_KEY,
longitude=user_lon,
latitude=user_lat,
radius=radius_km,
unit="km",
sort="ASC",
count=count,
withdist=True,
withcoord=True,
)
# results: list of (member, dist, (lon, lat))
drivers = []
for member, dist, (lon, lat) in results:
drivers.append({
"driver_id": member,
"distance_km": round(dist, 3),
"lon": lon,
"lat": lat,
})
return drivers
# Demo
update_driver_location("driver:1", 106.7009, 10.7769)
update_driver_location("driver:2", 106.7050, 10.7800)
update_driver_location("driver:3", 106.6900, 10.7700)
nearby = find_nearby_drivers(user_lon=106.70, user_lat=10.77, radius_km=3.0)
for d in nearby:
print(f"{d['driver_id']}: {d['distance_km']} km")
# driver:2: 0.541 km
# driver:1: 0.881 km
# driver:3: 1.124 km
Điểm quan trọng: geosearch trong redis-py nhận longitude, latitude theo keyword argument (không phải positional) — thứ tự không nhầm được. Nhưng khi dùng raw RESP hoặc CLI, luôn là longitude trước.
Use Case 2: Store Locator
Store locator khác ride-hailing ở chỗ: danh sách cửa hàng tương đối tĩnh (chỉ thay đổi khi mở/đóng cửa hàng mới), không liên tục cập nhật vị trí.
STORES_KEY = "stores"
# Nạp dữ liệu lần đầu (hoặc khi có cửa hàng mới)
def seed_stores(stores: list[dict]) -> None:
"""
stores: [{"id": "store:001", "lon": 106.70, "lat": 10.77}, ...]
"""
pipeline = r.pipeline()
for s in stores:
pipeline.geoadd(STORES_KEY, (s["lon"], s["lat"], s["id"]))
pipeline.execute()
def find_stores_nearby(
user_lon: float,
user_lat: float,
radius_km: float = 5.0,
) -> list[dict]:
results = r.geosearch(
STORES_KEY,
longitude=user_lon,
latitude=user_lat,
radius=radius_km,
unit="km",
sort="ASC",
count=20,
withdist=True,
)
return [
{"store_id": member, "distance_km": round(dist, 2)}
for member, dist in results
]
Với store locator, cần lưu ý: GEO key chỉ giữ tọa độ và member name. Các thông tin khác (tên cửa hàng, giờ mở cửa, số điện thoại) cần lưu riêng ở Hash key tương ứng, ví dụ store:001:info. Sau khi GEOSEARCH có danh sách member, dùng pipeline để lấy thêm thông tin từ các Hash đó:
def find_stores_with_info(user_lon: float, user_lat: float) -> list[dict]:
nearby = find_stores_nearby(user_lon, user_lat)
if not nearby:
return []
pipeline = r.pipeline()
for s in nearby:
pipeline.hgetall(f"{s['store_id']}:info")
infos = pipeline.execute()
result = []
for s, info in zip(nearby, infos):
result.append({**s, **info})
return result
Use Case 3: Geofencing
Geofencing là kiểm tra user có trong một vùng địa lý hay không, và phát notification khi trạng thái thay đổi (vào vùng hoặc ra vùng).
Cách đơn giản nhất với Redis GEO: mỗi zone được đại diện bởi một điểm trung tâm và bán kính. Kiểm tra user có trong zone hay không bằng GEOSEARCH từ tâm zone:
ZONES_KEY = "geofences"
def add_zone(zone_id: str, center_lon: float, center_lat: float) -> None:
"""Thêm zone (lưu tâm zone vào GEO key)."""
r.geoadd(ZONES_KEY, (center_lon, center_lat, zone_id))
def is_user_in_zone(
zone_id: str,
user_lon: float,
user_lat: float,
radius_km: float,
) -> bool:
"""Kiểm tra user có trong zone không."""
# GEOSEARCH từ tâm zone, tìm xem vị trí user có trong bán kính không.
# Trick: thêm user vào một GEO key tạm thời (hoặc dùng FROMLONLAT).
# Cách đơn giản hơn: tính GEODIST giữa tâm zone và vị trí user.
# Tạm thêm user vào key riêng để dùng FROMMEMBER
temp_key = "geo:temp:user_check"
r.geoadd(temp_key, (user_lon, user_lat, "user"))
r.expire(temp_key, 10) # cleanup sau 10s
# GEOSEARCH từ vị trí user, tìm zone_id trong bán kính
results = r.geosearch(
ZONES_KEY,
longitude=user_lon,
latitude=user_lat,
radius=radius_km,
unit="km",
count=1,
)
# Nếu zone_id nằm trong kết quả, user đang trong zone
return zone_id in results
def check_and_notify(zone_id: str, user_id: str, user_lon: float, user_lat: float, radius_km: float) -> str | None:
"""
Kiểm tra trạng thái vào/ra zone. Trả về event nếu trạng thái thay đổi.
Trạng thái được lưu trong Redis để so sánh giữa các lần check.
"""
state_key = f"geofence:state:{zone_id}:{user_id}"
was_inside = r.get(state_key) == "in"
is_inside = is_user_in_zone(zone_id, user_lon, user_lat, radius_km)
if is_inside and not was_inside:
r.set(state_key, "in", ex=3600)
return "enter"
elif not is_inside and was_inside:
r.set(state_key, "out", ex=3600)
return "exit"
elif is_inside:
r.set(state_key, "in", ex=3600)
return None
Với geofence phức tạp hơn (vùng đa giác, không phải hình tròn), GEO Redis không xử lý được trực tiếp. Cần logic ray-casting ở application layer hoặc dùng PostGIS.
Stale Location & Cleanup Pattern
Vấn đề cốt lõi: Redis GEO không có per-member TTL. Khi tài xế offline (tắt app, mất kết nối), vị trí của họ vẫn còn trong GEO key. Lần GEOSEARCH tiếp theo sẽ trả về vị trí cũ của tài xế đó — đây là stale location.
Có ba pattern xử lý phổ biến:
Pattern 1: Timestamp track + filter khi đọc
Lưu thêm timestamp cập nhật cuối của mỗi tài xế trong một Hash riêng. Khi GEOSEARCH trả về kết quả, application lọc bỏ tài xế có timestamp quá cũ:
LAST_SEEN_KEY = "drivers:last_seen"
STALE_THRESHOLD_SECONDS = 30 # tài xế không update > 30s coi là offline
def update_driver_location(driver_id: str, lon: float, lat: float) -> None:
pipeline = r.pipeline()
pipeline.geoadd("drivers:active", (lon, lat, driver_id))
pipeline.hset(LAST_SEEN_KEY, driver_id, int(datetime.now().timestamp()))
pipeline.execute()
def find_active_drivers(user_lon: float, user_lat: float) -> list[dict]:
results = r.geosearch(
"drivers:active",
longitude=user_lon, latitude=user_lat,
radius=3, unit="km",
sort="ASC", count=20, withdist=True,
)
now = int(datetime.now().timestamp())
active = []
for member, dist in results:
last_seen = r.hget(LAST_SEEN_KEY, member)
if last_seen and (now - int(last_seen)) <= STALE_THRESHOLD_SECONDS:
active.append({"driver_id": member, "distance_km": round(dist, 3)})
return active
Pattern 2: Periodic cleanup job
Một job chạy mỗi 30–60 giây, quét hash drivers:last_seen, tìm tài xế có timestamp cũ và ZREM họ khỏi GEO key:
def cleanup_stale_drivers() -> int:
"""Xóa tài xế offline khỏi GEO key. Trả về số tài xế đã xóa."""
now = int(datetime.now().timestamp())
all_drivers = r.hgetall(LAST_SEEN_KEY)
stale = [
driver_id
for driver_id, ts in all_drivers.items()
if now - int(ts) > STALE_THRESHOLD_SECONDS
]
if stale:
r.zrem("drivers:active", *stale)
r.hdel(LAST_SEEN_KEY, *stale)
return len(stale)
Pattern 3: Key toàn bộ với TTL (rebuild định kỳ)
Tạo GEO key với TTL ngắn (vd 60s). Chỉ những tài xế đang online mới cập nhật vị trí trong window này. Khi key hết hạn, tự động xóa hết. Tuy nhiên, pattern này tạo đợt rebuild đồng thời khi key expire — phù hợp hơn với use case ít tài xế hoặc tần suất rebuild thấp.
Trong thực tế, Pattern 1 + Pattern 2 kết hợp là phổ biến nhất: timestamp filter lọc ở application layer, còn cleanup job chạy nền để giữ GEO key không phình to theo thời gian.
Performance & Big-O
| Lệnh | Complexity | Ghi chú |
|---|---|---|
GEOADD | O(log N) mỗi member | N = tổng số member trong key |
GEOPOS | O(log N) mỗi member | |
GEODIST | O(log N) | |
GEOSEARCH BYRADIUS | O(N + log M) | N = số element trong vùng tìm, M = tổng element trong key |
GEOSEARCH BYBOX | O(N + log M) | Tương tự BYRADIUS |
GEOSEARCHSTORE | O(N + log M + K log K) | K = số element ghi vào ZSet đích |
Với GEOSEARCH: phần tốn kém là N (số điểm nằm trong vùng kết quả), không phải M (tổng điểm trong key). Điều này nghĩa là:
- 1 triệu tài xế trong key, bán kính tìm 3km chỉ trả 50 tài xế → N=50, rất nhanh (~0.5ms).
- Bán kính khổng lồ (50km giữa thành phố đông) → N có thể là 500k → chậm hơn nhiều và trả về lượng data khổng lồ.
Kết luận thực tế: giới hạn bán kính tìm kiếm ở mức hợp lý cho use case cụ thể, luôn dùng COUNT để cắt bớt kết quả. Tránh query không COUNT trên GEO key lớn với bán kính rộng.
Anti-patterns & Best Practices
Anti-patterns
-
Nhầm thứ tự longitude/latitude: GEOADD nhận
longitude latitude member, không phảilatitude longitude. Nếu truyền ngược, điểm được lưu tại vị trí sai hoàn toàn (thường ra giữa đại dương). Bug này không gây lỗi runtime — Redis không validate giá trị địa lý — nên đặc biệt khó phát hiện. - GEOSEARCH không có COUNT trên key lớn: bán kính 20km ở TP.HCM có thể chứa hàng chục nghìn điểm. Không đặt COUNT → trả về toàn bộ → chặn Redis event loop, băng thông mạng tăng đột biến, application OOM khi deserialize.
- Dùng GEORADIUS thay vì GEOSEARCH: GEORADIUS deprecated từ Redis 6.2, bị xóa khỏi documentation chính thức. Mặc dù một số phiên bản Redis vẫn chấp nhận như alias, không nên dựa vào đó.
- Dùng GEO cho routing đường thực: GEODIST trả về straight-line (đường chim bay). Khoảng cách 2km theo đường chim bay có thể là 4km theo đường thực vì tắc đường, sông, khu dân cư. Cho routing thực dùng OSRM (open-source) hoặc Google Maps Distance Matrix API.
- Không cleanup tài xế offline: GEO key giữ nguyên tài xế đã offline. GEOSEARCH vẫn trả về họ. Kết quả application hiển thị "tài xế có sẵn" nhưng không thể dispatch được. Cần cleanup pattern (xem Mục 11).
-
Lưu toàn bộ thông tin vào member name: member name nên là ID đơn giản (vd
"driver:1"), không nên nhét JSON hay metadata dài vào đó. Data phụ lưu ở Hash key tương ứng.
Best practices
- Luôn dùng GEOSEARCH, không dùng GEORADIUS / GEORADIUSBYMEMBER.
- Đặt COUNT trong mọi GEOSEARCH để giới hạn kết quả.
- Double-check thứ tự tham số: longitude trước, latitude sau — cả trong CLI lẫn client library.
- Tách thông tin phụ (tên, trạng thái, thông tin chi tiết) ra Hash key riêng, GEO key chỉ giữ tọa độ + member ID.
- Kết hợp timestamp tracking với periodic cleanup để xử lý stale location.
- Dùng WITHDIST để lấy khoảng cách thực tế trong cùng một lần query, không cần gọi GEODIST riêng.
- Kiểm tra encoding bằng
OBJECT ENCODING <key>: nhỏ thìlistpack, lớn thìskiplist. Khi key chuyển sang skiplist (>128 member mặc định), overhead memory tăng nhưng Big-O vẫn O(log N).
Tổng Kết & Quiz
Tổng kết
- GEO là Sorted Set với score = geohash 52-bit (longitude + latitude interleaved). Precision ~0.6m.
- Thứ tự tham số: longitude trước, latitude sau — ngược với convention địa lý thông thường.
- GEOADD để thêm / cập nhật; GEOPOS lấy tọa độ; GEODIST tính khoảng cách straight-line.
- GEOSEARCH (Redis 6.2+) thay thế GEORADIUS đã deprecated. Hỗ trợ BYRADIUS, BYBOX, FROMMEMBER, FROMLONLAT, WITHCOORD, WITHDIST, COUNT.
- GEOSEARCHSTORE lưu kết quả vào ZSet mới; với STOREDIST, score = khoảng cách.
- Use cases chính: ride-hailing (tài xế liên tục cập nhật), store locator (dữ liệu tĩnh), geofencing (vào/ra vùng).
- GEO không có per-member TTL. Xử lý stale location bằng timestamp tracking + filter + periodic cleanup.
- GEOSEARCH: O(N + log M) với N = số điểm trong vùng. Luôn giới hạn bán kính và đặt COUNT.
- Không dùng GEO cho routing đường thực (chỉ straight-line) — cần OSRM / routing API bên ngoài.
Quiz 5 câu
- GEO key Redis thực chất là data type gì ở tầng lưu trữ? Score của mỗi member có ý nghĩa gì?
- Tại sao GEOSEARCH với bán kính nhỏ nhanh hơn bán kính lớn dù key có cùng số lượng member? Điều này ảnh hưởng gì đến thiết kế hệ thống?
- Tài xế A ở toạ độ (106.7009, 10.7769). Tài xế B ở (106.7050, 10.7800). Lệnh CLI nào tính khoảng cách giữa họ theo km (giả sử đều trong key
drivers)? - Vì sao GEO không phù hợp để trả lời câu hỏi "đường đi ngắn nhất từ A đến B là bao nhiêu km?"
- Mô tả vấn đề stale location và ít nhất một cách xử lý. Tại sao GEO không tự giải quyết được vấn đề này?
Đáp án gợi ý
- GEO key là Sorted Set. Score của mỗi member là một số nguyên 52-bit được tính bằng cách interleaved (xen kẽ) 26 bit encode longitude và 26 bit encode latitude. Score này biểu diễn vị trí địa lý nhưng mang hình thức số nguyên — không có ý nghĩa trực tiếp nếu đọc thô.
- Complexity của GEOSEARCH là O(N + log M) với N = số điểm nằm trong vùng kết quả, M = tổng điểm trong key. Bán kính nhỏ → N nhỏ → nhanh; bán kính lớn → N lớn → chậm hơn và trả nhiều data hơn. Hệ quả thiết kế: giới hạn bán kính tối đa hợp lý cho use case (vd max 10km), luôn đặt COUNT để cắt output.
GEODIST drivers "driver:A" "driver:B" km— thay "driver:A" và "driver:B" bằng tên member thực tế. Hai member phải cùng trong một key.- GEODIST và GEOSEARCH tính khoảng cách straight-line (đường chim bay) theo công thức WGS84, không biết gì về đường sá, cầu, sông. Khoảng cách thực tế đi xe có thể gấp đôi hoặc hơn. Để tính routing đường bộ thực, cần engine như OSRM, Valhalla, hoặc Google Maps API.
- Stale location: khi tài xế offline (tắt app), vị trí của họ vẫn tồn tại trong GEO key (GEO không có per-member TTL). GEOSEARCH vẫn trả về họ trong kết quả, dù thực tế họ không có mặt. GEO không tự giải quyết vì đây là Sorted Set — không có khái niệm expiry cho từng member. Cách xử lý: track timestamp update cuối vào Hash riêng, khi đọc kết quả lọc bỏ member có timestamp quá cũ; song song đó có periodic job ZREM member offline ra khỏi GEO key.
Bài tiếp theo
Bài 28 đào sâu vào Encoding Internals: Redis chọn listpack, intset, skiplist, hay embstr/raw như thế nào, tại sao việc vượt ngưỡng encoding ảnh hưởng đến memory và Big-O, và cách đo đạc trong thực tế.
