如何使用StackExchange.Redis优化C#点在多边形校验 实现经纬度城市归属查询
基于StackExchange.Redis实现点在多边形快速匹配的解决方案
方案一:使用RediSearch模块(性能最优,推荐)
Redis原生不支持多边形空间查询,加载RediSearch模块后可直接支持GeoShape级别的多边形包含查询,无需自行遍历所有城市多边形,单次查询耗时可稳定在1ms以内。
实现步骤
- 环境准备:确保Redis实例已加载RediSearch、ReJSON模块,主流云厂商Redis实例均支持一键开启,自建实例可直接加载官方编译好的模块文件。
- 数据入库&索引构建:将所有城市的多边形以GeoJSON格式存入Redis JSON结构,同时为空间字段建立GEOSHAPE类型索引
// 连接Redis var multiplexer = ConnectionMultiplexer.Connect("你的Redis连接串"); var db = multiplexer.GetDatabase(); var ft = db.FT(); // 对应RediSearch命令 var json = db.JSON(); // 第一步:创建索引 ft.Create("idx:cities", new FTCreateParams() .On(IndexDataType.Json) .Prefix("city:"), new Schema() .AddGeoshapeField("$.polygon", "polygon")); // 第二步:存城市数据,Polygon转GeoJSON格式 var city1GeoJson = new { type = "Polygon", coordinates = new [] { new [] { 1.11865, 5.9581 }, // 注意RediSearch坐标是[经度,纬度]顺序 new [] { -1.47411, 4.99572 }, new [] { -2.00145, 6.19298 }, new [] { 0.38257, 6.98422 }, new [] { 1.11865, 5.9581 } } }; json.Set("city:1", "$", new { name = "City1", polygon = city1GeoJson }); // 按同样逻辑存入所有城市数据
- 查询逻辑:直接传入待判断的经纬度点,一次查询即可返回包含该点的城市
// 待判断点:纬度39.1,经度-6.5 var query = new Query("@polygon:[CONTAINS $POINT]") .SetParam("POINT", "POINT(-6.5 39.1)"); // 格式为POINT(经度 纬度) var res = ft.Search("idx:cities", query); // res.Documents里就是所有包含该点的城市数据,直接取即可
方案二:原生Redis无模块适配方案
如果无法为Redis加载额外模块,可通过「GEO预筛选+本地计算+结果缓存」的架构提升性能,相比当前全量遍历的逻辑性能可提升10倍以上。
实现步骤
- 预计算每个城市多边形的最小外接矩形(MBR),记录每个城市MBR的最大/最小经纬度、中心点坐标,将城市的多边形序列化后和MBR数据一同存入Redis Hash结构,同时将所有城市的中心点存入Redis GEO集合。
// 示例:计算多边形MBR public (double minLng, double maxLng, double minLat, double maxLat, double centerLng, double centerLat) CalcMBR(List<GeoPoints> poly) { var lngs = poly.Select(p => p.Lg).ToList(); var lats = poly.Select(p => p.Lt).ToList(); var minLng = lngs.Min(); var maxLng = lngs.Max(); var minLat = lats.Min(); var maxLat = lats.Max(); return (minLng, maxLng, minLat, maxLat, (minLng+maxLng)/2, (minLat+maxLat)/2); } // 存GEO集合 db.GeoAdd("geo:cities", new GeoEntry(centerLng, centerLat, cityId)); // 存城市元数据 db.HashSet($"city:meta:{cityId}", new HashEntry[] { new ("poly", JsonSerializer.Serialize(poly)), new ("minLng", minLng), new ("maxLng", maxLng), new ("minLat", minLat), new ("maxLat", maxLat) });
- 查询时先通过
GEORADIUS筛出可能匹配的城市,再做精确判断:
// 第一步:GEO预筛选,半径按你所在区域最大城市的直径设置,比如500km var candidateCityIds = db.GeoRadius("geo:cities", point.Lg, point.Lt, 500, GeoUnit.Kilometers) .Select(x => x.Member.ToString()) .ToList(); foreach(var cityId in candidateCityIds) { // 先判断点是否在城市MBR内,过滤无效候选 var meta = db.HashGetAll($"city:meta:{cityId}"); var minLng = double.Parse(meta.First(x => x.Name == "minLng").Value); var maxLng = double.Parse(meta.First(x => x.Name == "maxLng").Value); var minLat = double.Parse(meta.First(x => x.Name == "minLat").Value); var maxLat = double.Parse(meta.First(x => x.Name == "maxLat").Value); if(point.Lg < minLng || point.Lg > maxLng || point.Lt < minLat || point.Lt > maxLat) continue; // 再用你原有的IsPointInPolygon方法判断 var poly = JsonSerializer.Deserialize<List<GeoPoints>>(meta.First(x => x.Name == "poly").Value); if(IsPointInPolygon(poly, point)) { // 匹配到城市,把结果缓存1小时,相同点下次直接返回 db.StringSet($"geo:point:{point.Lg:F6}:{point.Lt:F6}", cityId, TimeSpan.FromHours(1)); return cityId; } }
额外优化建议
- 你原有
IsPointInPolygon方法可先增加MBR判断逻辑,再执行射线法计算,减少无效循环。 - 如果城市数量过万,可增加行政区划层级过滤,先匹配省份再匹配城市,进一步缩小候选范围。
内容的提问来源于stack exchange,提问作者user2921359
相关产品推荐
相关产品推荐

