SpringBoot使用GeoHash分页查询附近的人:RedisTemplate+GeoHash+Lua

作者:施自扬
微信号:sszzyy123aabbcc

SpringBoot使用GeoHash分页查询附近的人:RedisTemplate+GeoHash+Lua

架构设计

SpringBoot使用GeoHash分页查询附近的人:RedisTemplate+GeoHash+Lua_第1张图片

开发环境

SpringBoot
Redis(version>=3.2)

Redis原生命令实现

一、存入用户的经纬度

  1. geoadd 用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中
  2. 命令格式:
GEOADD [key] [longitude] [latitude] [member]
  1. 模拟五个用户存入经纬度,redis客户端执行如下命令:
GEOADD user 116.48105 39.996794 zhangsan
GEOADD user 116.514203 39.905409 lisi
GEOADD user 116.489033 40.007669 wangwu
GEOADD user 116.562108 39.787602 sunliu
GEOADD user 116.334255 40.027400 zhaoqi
  1. 通过redis客户端查看效果:
    SpringBoot使用GeoHash分页查询附近的人:RedisTemplate+GeoHash+Lua_第2张图片

二、查找距当前用户由近到远附近100km用户

  1. georadiusbymember可以找出位于指定范围内的元素,georadiusbymember 的中心点是由给定的位置元素决定的
  2. 命令格式:
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST]

[WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
  1. 模拟查找100km里距离lisi由近到远五个人
georadiusbymember user lisi 100 km asc count 5
  1. 命令执行效果如下:
    SpringBoot使用GeoHash分页查询附近的人:RedisTemplate+GeoHash+Lua_第3张图片

三、实现分页查询

  1. 每次分页查询的请求都计算一次然后拿到程序中在取相应的分页数据,优缺点:
  • 优点:实现简单,无需额外的存储空间
  • 缺点:当用户量大时,很显然不仅效率低,而且容易把程序的内存搞溢出
  1. 经过查找发现redis的github官网给出了分页Issues(参考:Will the GEORADIUS support pagination?),解决方案如下:
  • 利用GEORADIUSBYMEMBER 命令中的 STOREDIST 将排好序的数据存入一个Zset集合中,以后分页查直接从Zset
  • 命令如下:
georadiusbymember user lisi 100 km asc count 5 storedist lisilimitkey
  • 有序集合效果如下:
    SpringBoot使用GeoHash分页查询附近的人:RedisTemplate+GeoHash+Lua_第4张图片
  • 以后分页查询命令:
//首先删除本身元素
zrem lisilimitkey lisi
//分页查找元素(在此以:查找第1页,每页数量是3为例)
zrange lisilimitkey 0 2 withscores
  • 效果如下:
    SpringBoot使用GeoHash分页查询附近的人:RedisTemplate+GeoHash+Lua_第5张图片

四、代码实现

  1. 完整代码
https://github.com/shiziyang666/public/tree/master/demo
  1. 主要代码展示
  • 启动类
package com.demo;

import com.demo.service.PeopleNearbyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication implements CommandLineRunner {
     

    public static void main(String[] args) {
     
        SpringApplication.run(DemoApplication.class, args);
    }

    @Autowired
    private PeopleNearbyService peopleNearbyService;
    
    @Override
    public void run(String... args) throws Exception {
     
        //添加地理位置
        peopleNearbyService.postUserAddress("city", 116.405285, 39.904989, "北京");
        peopleNearbyService.postUserAddress("city", 121.47, 31.23, "上海");
        peopleNearbyService.postUserAddress("city", 113.27, 23.13, "广州");
        peopleNearbyService.postUserAddress("city", 43.86, 10.40, "深圳");

        //获取附近的人地理位置
        peopleNearbyService.listNearbyUser("city", "深圳", 8000, 4);

        //分页查询附近的人
        peopleNearbyService.listNearbyUserLimit(1, 2, "city", "深圳", "8000", "km", "asc", "shenzhennewkey");
    }

}

  • service
package com.demo.service;

/***
 * shiziyang
 * 附近的人
 */
public interface PeopleNearbyService {
     
    /***
     * 添加地理位置
     * @param key rediskey
     * @param precision 经度
     * @param dimension 维度
     * @param name 位置名称
     */
    void postUserAddress(String key, double precision, double dimension, String name);

    /***
     * 获取最近附近的人地理位置
     * @param key rediskey
     * @param name 位置名称
     * @param distance 范围km数
     * @param count 获取最近几条
     */
    void listNearbyUser(String key, String name, Integer distance, Integer count);

    /***
     * 分页获取最近附近的人地理位置
     * @param pageIndex 第几页
     * @param pageSize 每页条数
     * @param key redis key "city",
     * @param name 位置名称 "深圳",
     * @param distance 距离 "8000",
     * @param distanceUnit 距离单位 "km",
     * @param sort 排序 "asc",
     * @param newKey 新的redis key "shenzhennewkey"
     */
    void listNearbyUserLimit(Integer pageIndex, Integer pageSize, String key, String name, String distance, String distanceUnit, String sort, String newKey);
}
  • ServiceImpl
package com.demo.service.impl;

import com.demo.entity.MemberGpsEntity;
import com.demo.service.PeopleNearbyService;
import com.demo.util.GeoHashUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * shiziyang
 */
@Service
public class PeopleNearbyServiceImpl implements PeopleNearbyService {
     

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private GeoHashUtil geoHashUtil;

    @Override
    public void postUserAddress(String key, double precision, double dimension, String name) {
     
        //对应redis原生命令:GEOADD user 116.48105 39.996794 zhangsan
        redisTemplate.opsForGeo().add(key, new Point(precision, dimension), name);
        geoHashUtil.redisGeoAdd(key, precision, dimension, name);
    }

    @Override
    public void listNearbyUser(String key, String name, Integer distance, Integer count) {
     
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = geoHashUtil.geoNearByPlace(key, name, distance, count);
        System.out.println(geoResults);
    }

    @Override
    public void listNearbyUserLimit(Integer pageIndex, Integer pageSize, String key, String name, String distance, String distanceUnit, String sort, String newKey) {
     
        //将附近的人存储到一个key里
        Object execute = execute("return redis.call('georadiusbymember',KEYS[1],KEYS[2],KEYS[3],KEYS[4],KEYS[5],'storedist',KEYS[6])", key, name, distance, distanceUnit, sort, newKey);
        //给新key设置失效时间
        redisTemplate.expire(newKey, 6, TimeUnit.HOURS);
        //删除自己
        redisTemplate.opsForGeo().remove(newKey, name);
        //开始条数
        Integer startPage = (pageIndex - 1) * pageSize;
        //结束条数
        Integer endPage = pageIndex * pageSize - 1;
        //获取分页信息
        Set<ZSetOperations.TypedTuple<Object>> aaa =
                redisTemplate.opsForZSet().rangeWithScores(newKey, startPage, endPage);
        //处理返回值
        List<MemberGpsEntity> memberGpsEntityList = aaa.stream().map(aa -> {
     
            MemberGpsEntity memberGpsEntity = new MemberGpsEntity();
            //名称
            memberGpsEntity.setName(aa.getValue().toString());
            //距离
            memberGpsEntity.setDistance(aa.getScore());
            return memberGpsEntity;
        }).collect(Collectors.toList());
        //打印结果
        System.out.println(memberGpsEntityList);
    }

    /**
     * 执行lua脚本
     *
     * @param text lua 脚本
     * @param str  lua脚本的参数
     */
    private Object execute(String text, String... str) {
     
        //参数处理
        List<String> params = Arrays.asList(str);
        DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
        //设置返回值
        defaultRedisScript.setResultType(Long.class);
        //设置脚本
        defaultRedisScript.setScriptText(text);
        //执行命令
        Object execute = redisTemplate.execute(defaultRedisScript, params);
        return execute;
    }
}

  • GeoHashUtil
package com.demo.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;

@Component
public class GeoHashUtil {
     

    @Autowired
    private RedisTemplate redisTemplate;

    /***
     * 将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。
     * @param key redis的key
     * @param precision   经度
     * @param dimension   纬度
     * @param name  名称
     * @return
     */
    public Long redisGeoAdd(String key, double precision, double dimension, String name) {
     
//        Long addedNum = redisTemplate.opsForGeo().add("city", new Point(116.405285, 39.904989), "北京");
//        Long addedNum = redisTemplate.opsForGeo().add("city", new Point(121.47, 31.23), "上海");
//        Long addedNum = redisTemplate.opsForGeo().add("city", new Point(113.27, 23.13), "广州");
        Long addedNum = redisTemplate.opsForGeo().add(key, new Point(precision, dimension), name);//params: key, Point(经度, 纬度), 地方名称
        return addedNum;
    }

    /***
     * 从key里返回所有给定位置元素的位置(经度和纬度)。
     * @param key redis的key
     * @param nameList  名称的集合
     */
    public List<Point> redisGeoGet(String key, List<String> nameList) {
     
        List<Point> points = redisTemplate.opsForGeo().position(key, nameList);//params: key, 地方名称...
        return points;
    }


    /***
     * 返回两个给定位置之间的距离。
     * @param key redis的key
     * @param name1 地方名称1
     * @param name2 地方名称2
     * @return
     */
    public Distance geoDist(String key, String name1, String name2) {
     
        Distance distance = redisTemplate.opsForGeo()
                .distance(key, name1, name2, RedisGeoCommands.DistanceUnit.KILOMETERS);//params: key, 地方名称1, 地方名称2, 距离单位
        return distance;
    }


    /***
     * 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素,并给出所有位置元素与中心的平均距离。
     * @param key redis的key
     * @param precision 经度
     * @param dimension 纬度
     * @param distance 距离
     * @param count 人数
     * @return
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> redisNearByXY(String key, double precision, double dimension, Integer distance, Integer count) {
     
        Circle circle = new Circle(new Point(precision, dimension), new Distance(distance, Metrics.KILOMETERS));//Point(经度, 纬度) Distance(距离量, 距离单位)
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(count);
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo()
                .radius(key, circle, args);//params: key, Circle, GeoRadiusCommandArgs
        return results;
    }

    /***
     * 以给定的城市为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素,并给出所有位置元素与中心的平均距离。
     * @param key redis的key
     * @param name 名称
     * @param distance 距离
     * @param count 人数
     * @return
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> geoNearByPlace(String key, String name, Integer distance, Integer count) {
     
        Distance distances = new Distance(distance, Metrics.KILOMETERS);//params: 距离量, 距离单位
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(count);
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo()
                .radius(key, name, distances, args);//params: key, 地方名称, Circle, GeoRadiusCommandArgs
        return results;
    }


    /***
     * 返回一个或多个位置元素的 Geohash 表示
     * @param key redis的key
     * @param nameList  名称的集合
     */
    public List<String> geoHash(String key, List<String> nameList) {
     
        List<String> results = redisTemplate.opsForGeo().hash(key, nameList);//params: key, 地方名称...
        return results;
    }


}

GeoHashUtil的使用详情请查看:

https://blog.csdn.net/weixin_41677422/article/details/108260110

总结

分页实现思路:将geo集合中的数据按距离由近到远筛选好后,通过storedist放入一个新的Zset集合,然后通过zset的语法实现分页。

你可能感兴趣的:(java语言,java语言,redis,java)