前言
最近在做社交的業務,用戶進入首頁后需要查詢附近的人;
項目狀況:前期嘗試業務階段;
特點:
- 快速實現(不需要做太重,滿足初期推廣運營即可)
- 快速投入市場去運營
- 收集用戶的經緯度:
- 用戶在每次啟動時將當前的地理位置(經度,維度)上報給后臺
- 提到附近的人,腦海中首先浮現特點:
- 需要記錄每位用戶的經緯度
- 查詢當前用戶附近的人,搜索在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客戶端查看效果:
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);}}
/** 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;}}
/** 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的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。