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

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

强烈推荐一个大神的人工智能的教程:http://www.captainai.net/zhanghan


前言


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

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


特点:

  • 快速实现(不需要做太重,满足初期推广运营即可)
  • 快速投入市场去运营


收集用户的经纬度:

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


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

  • 需要记录每位用户的经纬度
  • 查询当前用户附近的人,搜索在N公里内用户


架构设计


  • 时序图

a73d92eab85c6cb85b23883ce994d8be_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI4MjkxMjQ=,size_16,color_FFFFFF,t_70#pic_center.png


  • 技术实现方案


SpringBoot


Redis(version>=3.2)


Redis原生命令实现


  • 存入用户的经纬度


  • geoadd 用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中


  • 命令格式:


GEOADD key longitude latitude member [longitude latitude member ...]


  • 模拟五个用户存入经纬度,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


  • 通过redis客户端查看效果:

20200809180623685.png


  • 查找距当前用户由近到远附近100km用户


  • georadiusbymember可以找出位于指定范围内的元素,georadiusbymember 的中心点是由给定的位置元素决定的
  • 命令格式:
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]


  • 模拟查找100km里距离sunliu由近到远五个人
georadiusbymember zhgeo sunliu 100 km asc count 5


  • 命令执行效果如下:

20200809180856647.png


  • 如何实现分页查询那?


  • 每次分页查询的请求都计算一次然后拿到程序中在取相应的分页数据,优缺点:


  • 优点:实现简单,无需额外的存储空间
  • 缺点:当用户量大时,很显然不仅效率低,而且容易把程序的内存搞溢出
  • 经过查找发现redis的github官网给出了分页Issues(参考:Will the GEORADIUS support pagination?),解决方案如下:


  • 利用GEORADIUSBYMEMBER 命令中的 STOREDIST 将排好序的数据存入一个Zset集合中,以后分页查直接从Zset


  • 命令如下:


georadiusbymember zhgeo sunliu 100 km asc count 5 storedist sunliu


  • 有序集合效果如下:

20200809180939803.png


  • 以后分页查询命令:


//首先删除本身元素
zrem sunliu sunliu
//分页查找元素(在此以:查找第1页,每页数量是3为例)
zrange sunliu 0 2 withscores


  • 效果如下:


20200809181007105.png


代码实现


  • 完整代码(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<String, Object> 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<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 sunliu
            Long executeResult = objRedisTemplate.execute(RedisLuaUtil.GEO_RADIUS_STOREDIST_SCRIPT(), scriptParams);
            if (null == executeResult || executeResult < 1) {
                return WrapMapper.ok(nearByPeopleDtoList);
            }
            //zset集合中去除自己
            //对应Redis原生命令:zrem sunliu sunliu
            Long 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 withscores
        Set<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;
        }
        @Override
        public String getSha1() {
            return sha1;
        }
        @Override
        public Class<T> getResultType() {
            return resultType;
        }
        @Override
        public String getScriptAsString() {
            return script;
        }
    }
}


测试


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


  • 进行请求

6197184373c2042503b76aec8f0917ad_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI4MjkxMjQ=,size_16,color_FFFFFF,t_70#pic_center.png


  • 查看效果

20200809181457887.png

2020080918155012.png

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

7e26db4ad1a2ed548f34b0205c7566bd_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI4MjkxMjQ=,size_16,color_FFFFFF,t_70#pic_center.png


总结


  • 亮点:
  • 分页实现思路:将geo集合中的数据按距离由近到远筛选好后,通过storedist放入一个新的Zset集合
  • redisTemplate没有针对原生命令georadiusbymember的storedist参数实现,灵活运用Lua脚本去实现
  • geo集合在亿级别以内的数据量没有问题,当超过亿后需要根据产品需要对Redis中的大值进行拆分,比如按照地域进行拆分等
  • 有了地理位置,自己正在研究如何通过经纬度绘制出自己的运动路线,验证出来后与大家共享

相关文章
|
3月前
|
NoSQL Java 网络安全
SpringBoot启动时连接Redis报错:ERR This instance has cluster support disabled - 如何解决?
通过以上步骤一般可以解决由于配置不匹配造成的连接错误。在调试问题时,一定要确保服务端和客户端的Redis配置保持同步一致。这能够确保SpringBoot应用顺利连接到正确配置的Redis服务,无论是单机模式还是集群模式。
363 5
|
4月前
|
存储 NoSQL 前端开发
Redis专题-实战篇一-基于Session和Redis实现登录业务
本项目基于SpringBoot实现黑马点评系统,涵盖Session与Redis两种登录方案。通过验证码登录、用户信息存储、拦截器校验等流程,解决集群环境下Session不共享问题,采用Redis替代Session实现数据共享与自动续期,提升系统可扩展性与安全性。
297 3
Redis专题-实战篇一-基于Session和Redis实现登录业务
|
4月前
|
存储 缓存 NoSQL
Redis专题-实战篇二-商户查询缓存
本文介绍了缓存的基本概念、应用场景及实现方式,涵盖Redis缓存设计、缓存更新策略、缓存穿透问题及其解决方案。重点讲解了缓存空对象与布隆过滤器的使用,并通过代码示例演示了商铺查询的缓存优化实践。
236 1
Redis专题-实战篇二-商户查询缓存
|
4月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
331 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
7月前
|
缓存 监控 NoSQL
Redis 实操要点:Java 最新技术栈的实战解析
本文介绍了基于Spring Boot 3、Redis 7和Lettuce客户端的Redis高级应用实践。内容包括:1)现代Java项目集成Redis的配置方法;2)使用Redisson实现分布式可重入锁与公平锁;3)缓存模式解决方案,包括布隆过滤器防穿透和随机过期时间防雪崩;4)Redis数据结构的高级应用,如HyperLogLog统计UV和GeoHash处理地理位置。文章提供了详细的代码示例,涵盖Redis在分布式系统中的核心应用场景,特别适合需要处理高并发、分布式锁等问题的开发场景。
497 42
|
6月前
|
NoSQL Java Redis
Redis基本数据类型及Spring Data Redis应用
Redis 是开源高性能键值对数据库,支持 String、Hash、List、Set、Sorted Set 等数据结构,适用于缓存、消息队列、排行榜等场景。具备高性能、原子操作及丰富功能,是分布式系统核心组件。
626 2
|
7月前
|
机器学习/深度学习 存储 NoSQL
基于 Flink + Redis 的实时特征工程实战:电商场景动态分桶计数实现
本文介绍了基于 Flink 与 Redis 构建的电商场景下实时特征工程解决方案,重点实现动态分桶计数等复杂特征计算。通过流处理引擎 Flink 实时加工用户行为数据,结合 Redis 高性能存储,满足推荐系统毫秒级特征更新需求。技术架构涵盖状态管理、窗口计算、Redis 数据模型设计及特征服务集成,有效提升模型预测效果与系统吞吐能力。
793 10
|
7月前
|
缓存 NoSQL 算法
高并发秒杀系统实战(Redis+Lua分布式锁防超卖与库存扣减优化)
秒杀系统面临瞬时高并发、资源竞争和数据一致性挑战。传统方案如数据库锁或应用层锁存在性能瓶颈或分布式问题,而基于Redis的分布式锁与Lua脚本原子操作成为高效解决方案。通过Redis的`SETNX`实现分布式锁,结合Lua脚本完成库存扣减,确保操作原子性并大幅提升性能(QPS从120提升至8,200)。此外,分段库存策略、多级限流及服务降级机制进一步优化系统稳定性。最佳实践包括分层防控、黄金扣减法则与容灾设计,强调根据业务特性灵活组合技术手段以应对高并发场景。
2058 7
|
7月前
|
机器学习/深度学习 数据采集 人机交互
springboot+redis互联网医院智能导诊系统源码,基于医疗大模型、知识图谱、人机交互方式实现
智能导诊系统基于医疗大模型、知识图谱与人机交互技术,解决患者“知症不知病”“挂错号”等问题。通过多模态交互(语音、文字、图片等)收集病情信息,结合医学知识图谱和深度推理,实现精准的科室推荐和分级诊疗引导。系统支持基于规则模板和数据模型两种开发原理:前者依赖人工设定症状-科室规则,后者通过机器学习或深度学习分析问诊数据。其特点包括快速病情收集、智能病症关联推理、最佳就医推荐、分级导流以及与院内平台联动,提升患者就诊效率和服务体验。技术架构采用 SpringBoot+Redis+MyBatis Plus+MySQL+RocketMQ,确保高效稳定运行。
573 0
|
存储 缓存 NoSQL
Spring Boot2.5 实战 MongoDB 与高并发 Redis 缓存|学习笔记
快速学习 Spring Boot2.5 实战 MongoDB 与高并发 Redis 缓存
Spring Boot2.5 实战 MongoDB 与高并发 Redis 缓存|学习笔记