Redis GEO

一、GEO数据结构原理

1. Redis GEO 数据结构

Redis GEO 底层使用的是 Sorted Set(有序集合),使用 GeoHash 算法将二维的经纬度转换为一维的字符串。

GeoHash 算法原理:

  • 将地球划分为网格

  • 每个网格用二进制编码表示

  • 将二进制编码转换为 base32 字符串

  • 字符串越长,精度越高

实际存储格式:

java 复制代码
key = "shop:geo:1"  # 类型1的店铺
member = "店铺ID"
score = GeoHash编码后的整数值

二、代码逐行解析

1. 查询店铺信息

java 复制代码
List<Shop> list = shopService.list();
// 假设Shop实体类包含:
// - id: Long      店铺ID
// - typeId: Long  店铺类型ID(如1:餐厅,2:酒店)
// - x: Double     经度
// - y: Double     纬度

2. 按照类型分组

java 复制代码
Map<Long, List<Shop>> map = list.stream()
    .collect(Collectors.groupingBy(Shop::getTypeId));

分组结果示例:

java 复制代码
{
    1: [Shop{id=1001, typeId=1, x=116.404, y=39.915}, Shop{id=1002, typeId=1, x=116.408, y=39.918}],
    2: [Shop{id=2001, typeId=2, x=116.406, y=39.912}, Shop{id=2002, typeId=2, x=116.410, y=39.916}],
    3: [Shop{id=3001, typeId=3, x=116.402, y=39.920}]
}

3. 关键:GeoLocation 详解

java 复制代码
// GeoLocation的结构
public class GeoLocation<T> {
    private final T name;     // 成员标识(这里用店铺ID)
    private final Point point; // 经纬度坐标
}

两种写入方式对比:

方式一:单条写入(注释掉的代码)
java 复制代码
// 每次循环调用一次Redis,性能较差
stringRedisTemplate.opsForGeo().add(
    key,                     // Redis键
    new Point(shop.getX(), shop.getY()),  // 经纬度
    shop.getId().toString()  // 成员名称(店铺ID)
);
方式二:批量写入(代码使用的)
java 复制代码
// 1. 创建GeoLocation列表
List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>();

// 2. 为每个店铺创建GeoLocation对象
for(Shop shop : value) {
    locations.add(new RedisGeoCommands.GeoLocation<>(
        shop.getId().toString(),           // member:店铺ID作为标识
        new Point(shop.getX(), shop.getY()) // point:经纬度坐标
    ));
}

// 3. 批量写入Redis(一次网络请求)
stringRedisTemplate.opsForGeo().add(key, locations);

批量写入的优势:

  • 减少网络IO次数

  • 提高性能

  • 原子性操作

三、实际数据示例

假设店铺数据:

店铺ID 类型ID 名称 经度(x) 纬度(y)
1001 1 星巴克 116.404 39.915
1002 1 麦当劳 116.408 39.918
2001 2 如家酒店 116.406 39.912

Redis 存储结果:

bash 复制代码
# 类型1的店铺集合
> GEOADD shop:geo:1 116.404 39.915 "1001"
> GEOADD shop:geo:1 116.408 39.918 "1002"

# 类型2的店铺集合  
> GEOADD shop:geo:2 116.406 39.912 "2001"

# 查看存储的数据结构
> TYPE shop:geo:1
zset  # 实际是有序集合

> ZRANGE shop:geo:1 0 -1 WITHSCORES
1) "1001"
2) "4069885365071587"  # GeoHash编码后的分数
3) "1002"
4) "4069885365805148"

四、如何使用存储的数据

1. 查询附近店铺

java 复制代码
// 查询经纬度(116.405, 39.916)附近10公里内的类型1店铺
Circle circle = new Circle(
    new Point(116.405, 39.916),  // 中心点
    new Distance(10, Metrics.KILOMETERS)  // 半径10公里
);

// 按距离升序返回
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
    .newGeoRadiusArgs()
    .includeDistance()  // 包含距离
    .includeCoordinates()  // 包含坐标
    .limit(10)  // 限制返回数量
    .sortAscending();  // 按距离升序

GeoResults<RedisGeoCommands.GeoLocation<String>> results = 
    stringRedisTemplate.opsForGeo()
        .radius("shop:geo:1", circle, args);

2. 获取两个店铺的距离

java 复制代码
// 计算店铺1001和1002的距离
Distance distance = stringRedisTemplate.opsForGeo()
    .distance("shop:geo:1", "1001", "1002", Metrics.KILOMETERS);

System.out.println(distance.getValue());  // 输出:2.356 km

3. 获取店铺的经纬度

java 复制代码
// 获取店铺1001的坐标
List<Point> points = stringRedisTemplate.opsForGeo()
    .position("shop:geo:1", "1001");

Point point = points.get(0);
System.out.println("经度:" + point.getX());  // 116.404
System.out.println("纬度:" + point.getY());  // 39.915

五、完整应用场景示例

场景:附近餐厅推荐

java 复制代码
@Service
public class ShopService {
    
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    
    /**
     * 查找附近的餐厅
     */
    public List<ShopVO> findNearbyRestaurants(
        Double userLng,   // 用户经度
        Double userLat,   // 用户纬度
        Double radiusKm,  // 搜索半径(公里)
        Integer limit     // 返回数量
    ) {
        String key = "shop:geo:1";  // 类型1=餐厅
        
        // 1. 创建搜索范围
        Circle circle = new Circle(
            new Point(userLng, userLat),
            new Distance(radiusKm, Metrics.KILOMETERS)
        );
        
        // 2. 设置查询参数
        RedisGeoCommands.GeoRadiusCommandArgs args = 
            RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
                .includeDistance()
                .includeCoordinates()
                .limit(limit)
                .sortAscending();
        
        // 3. 执行GEO查询
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = 
            stringRedisTemplate.opsForGeo().radius(key, circle, args);
        
        // 4. 解析结果
        List<ShopVO> result = new ArrayList<>();
        for (GeoResult<RedisGeoCommands.GeoLocation<String>> geoResult : geoResults) {
            // 店铺ID
            String shopId = geoResult.getContent().getName();
            
            // 距离
            Distance distance = geoResult.getDistance();
            
            // 坐标
            Point point = geoResult.getContent().getPoint();
            
            // 创建返回对象
            ShopVO shopVO = new ShopVO();
            shopVO.setId(Long.parseLong(shopId));
            shopVO.setDistance(distance.getValue());
            shopVO.setLongitude(point.getX());
            shopVO.setLatitude(point.getY());
            
            result.add(shopVO);
        }
        
        return result;
    }
    
    /**
     * 添加新店铺
     */
    public void addShop(Shop shop) {
        // 添加到数据库
        shopService.save(shop);
        
        // 同步到Redis GEO
        String key = "shop:geo:" + shop.getTypeId();
        stringRedisTemplate.opsForGeo().add(
            key,
            new Point(shop.getX(), shop.getY()),
            shop.getId().toString()
        );
    }
}

六、性能优化建议

1. 数据预热

java 复制代码
@Component
public class GeoDataLoader {
    @PostConstruct  // 项目启动时执行
    public void loadGeoData() {
        // 加载GEO数据到Redis
        // ... 同测试代码
    }
}

2. 分片策略(店铺数量多时)

java 复制代码
// 按城市分片,避免单个key过大
String key = "shop:geo:" + typeId + ":" + cityCode;

3. 缓存更新策略

java 复制代码
// 监听店铺信息变更
@EventListener
public void handleShopUpdate(ShopUpdateEvent event) {
    Shop shop = event.getShop();
    String key = "shop:geo:" + shop.getTypeId();
    
    // 更新GEO位置
    stringRedisTemplate.opsForGeo().add(
        key,
        new Point(shop.getX(), shop.getY()),
        shop.getId().toString()
    );
}

七、注意事项

1. 精度问题

bash 复制代码
// Redis GEO 精度大约为 1-2 米,适合大多数应用
// 如果需要更高精度,可以考虑其他方案

2. 数据一致性

java 复制代码
// 建议使用事务或分布式锁保证数据库和Redis的一致性
@Transactional
public void updateShopLocation(Long shopId, Double lng, Double lat) {
    // 1. 更新数据库
    shopMapper.updateLocation(shopId, lng, lat);
    
    // 2. 更新Redis
    Shop shop = shopMapper.selectById(shopId);
    String key = "shop:geo:" + shop.getTypeId();
    stringRedisTemplate.opsForGeo().add(
        key, 
        new Point(lng, lat), 
        shopId.toString()
    );
}

3. 数据清理

java 复制代码
// 店铺删除时同步清理GEO数据
public void deleteShop(Long shopId) {
    Shop shop = getById(shopId);
    
    // 1. 删除数据库记录
    removeById(shopId);
    
    // 2. 删除Redis GEO数据
    String key = "shop:geo:" + shop.getTypeId();
    stringRedisTemplate.opsForGeo().remove(key, shopId.toString());
}

通过这样的设计,你可以实现高效的附近店铺搜索功能,查询性能远高于直接在数据库中使用距离计算函数(如MySQL的ST_Distance)。

相关推荐
BD_Marathon1 小时前
【Java】集合里面的数据结构
java·数据结构·python
代码不停1 小时前
Java字符串 和 队列 + 宽搜 题目练习
java·开发语言
柒.梧.1 小时前
Servlet原理和Tomcat原理的知识总结
java·servlet·tomcat
quan26311 小时前
20251204,职级权限,开发实践分享
java·递归·java权限·职级架构
今天也想MK代码1 小时前
JS 注入机制深度解析
java·前端·javascript
路边草随风1 小时前
SparkSession read() 执行Impala任意sql返回Dataset
java·sql·spark
开心香辣派小星1 小时前
23种设计模式-18观察者(Observer)模式
java·开发语言·设计模式
Slow菜鸟1 小时前
Java项目基础架构(一)| 工程架构选型指南
java·开发语言·架构
专注于大数据技术栈1 小时前
java学习--注解之@Deprecated
java·学习