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

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

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


前言

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

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

特点:

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

快速投入市场去运营

收集用户的经纬度:

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

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

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

查询当前用户附近的人,搜索在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中的大值进行拆分,比如按照地域进行拆分等
  • 有了地理位置,自己正在研究如何通过经纬度绘制出自己的运动路线,验证出来后与大家共享

相关推荐

SQL轻松入门(5):窗口函数(sql语录中加窗口函数的执行)

01前言标题中有2个字让我在初次接触窗口函数时,真真切切明白了何谓”高级”?说来也是一番辛酸史!话说,我见识了窗口函数的强大后,便磨拳擦掌的要试验一番,结果在查询中输入语句,返回的结果却是报错,Wh...

28个SQL常用的DeepSeek提示词指令,码住直接套用

自从DeepSeek出现后,极大地提升了大家平时的工作效率,特别是对于一些想从事数据行业的小白,只需要掌握DeepSeek的提问技巧,SQL相关的问题也不再是个门槛。...

从零开始学SQL进阶,数据分析师必备SQL取数技巧,建议收藏

上一节给大家讲到SQL取数的一些基本内容,包含SQL简单查询与高级查询,需要复习相关知识的同学可以跳转至上一节,本节给大家讲解SQL的进阶应用,在实际过程中用途比较多的子查询与窗口函数,下面一起学习。...

SQL_OVER语法(sql语句over什么含义)

OVER的定义OVER用于为行定义一个窗口,它对一组值进行操作,不需要使用GROUPBY子句对数据进行分组,能够在同一行中同时返回基础行的列和聚合列。...

SQL窗口函数知多少?(sql窗口怎么执行)

我们在日常工作中是否经常会遇到需要排名的情况,比如:每个部门按业绩来排名,每人按绩效排名,对部门销售业绩前N名的进行奖励等。面对这类需求,我们就需要使用sql的高级功能——窗口函数。...

如何学习并掌握 SQL 数据库基础:从零散查表到高效数据提取

无论是职场数据分析、产品运营,还是做副业项目,掌握SQL(StructuredQueryLanguage)意味着你能直接从数据库中提取、分析、整合数据,而不再依赖他人拉数,节省大量沟通成本,让你...

SQL窗口函数(sql窗口函数执行顺序)

背景在数据分析中,经常会遇到按某某条件来排名、并找出排名的前几名,用日常SQL的GROUPBY,ORDERBY来实现特别的麻烦,有时甚至实现不了,这个时候SQL窗口函数就能发挥巨大作用了,窗...

sqlserver删除重复数据只保留一条,使用ROW_NUMER()与Partition By

1.使用场景:公司的小程序需要实现一个功能:在原有小程序上,有一个优惠券活动表。存储着活动产品数据,但因为之前没有做约束,导致数据的不唯一,这会使打开产品详情页时,可能会出现随机显示任意活动问题。...

SQL面试经典问题(一)(sql经典面试题及答案)

以下是三个精心挑选的经典SQL面试问题及其详细解决方案,涵盖了数据分析、排序限制和数据清理等常见场景。这些问题旨在考察SQL的核心技能,适用于初学者到高级开发者的面试准备。每个问题均包含清晰的...

SQL:求连续N天的登陆人员之通用解答

前几天发了一个微头条:...

SQL四大排序函数神技(sql中的排序是什么语句)

在日常SQL开发中,排序操作无处不在。当大家需要排序时,是否只会想到ORDERBY?今天,我们就来揭秘SQL中四个强大却常被忽略的排序函数:ROW_NUMBER()、RANK()、DENSE_RAN...

四、mysql窗口函数之row_number()函数的使用

1、窗口函数之row_number()使用背景窗口函数中,排序函数rank(),dense_rank()虽说都是排序函数,但是各有用处,假如像上章节说的“同组同分”两条数据,我们不想“班级名次”出现“...

ROW_NUMBER()函数(rownumber函数与rank区别)

ROW_NUMBER()是SQL中的一个窗口函数(WindowFunction)...

Dify「模板转换」节点终极指南:动态文本生成进阶技巧(附代码)Jinja2引擎解析

这篇文章是关于Dify「模板转换」节点的终极指南,解析了基于Jinja2模板引擎的动态文本生成技巧,涵盖多源文本整合、知识检索结构化、动态API构建及个性化内容生成等六大应用场景,助力开发者高效利用模...

Python 最常用的语句、函数有哪些?

1.#coding=utf-8①代码中有中文字符,最好在代码前面加#coding=utf-8②pycharm不加可能不会报错,但是代码最终是会放到服务器上,放到服务器上的时候运行可能会报错。③...