百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

Spring Boot实战分页查询附近的人:Redis+GeoHash+Lua

wptr33 2025-01-31 15:37 17 浏览


前言

最近在做社交的业务,用户进入首页后需要查询附近的人;

项目状况:前期尝试业务阶段;

特点:

快速实现(不需要做太重,满足初期推广运营即可)

快速投入市场去运营

收集用户的经纬度:

用户在每次启动时将当前的地理位置(经度,维度)上报给后台

提到附近的人,脑海中首先浮现特点:

需要记录每位用户的经纬度

查询当前用户附近的人,搜索在N公里内用户

架构设计

  • 时序图


  • 技术实现方案

SpringBoot

Redis(version>=3.2)

Redis原生命令实现

  • 存入用户的经纬度

1.geoadd 用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中

2.命令格式:

GEOADD?key?longitude?latitude?member?[longitude?latitude?member?...]

3.模拟五个用户存入经纬度,redis客户端执行如下命令:

GEOADD?zhgeo?116.48105?39.996794?zhangsan
GEOADD?zhgeo?116.514203?39.905409?lisi
GEOADD?zhgeo?116.489033?40.007669?wangwu
GEOADD?zhgeo?116.562108?39.787602?sunliu
GEOADD?zhgeo?116.334255?40.027400?zhaoqi

4.通过redis客户端查看效果:


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

1.georadiusbymember可以找出位于指定范围内的元素,georadiusbymember 的中心点是由给定的位置元素决定的

2.命令格式:

GEORADIUSBYMEMBER?key?member?radius?m|km|ft|mi?[WITHCOORD]?[WITHDIST]?[WITHHASH]?[COUNT?count]?[ASC|DESC]?[STORE?key]?[STOREDIST?key]

3.模拟查找100km里距离sunliu由近到远五个人

 georadiusbymember?zhgeo?sunliu?100?km?asc?count?5

4.命令执行效果如下:


  • 如何实现分页查询那?

1.每次分页查询的请求都计算一次然后拿到程序中在取相应的分页数据,优缺点:

(1)优点:实现简单,无需额外的存储空间

(2)缺点:当用户量大时,很显然不仅效率低,而且容易把程序的内存搞溢出

2.经过查找发现redis的github官网给出了分页Issues(参考:Will the GEORADIUS support pagination?),解决方案如下:

(1)利用GEORADIUSBYMEMBER 命令中的 STOREDIST 将排好序的数据存入一个Zset集合中,以后分页查直接从Zset

(2)命令如下:

georadiusbymember?zhgeo?sunliu?100?km?asc?count?5?storedist?sunliu 

(3)有序集合效果如下:

(4)以后分页查询命令:

//首先删除本身元素
zrem?sunliu?sunliu
//分页查找元素(在此以:查找第1页,每页数量是3为例)
zrange?sunliu?0?2?withscores 

(5)效果如下:

代码实现

  • 完整代码(GitHub,欢迎大家Star,Fork,Watch) https://github.com/dangnianchuntian/springboot
  • 主要代码展示
  • Controller
??/*
???*?Copyright?(c)?2020.?zhanghan_java@163.com?All?Rights?Reserved.
???*?项目名称:Spring?Boot实战分页查询附近的人:?Redis+GeoHash+Lua
???*?类名称:GeoController.java
???*?创建人:张晗
???*?联系方式:zhanghan_java@163.com
???*?开源地址:?https://github.com/dangnianchuntian/springboot
???*?博客地址:?https://zhanghan.blog.csdn.net
???*/

??package?com.zhanghan.zhnearbypeople.controller;

??import?org.springframework.beans.factory.annotation.Autowired;
??import?org.springframework.validation.annotation.Validated;
??import?org.springframework.web.bind.annotation.RequestBody;
??import?org.springframework.web.bind.annotation.RequestMapping;
??import?org.springframework.web.bind.annotation.RequestMethod;
??import?org.springframework.web.bind.annotation.RestController;

??import?com.zhanghan.zhnearbypeople.controller.request.ListNearByPeopleRequest;
??import?com.zhanghan.zhnearbypeople.controller.request.PostGeoRequest;
??import?com.zhanghan.zhnearbypeople.service.GeoService;

??@RestController
??public?class?GeoController?{

??????@Autowired
??????private?GeoService?geoService;

??????/**
???????*?记录用户地理位置
???????*/
??????@RequestMapping(value?=?"/post/geo",?method?=?RequestMethod.POST)
??????public?Object?postGeo(@RequestBody?@Validated?PostGeoRequest?postGeoRequest)?{
??????????return?geoService.postGeo(postGeoRequest);
??????}

??????/**
???????*?分页查询当前用户附近的人
???????*/
??????@RequestMapping(value?=?"/list/nearby/people",?method?=?RequestMethod.POST)
??????public?Object?listNearByPeople(@RequestBody?@Validated?ListNearByPeopleRequest?listNearByPeopleRequest)?{
??????????return?geoService.listNearByPeople(listNearByPeopleRequest);
??????}

??}
  • service
??/*
???*?Copyright?(c)?2020.?zhanghan_java@163.com?All?Rights?Reserved.
???*?项目名称:Spring?Boot实战分页查询附近的人:?Redis+GeoHash+Lua
???*?类名称:GeoServiceImpl.java
???*?创建人:张晗
???*?联系方式:zhanghan_java@163.com
???*?开源地址:?https://github.com/dangnianchuntian/springboot
???*?博客地址:?https://zhanghan.blog.csdn.net
???*/

??package?com.zhanghan.zhnearbypeople.service.impl;

??import?java.util.ArrayList;
??import?java.util.List;
??import?java.util.Set;

??import?org.slf4j.Logger;
??import?org.slf4j.LoggerFactory;
??import?org.springframework.beans.factory.annotation.Autowired;
??import?org.springframework.beans.factory.annotation.Value;
??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.stereotype.Service;

??import?com.zhanghan.zhnearbypeople.controller.request.ListNearByPeopleRequest;
??import?com.zhanghan.zhnearbypeople.controller.request.PostGeoRequest;
??import?com.zhanghan.zhnearbypeople.dto.NearByPeopleDto;
??import?com.zhanghan.zhnearbypeople.service.GeoService;
??import?com.zhanghan.zhnearbypeople.util.RedisLuaUtil;
??import?com.zhanghan.zhnearbypeople.util.wrapper.WrapMapper;

??@Service
??public?class?GeoServiceImpl?implements?GeoService?{

??????private?static?Logger?logger?=?LoggerFactory.getLogger(GeoServiceImpl.class);

??????@Autowired
??????private?RedisTemplate?objRedisTemplate;

??????@Value("${zh.geo.redis.key:zhgeo}")
??????private?String?zhGeoRedisKey;

??????@Value("${zh.geo.zset.redis.key:zhgeozset:}")
??????private?String?zhGeoZsetRedisKey;

??????/**
???????*?记录用户访问记录
???????*/
??????@Override
??????public?Object?postGeo(PostGeoRequest?postGeoRequest)?{

??????????//对应redis原生命令:GEOADD?zhgeo?116.48105?39.996794?zhangsan
??????????Long?flag?=?objRedisTemplate.opsForGeo().add(zhGeoRedisKey,?new?RedisGeoCommands.GeoLocation<>(postGeoRequest
??????????????????.getCustomerId(),?new?Point(postGeoRequest.getLatitude(),?postGeoRequest.getLongitude())));

??????????if?(null?!=?flag?&&?flag?>?0)?{
??????????????return?WrapMapper.ok();
??????????}

??????????return?WrapMapper.error();
??????}

??????/**
???????*?分页查询附近的人
???????*/
??????@Override
??????public?Object?listNearByPeople(ListNearByPeopleRequest?listNearByPeopleRequest)?{

??????????String?customerId?=?listNearByPeopleRequest.getCustomerId();

??????????String?strZsetUserKey?=?zhGeoZsetRedisKey?+?customerId;

??????????List?nearByPeopleDtoList?=?new?ArrayList<>();

??????????//如果是从第1页开始查,则将附近的人写入zset集合,以后页直接从zset中查
??????????if?(1?==?listNearByPeopleRequest.getPageIndex())?{
??????????????List?scriptParams?=?new?ArrayList<>();
??????????????scriptParams.add(zhGeoRedisKey);
??????????????scriptParams.add(customerId);
??????????????scriptParams.add("100");
??????????????scriptParams.add(RedisGeoCommands.DistanceUnit.KILOMETERS.getAbbreviation());
??????????????scriptParams.add("asc");
??????????????scriptParams.add("storedist");
??????????????scriptParams.add(strZsetUserKey);

??????????????//用Lua脚本实现georadiusbymember中的storedist参数
??????????????//对应Redis原生命令:georadiusbymember?zhgeo?sunliu?100?km?asc?count?5?storedist?sunliu
??????????????Long?executeResult?=?objRedisTemplate.execute(RedisLuaUtil.GEO_RADIUS_STOREDIST_SCRIPT(),?scriptParams);

??????????????if?(null?==?executeResult?||?executeResult??listNearByPeopleFromZset(String?strZsetUserKey,?Integer?pageIndex,?Integer?pageSize)?{

??????????Integer?startPage?=?(pageIndex?-?1)?*?pageSize;
??????????Integer?endPage?=?pageIndex?*?pageSize?-?1;
??????????List?nearByPeopleDtoList?=?new?ArrayList<>();
??????????//对应Redis原生命令:zrange?key?0?2?withscores
??????????Set>?zsetUsers?=?objRedisTemplate.opsForZSet()
??????????????????.rangeWithScores(strZsetUserKey,?startPage,
??????????????????????????endPage);

??????????for?(ZSetOperations.TypedTuple?zsetUser?:?zsetUsers)?{
??????????????NearByPeopleDto?nearByPeopleDto?=?new?NearByPeopleDto();
??????????????nearByPeopleDto.setCustomerId(zsetUser.getValue().toString());
??????????????nearByPeopleDto.setDistance(zsetUser.getScore());
??????????????nearByPeopleDtoList.add(nearByPeopleDto);
??????????}

??????????return?nearByPeopleDtoList;
??????}
??}
  • RedisLuaUtil
??/*
???*?Copyright?(c)?2020.?zhanghan_java@163.com?All?Rights?Reserved.
???*?项目名称:Spring?Boot实战分页查询附近的人:?Redis+GeoHash+Lua
???*?类名称:RedisLuaUtil.java
???*?创建人:张晗
???*?联系方式:zhanghan_java@163.com
???*?开源地址:?https://github.com/dangnianchuntian/springboot
???*?博客地址:?https://zhanghan.blog.csdn.net
???*/

??package?com.zhanghan.zhnearbypeople.util;


??import?org.springframework.data.redis.core.script.DigestUtils;
??import?org.springframework.data.redis.core.script.RedisScript;

??public?class?RedisLuaUtil?{

??????private?static?final?RedisScript?GEO_RADIUS_STOREDIST_SCRIPT;

??????public?static?RedisScript?GEO_RADIUS_STOREDIST_SCRIPT()?{
??????????return?GEO_RADIUS_STOREDIST_SCRIPT;
??????}

??????static?{
??????????StringBuilder?sb?=?new?StringBuilder();
??????????sb.append("return?redis.call('georadiusbymember',KEYS[1],KEYS[2],KEYS[3],KEYS[4],KEYS[5],KEYS[6],KEYS[7])");
??????????GEO_RADIUS_STOREDIST_SCRIPT?=?new?RedisScriptImpl<>(sb.toString(),?Long.class);
??????}

??????private?static?class?RedisScriptImpl?implements?RedisScript?{
??????????private?final?String?script;
??????????private?final?String?sha1;
??????????private?final?Class?resultType;

??????????public?RedisScriptImpl(String?script,?Class?resultType)?{
??????????????this.script?=?script;
??????????????this.sha1?=?DigestUtils.sha1DigestAsHex(script);
??????????????this.resultType?=?resultType;
??????????}

??????????@Override
??????????public?String?getSha1()?{
??????????????return?sha1;
??????????}

??????????@Override
??????????public?Class?getResultType()?{
??????????????return?resultType;
??????????}

??????????@Override
??????????public?String?getScriptAsString()?{
??????????????return?script;
??????????}
??????}
??}

测试

  • 模拟用户上传地理位置进行存储

1.进行请求

2.查看效果

3.模拟用户sunliu查找附近100km的人,按3条一分页进行查询 进行请求

总结

  • 亮点:

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

2.redisTemplate没有针对原生命令georadiusbymember的storedist参数实现,灵活运用Lua脚本去实现

  • geo集合在亿级别以内的数据量没有问题,当超过亿后需要根据产品需要对Redis中的大值进行拆分,比如按照地域进行拆分等
  • 有了地理位置,自己正在研究如何通过经纬度绘制出自己的运动路线,验证出来后与大家共享

相关推荐

Python自动化脚本应用与示例(python办公自动化脚本)

Python是编写自动化脚本的绝佳选择,因其语法简洁、库丰富且跨平台兼容性强。以下是Python自动化脚本的常见应用场景及示例,帮助你快速上手:一、常见自动化场景文件与目录操作...

Python文件操作常用库高级应用教程

本文是在前面《Python文件操作常用库使用教程》的基础上,进一步学习Python文件操作库的高级应用。一、高级文件系统监控1.1watchdog库-实时文件系统监控安装与基本使用:...

Python办公自动化系列篇之六:文件系统与操作系统任务

作为高效办公自动化领域的主流编程语言,Python凭借其优雅的语法结构、完善的技术生态及成熟的第三方工具库集合,已成为企业数字化转型过程中提升运营效率的理想选择。该语言在结构化数据处理、自动化文档生成...

14《Python 办公自动化教程》os 模块操作文件与文件夹

在日常工作中,我们经常会和文件、文件夹打交道,比如将服务器上指定目录下文件进行归档,或将爬虫爬取的数据根据时间创建对应的文件夹/文件,如果这些还依靠手动来进行操作,无疑是费时费力的,这时候Pyt...

python中os模块详解(python os.path模块)

os模块是Python标准库中的一个模块,它提供了与操作系统交互的方法。使用os模块可以方便地执行许多常见的系统任务,如文件和目录操作、进程管理、环境变量管理等。下面是os模块中一些常用的函数和方法:...

21-Python-文件操作(python文件的操作步骤)

在Python中,文件操作是非常重要的一部分,它允许我们读取、写入和修改文件。下面将详细讲解Python文件操作的各个方面,并给出相应的示例。1-打开文件...

轻松玩转Python文件操作:移动、删除

哈喽,大家好,我是木头左!Python文件操作基础在处理计算机文件时,经常需要执行如移动和删除等基本操作。Python提供了一些内置的库来帮助完成这些任务,其中最常用的就是os模块和shutil模块。...

Python 初学者练习:删除文件和文件夹

在本教程中,你将学习如何在Python中删除文件和文件夹。使用os.remove()函数删除文件...

引人遐想,用 Python 获取你想要的“某个人”摄像头照片

仅用来学习,希望给你们有提供到学习上的作用。1.安装库需要安装python3.5以上版本,在官网下载即可。然后安装库opencv-python,安装方式为打开终端输入命令行。...

Python如何使用临时文件和目录(python目录下文件)

在某些项目中,有时候会有大量的临时数据,比如各种日志,这时候我们要做数据分析,并把最后的结果储存起来,这些大量的临时数据如果常驻内存,将消耗大量内存资源,我们可以使用临时文件,存储这些临时数据。使用标...

Linux 下海量文件删除方法效率对比,最慢的竟然是 rm

Linux下海量文件删除方法效率对比,本次参赛选手一共6位,分别是:rm、find、findwithdelete、rsync、Python、Perl.首先建立50万个文件$testfor...

Python 开发工程师必会的 5 个系统命令操作库

当我们需要编写自动化脚本、部署工具、监控程序时,熟练操作系统命令几乎是必备技能。今天就来聊聊我在实际项目中高频使用的5个系统命令操作库,这些可都是能让你效率翻倍的"瑞士军刀"。一...

Python常用文件操作库使用详解(python文件操作选项)

Python生态系统提供了丰富的文件操作库,可以处理各种复杂的文件操作需求。本教程将介绍Python中最常用的文件操作库及其实际应用。一、标准库核心模块1.1os模块-操作系统接口主要功能...

11. 文件与IO操作(文件io和网络io)

本章深入探讨Go语言文件处理与IO操作的核心技术,结合高性能实践与安全规范,提供企业级解决方案。11.1文件读写11.1.1基础操作...

Python os模块的20个应用实例(python中 import os模块用法)

在Python中,...