SpringBoot与Redis实现附近的人功能:快速社交业务开发指南
前言

最近在做社交的业务,用户进入首页后需要查询附近的人;
项目状况:前期尝试业务阶段;
特点:
快速实现(不需要做太重,满足初期推广运营即可)
快速投入市场去运营
收集用户的经纬度:
用户在每次启动时将当前的地理位置(经度,维度)上报给后台
提到附近的人,脑海中首先浮现特点:
需要记录每位用户的经纬度
查询当前用户附近的人,搜索在N公里内用户
架构设计
SpringBoot
Redis(version>=3.2)
Redis原生命令实现
geoadd 可用来存储特定的地理空间位置,它能把一个或多个经度、纬度以及位置名称添加到指定的 key 里。
2.命令格式:
在 key 中添加地理位置信息,包括经度、纬度和成员,还可以有更多的经度、纬度和成员。
3.模拟五个用户存入经纬度,redis客户端执行如下命令:
在 zhgeo 这个位置,经度为 116.48105,纬度为 39.996794,名为 zhangsan 的地点被添加进去。
在 zhgeo 这个地方添加一个坐标为 116.514203、39.905409 的点,名为 lisi。
在 zhgeo 中添加一个坐标点,其经度为 116.489033,纬度为 40.007669,名称为 wangwu。
在 zhgeo 这个位置添加坐标为 116.562108 和 39.787602 的 sunliu 数据。
在 zhgeo 这个地方添加一个坐标为 116.334255, 40.027400 的点,这个点的名称是 zhaoqi。
4.通过redis客户端查看效果:
georadiusbymember 能够找出处于指定范围之内的元素,其中心点是由所给定的位置元素来决定的。
2.命令格式:
GEORADIUSBYMEMBER 表示通过成员来获取地理半径相关的操作。它需要指定 key 以及成员 member ,还需要指定半径的单位,如 m|km|ft|mi 等。可以选择使用 WITHCOORD 来获取坐标信息,使用 WITHDIST 来获取距离信息,使用 WITHHASH 来获取哈希值。还可以指定 COUNT 以及 count 来限制返回的数量,使用 ASC 或 DESC 来指定排序顺序,使用 STORE 以及 key 来存储结果,使用 STOREDIST 以及 key 来存储距离信息。
3.模拟查找100km里距离sunliu由近到远五个人
在 zhgeo 中,使用 georadiusbymember 命令,以 sunliu 为中心,半径 100km 进行范围查询,按照升序排列,获取前 5 个结果。
4.命令执行效果如下:
每次分页查询的请求都会进行一次计算,接着将其拿到程序中,然后获取相应的分页数据。优缺点如下:
(1)优点:实现简单,无需额外的存储空间
缺点在于,当用户数量较大的时候,很明显会出现效率低下的情况,同时也容易导致程序的内存被耗尽而溢出。
查找后发现,redis 的 github 官网给出了分页 Issues,参考为:Will the GEORADIUS support pagination? 解决方案如下:
利用 GEORADIUSBYMEMBER 命令里的 STOREDIST,把排好序的数据存入一个 Zset 集合里,之后进行分页查询时直接从 Zset 进行操作。
(2)命令如下:
使用 georadiusbymember 命令,在 zhgeo 中以 sunliu 为中心,半径 100km 进行搜索,按照升序排列,返回前 5 个结果,并存储距离信息为 sunliu
(3)有序集合效果如下:
(4)以后分页查询命令:
//首先删除本身元素
zrem sunliu sunliu
//分页查找元素(在此以:查找第1页,每页数量是3为例)
zrange sunliu 0 2 withscores
(5)效果如下:
代码实现
/*
Copyright (c) 2020. 此为 zhanghan_java@163.com 的所有权利均已保留。
项目名称为 Spring Boot 实战分页查询附近的人,使用了 Redis、GeoHash 和 Lua 技术。
* 类名称:GeoController.java
* 创建人:张晗
联系方式为 zhanghan_java@163.com 。
开源的地址为:https://github.com/dangnianchuntian/springboot
博客的地址是 https://zhanghan.blog.csdn.net
*/
com.zhanghan.zhnearbypeople.controller 这个包;这个包是属于 com.zhanghan.zhnearbypeople 这个范围下的控制器部分;它在 com.zhanghan.zhnearbypeople 这个体系中承担着特定的控制功能。
在代码中使用了“import org.springframework.beans.factory.annotation.Autowired;”这一语句,它用于引入 Spring 框架中的自动装配注解相关的类和功能。通过该语句,可以在后续的代码中方便地使用自动装配的特性,无需手动创建和管理对象的依赖关系,提高了开发效率和代码的可维护性。
引入了 org.springframework.validation.annotation.Validated 这个类。
引入 org.springframework.web.bind.annotation.RequestBody;
在代码中引入了 org.springframework.web.bind.annotation.RequestMapping 这个类或包。 它用于处理 HTTP 请求与方法之间的映射关系。 通过这个注解,可以定义特定的 URL 路径与相应的处理方法进行关联。 这样在接收到特定的 HTTP 请求时,就能够准确地调用对应的处理逻辑。 它在 Spring Web 框架中起着重要的作用,帮助实现 Web 应用程序的请求处理和路由功能。
引入 org.springframework.web.bind.annotation.RequestMethod;
引入 org.springframework.web.bind.annotation.RestController 类。
导入 com.zhanghan.zhnearbypeople.controller.request 包下的 ListNearByPeopleRequest 类。
导入位于 com.zhanghan.zhnearbypeople.controller.request 路径下的 PostGeoRequest 类;
引入 com.zhanghan.zhnearbypeople.service.GeoService 类。
@RestController
公共类 GeoController 。
@Autowired
创建了一个名为 geoService 的私有 GeoService 对象。
/**
* 记录用户地理位置
*/
通过@RequestMapping 来指定路径为"/post/geo",并且指定请求方法为 RequestMethod.POST 。
即通过传入的包含有效数据的 PostGeoRequest 对象来执行地理相关的操作并返回一个 Object 对象。
调用 geoService 的 postGeo 方法并传入 postGeoRequest ,然后返回该方法的结果。
}
/**
* 分页查询当前用户附近的人
*/
通过@RequestMapping 来指定路径为"/list/nearby/people",并且使用 RequestMethod.POST 作为请求方法。
调用 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
*/
在 com.zhanghan.zhnearbypeople 这个包下的服务实现类中。
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
引入 org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
在 Java 中,使用了 `import` 关键字来导入相关的类或包。这里导入的是 `org.springframework.beans.factory.annotation.Value` 这个类,它用于在 Spring 框架中进行依赖注入等相关操作。
引入 org.springframework.data.geo 包中的 Point 类。
导入 org.springframework.data.redis.connection.RedisGeoCommands 类。
引入 org.springframework.data.redis.core 包中的 RedisTemplate 类。
引入 org.springframework.data.redis.core 包中的 ZSetOperations 类。
引入了 org.springframework.stereotype.Service;使用了 org.springframework.stereotype.Service 相关的内容;包含了 org.springframework.stereotype.Service 这一要素;将 org.springframework.stereotype.Service 进行了引入;利用了 org.springframework.stereotype.Service ;具备了 org.springframework.stereotype.Service 这一特性;对 org.springframework.stereotype.Service 进行了相关操作;涉及到了 org.springframework.stereotype.Service ;与 org.springframework.stereotype.Service 相关联;包含有 org.springframework.stereotype.Service ;运用了 org.springframework.stereotype.Service ;有 org.springframework.stereotype.Service 的参与;引入了关于 org.springframework.stereotype.Service 的部分;利用了关于 org.springframework.stereotype.Service 的内容;与 org.springframework.stereotype.Service 有着关联;包含着 org.springframework.stereotype.Service 这一元素;对 org.springframework.stereotype.Service 作出了相关处理;涉及到关于 org.springframework.stereotype.Service 的方面;与 org.springframework.stereotype.Service 存在关联;包含了与 org.springframework.stereotype.Service 相关的部分;利用了与 org.springframework.stereotype.Service 相关的内容;与 org.springframework.stereotype.Service 有一定的联系;包含有与 org.springframework.stereotype.Service 相关的元素;对与 org.springframework.stereotype.Service 相关的内容进行了操作;涉及到与 org.springframework.stereotype.Service 相关的方面;与 org.springframework.stereotype.Service 保持着关联;包含着与 org.springframework.stereotype.Service 相关的要素;对与 org.springframework.stereotype.Service 相关的部分进行了处理;利用了与 org.springframework.stereotype.Service 相关的特性;与 org.springframework.stereotype.Service 存在着一定的联系;包含有与 org.springframework.stereotype.Service 相关的特性元素。
import com.zhanghan.zhnearbypeople.controller.request.ListNearByPeopleRequest;
import com.zhanghan.zhnearbypeople.controller.request.PostGeoRequest;
导入 com.zhanghan.zhnearbypeople.dto 包下的 NearByPeopleDto 类。
import com.zhanghan.zhnearbypeople.service.GeoService;
引入 com.zhanghan.zhnearbypeople.util 包下的 RedisLuaUtil 类。
引入 com.zhanghan.zhnearbypeople.util.wrapper 包下的 WrapMapper 类。
@Service
公共类 GeoServiceImpl 实现了 GeoService 接口。
创建了一个名为 logger 的私有静态 Logger 实例,该实例是通过 LoggerFactory.getLogger(GeoServiceImpl.class) 方法获取的。
@Autowired
private RedisTemplate objRedisTemplate;
@Value 中设置的键值为 "${zh.geo.redis.key:zhgeo}" 。
private 这个字符串表示中文地理相关的 Redis 键。
@Value 的值为 "${zh.geo.zset.redis.key:zhgeozset:}"
private 这个字符串是 zhGeoZsetRedisKey;
/**
* 记录用户访问记录
*/
@Override
public Object 进行了 postGeo 操作,传入的参数是 PostGeoRequest 类型的 postGeoRequest 。
对应 redis 原生命令:将坐标 116.48105 和 39.996794 对应的位置添加到名为 zhgeo 的地理空间索引中,并标记为 zhangsan
Long flag 等于 objRedisTemplate 的 opsForGeo 方法添加到 zhGeoRedisKey 中,添加的是一个新的 RedisGeoCommands.GeoLocation<>(postGeoRequest
获取客户 ID,同时创建一个新的点对象,该点对象的纬度为 postGeoRequest 的纬度,经度为 postGeoRequest 的经度。
如果 flag 不为 null 并且 flag 大于 0 ,那么 {
返回 WrapMapper.ok() 。
}
返回 WrapMapper.error()。
}
/**
* 分页查询附近的人
*/
@Override
public Object 为列表附近人员请求执行列出附近人员的行为(ListNearByPeopleRequest listNearByPeopleRequest)。
获取的附近人员请求中的客户 ID 被赋值给了字符串 customerId 。
strZsetUserKey 等于 zhGeoZsetRedisKey 加上 customerId;
ListnearByPeopleDtoList 是一个新创建的 ArrayList 。<>();
如果从第 1 页开始查,就把附近的人写入 zset 集合;以后页直接从 zset 中查。
如果 1 等于 listNearByPeopleRequest 的页码索引(pageIndex),那么就执行以下操作;如果 1 不等于 listNearByPeopleRequest 的页码索引(pageIndex),则不执行以下操作。
List scriptParams = new ArrayList<>();
scriptParams 被添加了 zhGeoRedisKey 。
scriptParams 会添加 customerId 。
语言风格保持原文风格,去掉最前面的序号,将长句拆分成多个小分句,使分句尽量简短。比如:scriptParams 加上了“100”。
scriptParams 被添加了 RedisGeoCommands.DistanceUnit.KILOMETERS 的缩写。
scriptParams 被添加了 "asc" 。
将“storedist”添加到 scriptParams 中。
scriptParams 被添加了 strZsetUserKey 。
使用 Lua 脚本来实现 georadiusbymember 中的 storedist 参数。
最后返回结果
ObjRedisTemplate 执行了 RedisLuaUtil.GEO_RADIUS_STOREDIST_SCRIPT() 脚本,得到了 Long 类型的 executeResult,其参数为 scriptParams
如果 executeResult 为 null 或者 executeResult 为空,那么就会执行后续的逻辑。如果 executeResult 不为 null 且 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从给定的有序集合中列出靠近的人(包含页码索引和每页大小参数)。
整数 startPage 等于 (pageIndex 减 1) 乘以 pageSize ;
整数 endPage 等于 pageIndex 乘以 pageSize 再减去 1 。
List nearByPeopleDtoList = new ArrayList<>();
对应 Redis 原生命令为:从键 key 中获取索引从 0 到 2 的元素,并带有对应的分数值。
SetobjRedisTemplate 的 opsForZSet 方法被赋值给了 zsetUsers
使用 strZsetUserKey 来获取范围并获取相应的分数,起始页面为 startPage 。
以下是改写后的句子:语言风格保持原文风格,去掉最前面的序号,将长句拆分成多个小分句,分句越短越好,去掉 endPage) 。
对于(ZSetOperations.TypedTuple)
/*
* 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
*/
在 com.zhanghan.zhnearbypeople 这个包中,包含了一些工具类的代码。这些工具类的代码被用于各种不同的功能和操作。它们可以帮助开发者更方便地处理数据、进行计算或者实现其他特定的任务。
导入 org.springframework.data.redis.core.script 包下的 DigestUtils 类。
引入了 org.springframework.data.redis.core.script 包中的 RedisScript 类。
public class RedisLuaUtil {
改写后的句子:定义了一个具有私有属性、静态特性且不可修改的 RedisScript。 GEO_RADIUS_STOREDIST_SCRIPT;
公共静态的 RedisScript 。 它是一个用于在 Redis 中执行脚本的工具。 可以通过这个工具来定义和执行复杂的 Redis 操作脚本。 这些脚本可以包含各种 Redis 命令和逻辑。 以便在 Redis 服务器上高效地执行。 它提供了一种方便的方式来处理批量数据操作和复杂的业务逻辑。 在与 Redis 进行交互时非常有用。GEO_RADIUS_STOREDIST_SCRIPT 函数的作用是:通过特定的地理坐标范围和存储的距离相关信息来进行相关的操作和计算,以实现特定的地理空间相关的功能和效果。
返回 GEO_RADIUS_STOREDIST_SCRIPT。
}
static {
创建了一个 StringBuilder 对象,名为 sb 。
sb 追加内容为“调用 redis 的‘georadiusbymember’命令,参数依次为 KEYS[1]、KEYS[2]、KEYS[3]、KEYS[4]、KEYS[5]、KEYS[6]、KEYS[7]”
GEO_RADIUS_STOREDIST_SCRIPT 是一个新的 RedisScriptImpl 。<>(sb.toString(), Long.class);
}
private static class RedisScriptImpl 这个类是私有的静态类。它主要用于实现与 Redis 相关的脚本功能。在代码中,它可能被用于定义和执行特定的 Redis 脚本逻辑,以满足特定的业务需求。例如,可以通过这个类来编写和执行 Redis 的事务脚本、流水线脚本等。它为与 Redis 进行交互提供了一种方便的方式,使得在代码中可以更高效地处理 Redis 数据和操作。 implements RedisScript {
private 类型的 final 字符串 script 。
声明了一个私有且不可变的字符串变量 sha1 。
private final Class resultType;
RedisScriptImpl 是一个公共类,它接受一个字符串类型的脚本和一个类作为参数。 resultType) {
this 的 script 被赋值为 script 。
this.sha1 等于 DigestUtils 的 sha1DigestAsHex 方法对 script 进行处理后的结果。
this 的 resultType 被赋值为 resultType 。
}
@Override
获取 Sha1 的方法是一个公共的字符串类型的方法。它用于获取某个特定对象或数据的 Sha1 值。通过执行特定的算法和操作,这个方法能够计算出相应的 Sha1 哈希值,并将其以字符串的形式返回。
return sha1;
}
@Override
public Class getResultType() {
返回 resultType。
}
@Override
获取字符串形式的脚本。
return script;
}
}
}
测试
1.进行请求
2.查看效果
模拟用户 sunliu 查找距离自身 100km 范围内的人,然后按照 3 条为一页的方式进行查询,并发起请求。
总结
分页实现思路:先把 geo 集合里的数据按照距离从近到远进行筛选,接着通过 storedist 把筛选后的数据放入一个新的 Zset 集合。
redisTemplate 未对原生命令 georadiusbymember 的 storedist 参数进行实现,而是通过灵活运用 Lua 脚本来实现。