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

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

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


前言

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

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

特点:

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

快速投入市场去运营

收集用户的经纬度:

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

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

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

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

相关推荐

VPS主机搭建Ghost环境:Nginx Node.js MariaDB

Ghost是一款个人博客系统,它是使用Node.js语言和MySQL数据库开发的,同时支持MySQL、MariaDB、SQLite和PostgreSQL。用户可以在支持Node.js的服务器上使用自己...

centos7飞速搭建zabbix5.0并添加windows、linux监控

一、环境zabbix所在服务器系统为centos7,监控的服务器为windows2016和centos7。二、安装zabbix官方安装帮助页面...

Zabbix5.0安装部署

全盘展示运行状态,减轻运维人员的重复性工作量,提高系统排错速度,加速运维知识学习积累。1.png...

MariaDB10在CentOS7系统下,迁移数据存储位置

背景在CentOS7下如果没有默认安装MySQL数据库,可以选择安装MariaDB,最新的版本现在是10可以选择直接yum默认安装的方式yum-yinstallmariadbyum-yi...

frappe项目安装过程

1,准备一台虚拟机,debian12或者ubuntusever22.04.3可以用virtualbox/qemu,或者你的超融合服务器安装一些常用工具和依赖库我这里选择server模式安装,用tab...

最新zabbix一键安装脚本(基于centos8)

一、环境准备注意:操作系统必须是centos8及以上的,因为我配的安装源是centos8的。并且必须连接互联网,脚本是基于yum安装的!!!...

ip地址管理之phpIPAM保姆级安装教程 (原创)

本教程基于Ubuntu24.04LTS,安装phpIPAM(最新稳定版1.7),使用Apache、PHP8.3和MariaDB,遵循最佳实践,确保安全性和稳定性。一、环境准备1....

centos7傻瓜式安装搭建zabbix5.0监控服务器教程

zabbix([`zaebiks])是一个基于WEB界面的提供分布式系统监视...

zabbix7.0LTS 保姆级安装教程 小白也能轻松上手安装

系统环境:rockylinux9.4(yumupdate升级到最新版本)数据库:mariadb10.5.22第一步:关闭防火墙和selinux使用脚本关闭...

ubuntu通过下载安装包安装mariadb10.4

要在Ubuntu18.04上安装MariaDB10.4.34,用的是那个tar.gz的安装包。步骤大概是:...

从0到1:基于 Linux 快速搭建高可用 MariaDB Galera 集群(实战指南)

在企业生产环境中,数据库的高可用性至关重要。今天带你从0到1,手把手在Linux系统上快速搭建一个高可用MariaDBGaleraCluster,实现数据库同步复制、故障自动恢复,保障业务...

Windows 中安装 MariaDB 数据库

mariadb在Windows下的安装非常简单,下载程序双击运行就可以了。需要注意:mariadb和MySQL数据库在Windows下默认是不区分大小写的,但是在Linux下是区分...

SQL执行顺序(SqlServer)

学习SQL这么久,如果突然有人问你SQL的执行顺是怎么样的?是不是很多人会觉得C#、JavaScript都是根据编程顺序来处理的,那么SQL也是根据编程顺序来执行的吗?...

C# - StreamWriter与StreamReader 读写文件 101

读写文本文件的方式:1)File静态类的File.ReadAllLines();与File.WriteAllLines();方法进行读写...

C#中的数组探究与学习

C#中的数组一般分为:...