Redis 的 Geospatial(地理空间)功能在实际项目中非常实用,它基于 Sorted Set 数据结构,通过 GeoHash 算法将经纬度编码为分数,实现了高效的地理位置存储和查询。
1. Geospatial 核心命令
bash
# 添加地理位置
GEOADD key longitude latitude member
# 获取地理位置
GEOPOS key member
# 计算距离
GEODIST key member1 member2 [unit]
# 附近搜索
GEORADIUS key longitude latitude radius unit [WITHDIST] [WITHCOORD] [COUNT count]
GEORADIUSBYMEMBER key member radius unit [WITHDIST] [WITHCOORD] [COUNT count]
# GeoHash(用于外部地图服务)
GEOHASH key member
2. 实际应用场景
2.1 附近的人/商家搜索
python
import redis
import json
class NearbyService:
def __init__(self):
self.redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
def add_user_location(self, user_id, lng, lat):
"""添加用户位置"""
self.redis_client.geoadd(
'user:locations',
lng, lat, f"user:{user_id}"
)
def find_nearby_users(self, center_lng, center_lat, radius_km=5, limit=10):
"""查找附近用户"""
return self.redis_client.georadius(
'user:locations',
center_lng, center_lat, radius_km, 'km',
withdist=True, # 返回距离
withcoord=True, # 返回坐标
count=limit, # 限制数量
sort='ASC' # 按距离排序
)
def calculate_distance(self, user1_id, user2_id):
"""计算两个用户间的距离"""
return self.redis_client.geodist(
'user:locations',
f"user:{user1_id}",
f"user:{user2_id}",
unit='km'
)
# 使用示例
service = NearbyService()
# 添加测试数据
service.add_user_location("1001", 116.3974, 39.9093) # 北京
service.add_user_location("1002", 116.4074, 39.9193) # 北京附近
service.add_user_location("1003", 121.4737, 31.2304) # 上海
# 查找天安门附近的用户
nearby_users = service.find_nearby_users(116.3974, 39.9093, 10)
print("附近用户:", nearby_users)
2.2 外卖/快递配送系统
python
class DeliverySystem:
def __init__(self):
self.redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
def add_restaurant(self, restaurant_id, lng, lat, name):
"""添加餐厅位置"""
self.redis_client.geoadd('restaurants', lng, lat, restaurant_id)
self.redis_client.hset(f'restaurant:{restaurant_id}', 'name', name)
def add_rider(self, rider_id, lng, lat):
"""添加骑手位置"""
self.redis_client.geoadd('riders', lng, lat, f"rider:{rider_id}")
def find_nearest_riders(self, restaurant_id, radius_km=3, count=5):
"""为餐厅寻找最近骑手"""
# 获取餐厅位置
pos = self.redis_client.geopos('restaurants', restaurant_id)
if not pos or not pos[0]:
return []
lng, lat = pos[0]
# 查找附近骑手
riders = self.redis_client.georadius(
'riders', lng, lat, radius_km, 'km',
withdist=True,
count=count,
sort='ASC'
)
return riders
def track_delivery_range(self, order_id, target_lng, target_lat, max_radius_km):
"""监控配送范围"""
key = f"delivery:range:{order_id}"
# 设置目标位置和半径
self.redis_client.geoadd(key, target_lng, target_lat, "target")
self.redis_client.expire(key, 3600) # 1小时过期
return key
# 使用示例
delivery = DeliverySystem()
# 添加餐厅
delivery.add_restaurant("r001", 116.3974, 39.9093, "北京烤鸭店")
# 添加骑手
delivery.add_rider("d001", 116.4000, 39.9100)
delivery.add_rider("d002", 116.3900, 39.9000)
# 寻找最近骑手
nearest_riders = delivery.find_nearest_riders("r001")
print("最近骑手:", nearest_riders)
2.3 车辆监控与电子围栏
python
class VehicleTrackingSystem:
def __init__(self):
self.redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
def update_vehicle_location(self, vehicle_id, lng, lat):
"""更新车辆位置"""
self.redis_client.geoadd('vehicles:current', lng, lat, vehicle_id)
# 同时保存到历史记录(使用时间戳)
timestamp_key = f"vehicles:history:{vehicle_id}"
self.redis_client.geoadd(timestamp_key, lng, lat, str(int(time.time())))
# 保留最近100个位置点
self.redis_client.zremrangebyrank(timestamp_key, 0, -100)
def create_geofence(self, fence_id, points):
"""创建电子围栏"""
# points: [(lng1, lat1), (lng2, lat2), ...]
fence_key = f"geofence:{fence_id}"
# 添加围栏边界点
for i, (lng, lat) in enumerate(points):
self.redis_client.geoadd(fence_key, lng, lat, f"point_{i}")
return fence_key
def check_vehicles_in_fence(self, fence_id, radius_km=0.1):
"""检查围栏内车辆"""
fence_key = f"geofence:{fence_id}"
# 获取围栏中心点(使用第一个点作为参考)
center = self.redis_client.geopos(fence_key, "point_0")
if not center or not center[0]:
return []
lng, lat = center[0]
# 查找围栏内的车辆
vehicles = self.redis_client.georadius(
'vehicles:current',
lng, lat, radius_km, 'km',
withdist=True,
withcoord=True
)
return vehicles
def get_vehicle_trail(self, vehicle_id, start_time=0, end_time=None):
"""获取车辆轨迹"""
if end_time is None:
end_time = int(time.time())
key = f"vehicles:history:{vehicle_id}"
positions = self.redis_client.zrangebyscore(
key, start_time, end_time, withscores=True
)
return positions
# 使用示例
tracking = VehicleTrackingSystem()
# 更新车辆位置
tracking.update_vehicle_location("car001", 116.3974, 39.9093)
# 创建禁行区域围栏
fence_points = [
(116.3964, 39.9083),
(116.3984, 39.9083),
(116.3984, 39.9103),
(116.3964, 39.9103)
]
tracking.create_geofence("restricted_area", fence_points)
# 检查禁行区域内车辆
violations = tracking.check_vehicles_in_fence("restricted_area")
print("禁行区域内车辆:", violations)
3. 性能优化建议
3.1 数据分片策略
python
def get_shard_key(base_key, lng, lat, shard_size=10):
"""根据地理位置分片"""
# 将经纬度映射到分片
lng_shard = int((lng + 180) / 360 * shard_size)
lat_shard = int((lat + 90) / 180 * shard_size)
return f"{base_key}:{lng_shard}:{lat_shard}"
# 使用分片存储
shard_key = get_shard_key("user:locations", lng, lat)
redis_client.geoadd(shard_key, lng, lat, user_id)
3.2 缓存策略
python
def get_nearby_users_cached(center_lng, center_lat, radius_km=5):
"""带缓存的附近用户查询"""
cache_key = f"nearby:{round(center_lng, 2)}:{round(center_lat, 2)}:{radius_km}"
# 尝试从缓存获取
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# 查询数据库
result = find_nearby_users(center_lng, center_lat, radius_km)
# 缓存结果(30秒过期)
redis_client.setex(cache_key, 30, json.dumps(result))
return result
4. 注意事项
-
精度问题:GeoHash 精度有限,不适合需要极高精度的场景
-
数据一致性:定期清理过期数据,避免 Sorted Set 过大
-
内存使用:大量地理位置数据会占用较多内存,需要监控
-
集群部署:在 Redis Cluster 中,Geospatial 数据会根据 key 分布到不同节点
Redis Geospatial 为地理位置相关应用提供了简单高效的解决方案,特别适合需要实时地理位置查询的场景。