首页 > Java, Redis > Redis GeoHash 的一个小示例

Redis GeoHash 的一个小示例

上周产品经理提了一个类似于 LBS 的应用,第一时间想到了忘记了之前什么时候看 Redis 的 API,发现 Redis 自 3.2 版本之后,新增了一类关于地理位置相关的 API,于是拿来测试一下,发现特别好用,写一个小例子作为笔记。

首先需要说明的是,由于我们公司的 JDK 的版本是 1.7,所以我采用的 spring-data-redis 的版本是:1.8.20.RELEASE,最新二点几的版本已经不支持 JDK 1.7,而一点几和二点几的版本的 API 有略微的差异(下面会说明,还有一点点我的小感悟),废话不多说,直接看例子:


    @Override
    public Long geoAdd(String key, List<Entity> entities) {
        redisTemplate.delete(key);
        GeoOperations geoOperations = redisTemplate.opsForGeo();
        Map<String, Point> map = new HashMap<>();
        Point point = null;
        for (Entity entity : entities) {
            point = new Point(entity.getLongitude(), entity.getLatitude());
            map.put(gson.toJson(entity), point);
        }

        Long add = geoOperations.geoAdd(key, map);

        return add;

    }

参数就两个很简单,一个是 key,一个是数据集,我们将在这个集合中找出符合条件的数据,需要说明的是:

1. 第一行我先调用了一个 delete 方法,是将上次放进去的数据删除,因为这个命令是 add,也就是新增,但是 redis 并没有提供直接删除这个 key 的命令(有一个 remove 的方法,但是需要传入删除哪些数据,也就是不能只给一个 key,把这个 key 对应的数据都删除,个人感觉不太好用),当然你也可以在计算后取得相应的数据之后删除,个人感觉都一样,不要忘记清理数据就行,另一个方法就是设置过期时间,也都行;
2. 为什么要清理数据?因为这个 add,不同的情况,放进去的数据应该不同的,如果 entities 已经发生变更,而一直 add,那么数据将会乱掉,所以先把之前的数据删掉再说;
3. 我个人采用的是把数据放到了 Map 中,其中 key 是对象序列化之后的 json 串,目的是为了下面找到对应的数据之后,直接反序列化成对象进行返回,当然也可以采用其他的方案,还有就是 add 还有一些其他的 API,这个大家可以自己看文档,选择合适的就行;

数据放进去之后就是计算了,我们的需求就是算一个人旁边几公里内有多少符合条件的数据,代码如下:


    @Override
    public Page<Entity> geoRadius(String key, Double latitude, Double longitude, Integer distance, String sort, Integer pageNo, Integer pageSize) {
        GeoOperations geoOperations = redisTemplate.opsForGeo();
        Circle circle = new Circle(new Point(latitude, longitude), new Distance(distance / 1000, RedisGeoCommands.DistanceUnit.KILOMETERS));
        RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs();
        geoRadiusCommandArgs = geoRadiusCommandArgs.includeCoordinates().includeDistance();
        if ("DESC".equals(sort)) {
            geoRadiusCommandArgs.sortDescending();
        } else {
            geoRadiusCommandArgs.sortAscending();
        }

        GeoResults<RedisGeoCommands.GeoLocation<String>> radiusGeo = geoOperations.geoRadius(key, circle, geoRadiusCommandArgs);
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = radiusGeo.getContent();
        List<Entity> entities = null;
        Integer size = null;
        if (CollectionUtils.isNotEmpty(list)) {

            int limit = pageNo * pageSize;
            size = list.size();
            if (limit > size) {
                limit = size;
            }

            list = list.subList((pageNo - 1) * pageSize, limit);
            entities = new ArrayList<>(pageSize);

            for (GeoResult<RedisGeoCommands.GeoLocation<String>> geoLocationGeoResult : list) {
                RedisGeoCommands.GeoLocation<String> geoLocationGeoResultContent = geoLocationGeoResult.getContent();

                Distance distance1 = geoLocationGeoResult.getDistance();
                double value = distance1.getValue();
                value = (double) Math.round(value * 100) / 100;

                String name = geoLocationGeoResultContent.getName();
                Entity entity = gson.fromJson(name, Entity.class);
                entity.setDistance(value);

                entities.add(entity);
            }
        }
        Page<entity> page = new Page<>();
        page.setData(entities);
        page.setTotal(size);

        return page;
    }

这段代码需要说明的是:

1. 参数 key 就是之前放进去数据的 key(这不废话吗?),然后是当前人的经纬度,多远以内的数据,由近到远排序还是由远到近的排序方式,以及分页数据;
2. 例子中距离多远以内的,传进来的是米,但是后面返回的是千米,所以除以 1000 转了一次,根据业务而定;
3. 默认是正序,也就是由近到远,但是也支持由远到近
4. 如果仅仅是排序,也就是对所有的数据由远到近或者由近到远,那么距离就应该是无穷远,Integer.MAX_VALUE;
5. 有一个 geoRadiusCommandArgs.limit(); 方法,其实就是取 top N,应该挺常用的,但这次不适合我们的应用
6. list = list.subList((page – 1) * pageSize, limit); 的意思是说,只需要取 pageSize 个数据进行反序列化就好了,而上面那个判断,是为了防止最后一页下表越界;
7. value = (double) Math.round(value * 10) / 10; 数据距离中心点的距离,四舍五入,根据需求就好;
8. 就是对应的数据反序列化以及组装返回了

经过以上,就可以做一个基于 LBS 的应用了,很简单吧?需要补充说明的是:

1. redis 还有好几个其他的 GeoHash 相关的命令和 API,自行查询文档就好;
2. redis 这个算法,其实就是基于 GeoHash,关于这个算法已经实现,网上有很多资料,有比较详细的说明,感兴趣的自行查阅相关文档就好;
3. spring-data-redis 一点几和二点几,你说相关的 API 有区别,有什么区别啊?区别很简单就是一点几的方法,都是 geo 打头,例如 geoAdd、geoRadius 等,而二点几版本直接是:add、radius 等,其实我们根据命令 GeoOperations geoOperations = redisTemplate.opsForGeo(); 得到的肯定是操作 geo 相关的命令,这个时候方法再以 geo 打头不是有点画蛇添足吗?但是这个给我们什么提示呢?我们很多时候,根据 id 查询的是,不用写 entityService.getEntityById(),因为理论上这个时候我们肯定是查询 Entity,所以直接写 entityService.getById(),这就告诉我们命名的时候需要仔细斟酌,见名知意的情况下越短越好。

参考资料:
1. http://redisdoc.com/geo/index.html 官方 API 是最好的学习资料
2. https://mygodccl.iteye.com/blog/2374978

全文完,如果本文对您有所帮助,请花 1 秒钟帮忙点击一下广告,谢谢。

作 者: BridgeLi,https://www.bridgeli.cn
原文链接:https://www.bridgeli.cn/archives/618
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
分类: Java, Redis 标签: , ,
  1. 本文目前尚无任何评论.

请输入正确的验证码