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

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

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


前言

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

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

特点:

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

快速投入市场去运营

收集用户的经纬度:

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

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

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

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

相关推荐

MySQL进阶五之自动读写分离mysql-proxy

自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...

Postgres vs MySQL_vs2022连接mysql数据库

...

3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?

引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...

一文由浅入深带你完全掌握MySQL的锁机制原理与应用

本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...

验证Mysql中联合索引的最左匹配原则

后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...

MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)

目录1.索引基础...

你会看 MySQL 的执行计划(EXPLAIN)吗?

SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...

MySQL 从入门到精通(四)之索引结构

索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...

mysql总结——面试中最常问到的知识点

mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...

mysql总结——面试中最常问到的知识点(2)

首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...

MySQL基础全知全解!超详细无废话!轻松上手~

本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...

深入剖析 MySQL 中的锁机制原理_mysql 锁详解

在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...

Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析

引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...

MySQL基础篇:DQL数据查询操作_mysql 查

一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...

MySql:索引的基本使用_mysql索引的使用和原理

一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...