前言

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

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

特点:

  • 快速实现(不需要做太重,满足初期推广运营即可)
  • 快速投入市场去运营
  • 收集用户的经纬度:
  • 用户在每次启动时将当前的地理位置(经度,维度)上报给后台
  • 提到附近的人,脑海中首先浮现特点:
  • 需要记录每位用户的经纬度
  • 查询当前用户附近的人,搜索在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;@RestControllerpublic class GeoController {@Autowiredprivate 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;@Servicepublic class GeoServiceImpl implements GeoService {private static Logger logger = LoggerFactory.getLogger(GeoServiceImpl.class);@Autowiredprivate RedisTemplate<String, Object> objRedisTemplate;@Value("${zh.geo.redis.key:zhgeo}")private String zhGeoRedisKey;@Value("${zh.geo.zset.redis.key:zhgeozset:}")private String zhGeoZsetRedisKey;/*** 记录用户访问记录*/@Overridepublic Object postGeo(PostGeoRequest postGeoRequest) {//对应redis原生命令:GEOADD zhgeo 116.48105 39.996794 zhangsanLong 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();}/*** 分页查询附近的人*/@Overridepublic Object listNearByPeople(ListNearByPeopleRequest listNearByPeopleRequest) {String customerId = listNearByPeopleRequest.getCustomerId();String strZsetUserKey = zhGeoZsetRedisKey + customerId;List<NearByPeopleDto> nearByPeopleDtoList = new ArrayList<>();//如果是从第1页开始查,则将附近的人写入zset集合,以后页直接从zset中查if (1 == listNearByPeopleRequest.getPageIndex()) {List<String> 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 sunliuLong executeResult = objRedisTemplate.execute(RedisLuaUtil.GEO_RADIUS_STOREDIST_SCRIPT(), scriptParams);if (null == executeResult || executeResult < 1) {return WrapMapper.ok(nearByPeopleDtoList);}//zset集合中去除自己//对应Redis原生命令:zrem sunliu sunliuLong remove = objRedisTemplate.opsForZSet().remove(strZsetUserKey, customerId);}nearByPeopleDtoList = listNearByPeopleFromZset(strZsetUserKey, listNearByPeopleRequest.getPageIndex(),listNearByPeopleRequest.getPageSize());return WrapMapper.ok(nearByPeopleDtoList);}/*** 分页从zset中查询指定用户附近的人*/private List<NearByPeopleDto> listNearByPeopleFromZset(String strZsetUserKey, Integer pageIndex, Integer pageSize) {Integer startPage = (pageIndex - 1) * pageSize;Integer endPage = pageIndex * pageSize - 1;List<NearByPeopleDto> nearByPeopleDtoList = new ArrayList<>();//对应Redis原生命令:zrange key 0 2 withscoresSet<ZSetOperations.TypedTuple<Object>> zsetUsers = objRedisTemplate.opsForZSet().rangeWithScores(strZsetUserKey, startPage,endPage);for (ZSetOperations.TypedTuple<Object> 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<Long> GEO_RADIUS_STOREDIST_SCRIPT;public static RedisScript<Long> 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<T> implements RedisScript<T> {private final String script;private final String sha1;private final Class<T> resultType;public RedisScriptImpl(String script, Class<T> resultType) {this.script = script;this.sha1 = DigestUtils.sha1DigestAsHex(script);this.resultType = resultType;}@Overridepublic String getSha1() {return sha1;}@Overridepublic Class<T> getResultType() {return resultType;}@Overridepublic String getScriptAsString() {return script;}}}

测试

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

1.进行请求

2.查看效果

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

总结

  • 亮点:

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

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

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

作者:hank
原文:http://toutiao.com/i6861984693340865036

springboot controller 分页查询_Spring Boot实战分页查询附近的人:Redis+GeoHash+Lua相关推荐

  1. springboot controller 分页查询_Spring Boot实战分页查询附近的人: Redis+GeoHash+Lua

    您的支持是我不断创作巨大动力 CSDN博客地址(关注,点赞) 人工智能推荐 GitHub(Star,Fork,Watch) 前言 最近在做社交的业务,用户进入首页后需要查询附近的人: 项目状况:前期尝 ...

  2. Spring Boot实战解决高并发数据入库: Redis 缓存+MySQL 批量入库

    前言 最近在做阅读类的业务,需要记录用户的PV,UV: 项目状况:前期尝试业务阶段: 特点: 快速实现(不需要做太重,满足初期推广运营即可) 快速投入市场去运营 收集用户的原始数据,三要素: 谁 在什 ...

  3. springboot 事务统一配置_Spring Boot实现分布式微服务开发实战系列(五)

    最近已经推出了好几篇SpringBoot+Dubbo+Redis+Kafka实现电商的文章,今天再次回到分布式微服务项目中来,在开始写今天的系列五文章之前,我先回顾下前面的内容. 系列(一):主要说了 ...

  4. springboot 后台模板_spring boot实战

    亲们,我们今天来看看spring boot.如果你掌握了Servlet.JSP.Filter.Listener等web开发技术,掌握了Spring MVC.Spring.Mybatis框架的使用,掌握 ...

  5. springboot map数据类型注入_Spring Boot(五):春眠不觉晓,Mybatis知多少

    在JavaWeb项目开发中,我们使用最多的ORM框架可能就是Mybatis了,那么对于常用的mybatis,你究竟了解多少呢? 一 全局了解 1 Mybatis是什么 MyBatis 是支持定制化 S ...

  6. springboot 订单重复提交_Spring Boot (一) 校验表单重复提交

    一.前言 在某些情况下,由于网速慢,用户操作有误(连续点击两下提交按钮),页面卡顿等原因,可能会出现表单数据重复提交造成数据库保存多条重复数据. 存在如上问题可以交给前端解决,判断多长时间内不能再次点 ...

  7. springboot 数据库假面_Spring Boot实战:数据库操作

    上篇文章中已经通过一个简单的HelloWorld程序讲解了Spring boot的基本原理和使用.本文主要讲解如何通过spring boot来访问数据库,本文会演示三种方式来访问数据库,第一种是Jdb ...

  8. springboot模板引擎_Spring Boot实战:如何搞定前端模板引擎?

    作者:liuxiaopeng 链接:https://www.cnblogs.com/paddix/p/8905531.html 前言 虽然现在很多开发,都采用了前后端完全分离的模式,即后端只提供数据接 ...

  9. springboot使用netty容器_Spring Boot 2 实战:使用 Undertow 来替代Tomcat

    1. Undertow 简介 Undertow 是一个采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制.Undertow 是红帽公司的开源产品,是 Wild ...

最新文章

  1. 专访思必驰初敏:离开微软、放弃阿里,一个语音交互的“实用主义者”
  2. 几个步骤,让你的 iOS 代码容易阅读
  3. 商丘网络推广中网站内部优化需要注意的要点有哪些?
  4. 电脑开机3秒就重启循环_移动办公神器!电脑包秒变支架、鼠标垫,3合1设计超方便!...
  5. Qt 之 消息机制和事件讲解
  6. unity中链接字符串和变量显示_理解Unity中的优化(六):字符串和文本
  7. vim、gvim在windows下中文乱码的终极解决方案
  8. MYSQL性能优化详解(二)
  9. js删除数组中的某个对象
  10. Android nfc编译,【Android编译】各个模块编译方法
  11. C标准函数库中获取时间与日期、对时间与日期数据操作及格式化
  12. imx6 android6.0.1,mfgtool刷写i.MX6 android6.0版本失败
  13. 【文本摘要】BottleSum——文本摘要论文系列解读
  14. 微信小程序报错40163-“errmsg“解决方案
  15. 交换机基础知识 - 从零开始学习
  16. Rstudio插入Excel数据
  17. cenos7开启SMB文件共享,小米摄像头无法搜索到的原因
  18. maven报错问题汇总
  19. 转给你身边的工程师!从零开始搭建一个完整AGV控制系统
  20. [COGS2189][HZOI 2015]帕秋莉的超级多项式-NTT-多项式求逆-多项式求ln-多项式开方-多项式求exp-多项式快速幂

热门文章

  1. python 代理ip验证_Python 快速验证代理IP是否有效,再也不用担心被封IP了!
  2. java程序设计_80后程序员,带你深入理解Java基本的程序设计结构,不来你别后悔...
  3. insert 语句_CTF从入门到提升(七)insert 等数据表相关操作注入及例题分享
  4. html5 div css 页签,div css 实现tabs标签的思路及示例代码
  5. mysql查询大量数据报错_mysql 查询大量数据报错
  6. 鼠标控制视角wasd移动_无线款,轻量级,稳控制:雷蛇(Razer) 毒蝰终极版鼠标的快速体验...
  7. b+树时间复杂度_阿里面试,问了B+树,这个回答让我通过了
  8. initrd.img解压和压缩
  9. python字典功能默写_Python 内存分配时的小秘密
  10. centos7查看当前系统时间、_CentOS7 设置系统时间