9、GEO搜附近
很多生活类的APP都具备一个搜索附近的功能,比如美团搜索附近的商家;
如果自己想要根据经纬度来实现一个搜索附近的功能,是非常麻烦的;但是Redis 在3.2的版本新增了Redis GEO,用于存储地址位置信息,并对支持范围搜索;基于GEO就能轻松且快速的开发一个搜索附近的功能;
GEO API 及Redis-cli 操作:
geoadd:新增位置坐标。
127.0.0.1:6379> GEOADD drinks 116.62445 39.86206 starbucks 117.3514785 38.7501247 yidiandian 116.538542 39.75412 xicha (integer) 3
geopos:获取位置坐标。
127.0.0.1:6379> GEOPOS drinks starbucks 1) 1) "116.62445157766342163" 2) "39.86206038535793539" 127.0.0.1:6379> GEOPOS drinks starbucks yidiandian mxbc 1) 1) "116.62445157766342163" 2) "39.86206038535793539" 2) 1) "117.35148042440414429" 2) "38.75012383773680114" 3) (nil)
geodist:计算两个位置之间的距离。
单位参数:
127.0.0.1:6379> GEODIST drinks starbucks yidiandian "138602.4133" 127.0.0.1:6379> GEODIST drinks starbucks xicha "14072.1255" 127.0.0.1:6379> GEODIST drinks starbucks xicha m "14072.1255" 127.0.0.1:6379> GEODIST drinks starbucks xicha km "14.0721"
-
m :米,默认单位。 -
km :千米。 -
mi :英里。 -
ft :英尺。
georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
参数说明
127.0.0.1:6379> GEORADIUS drinks 116 39 100 km WITHDIST 1) 1) "xicha" 2) "95.8085" 127.0.0.1:6379> GEORADIUS drinks 116 39 100 km WITHDIST WITHCOORD 1) 1) "xicha" 2) "95.8085" 3) 1) "116.53854042291641235" 2) "39.75411928478748536" 127.0.0.1:6379> GEORADIUS drinks 116 39 100 km WITHDIST WITHCOORD WITHHASH 1) 1) "xicha" 2) "95.8085" 3) (integer) 4069151800882301 4) 1) "116.53854042291641235" 2) "39.75411928478748536" 127.0.0.1:6379> GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD COUNT 1 1) 1) "xicha" 2) "95.8085" 3) 1) "116.53854042291641235" 2) "39.75411928478748536" 127.0.0.1:6379> GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD COUNT 1 ASC 1) 1) "xicha" 2) "95.8085" 3) 1) "116.53854042291641235" 2) "39.75411928478748536" 127.0.0.1:6379> GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD COUNT 1 DESC 1) 1) "starbucks" 2) "109.8703" 3) 1) "116.62445157766342163" 2) "39.86206038535793539"
-
m :米,默认单位。 -
km :千米。 -
mi :英里。 -
ft :英尺。 -
WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 -
WITHCOORD: 将位置元素的经度和纬度也一并返回。 -
WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。 -
COUNT 限定返回的记录数。 -
ASC: 查找结果根据距离从近到远排序。 -
DESC: 查找结果根据从远到近排序。
georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
功能和上面的georadius类似,只是georadius是以经纬度坐标为中心,这个是以某个地点为中心;
geohash:返回一个或多个位置对象的 geohash 值。
127.0.0.1:6379> GEOHASH drinks starbucks xicha 1) "wx4fvbem6d0" 2) "wx4f5vhb8b0"
SpringBoot 操作
通过SpringBoot操作GEO的示例如下
import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.geo.*; import org.springframework.data.redis.connection.RedisGeoCommands; import org.springframework.data.redis.core.RedisTemplate; import java.util.List; /** * @author 一行Java * @title: GEOTest * @projectName ehang-spring-boot * @description: TODO * @date 2022/7/28 17:29 */ @SpringBootTest @Slf4j public class GEOTest { private final String KEY = "geo:drinks"; @Autowired RedisTemplate redisTemplate; @Test public void test() { add("starbucks", new Point(116.62445, 39.86206)); add("yidiandian", new Point(117.3514785, 38.7501247)); add("xicha", new Point(116.538542, 39.75412)); get("starbucks", "yidiandian", "xicha"); GeoResults nearByXY = getNearByXY(new Point(116, 39), new Distance(120, Metrics.KILOMETERS)); List<GeoResult> content = nearByXY.getContent(); for (GeoResult geoResult : content) { log.info("{}", geoResult.getContent()); } GeoResults nearByPlace = getNearByPlace("starbucks", new Distance(120, Metrics.KILOMETERS)); content = nearByPlace.getContent(); for (GeoResult geoResult : content) { log.info("{}", geoResult.getContent()); } getGeoHash("starbucks", "yidiandian", "xicha"); del("yidiandian", "xicha"); } private void add(String name, Point point) { Long add = redisTemplate.opsForGeo().add(KEY, point, name); log.info("成功添加名称:{} 的坐标信息信息:{}", name, point); } private void get(String... names) { List<Point> position = redisTemplate.opsForGeo().position(KEY, names); log.info("获取名称为:{} 的坐标信息:{}", names, position); } private void del(String... names) { Long remove = redisTemplate.opsForGeo().remove(KEY, names); log.info("删除名称为:{} 的坐标信息数量:{}", names, remove); } /** * 根据坐标 获取指定范围的位置 * * @param point * @param distance * @return */ private GeoResults getNearByXY(Point point, Distance distance) { Circle circle = new Circle(point, distance); RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs. newGeoRadiusArgs(). includeDistance(). // 包含距离 includeCoordinates(). // 包含坐标 sortAscending(). // 排序 还可选sortDescending() limit(5); // 获取前多少个 GeoResults geoResults = redisTemplate.opsForGeo().radius(KEY, circle, args); log.info("根据坐标获取:{} {} 范围的数据:{}", point, distance, geoResults); return geoResults; } /** * 根据一个位置,获取指定范围内的其他位置 * * @param name * @param distance * @return */ private GeoResults getNearByPlace(String name, Distance distance) { RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs. newGeoRadiusArgs(). includeDistance(). // 包含距离 includeCoordinates(). // 包含坐标 sortAscending(). // 排序 还可选sortDescending() limit(5); // 获取前多少个 GeoResults geoResults = redisTemplate.opsForGeo() .radius(KEY, name, distance, args); log.info("根据位置:{} 获取: {} 范围的数据:{}", name, distance, geoResults); return geoResults; } /** * 获取GEO HASH * * @param names * @return */ private List<String> getGeoHash(String... names) { List<String> hash = redisTemplate.opsForGeo().hash(KEY, names); log.info("names:{} 对应的hash:{}", names, hash); return hash; } }
执行日志:
成功添加名称:starbucks 的坐标信息信息:Point [x=116.624450, y=39.862060] 成功添加名称:yidiandian 的坐标信息信息:Point [x=117.351479, y=38.750125] 成功添加名称:xicha 的坐标信息信息:Point [x=116.538542, y=39.754120] 获取名称为:[starbucks, yidiandian, xicha] 的坐标信息:[Point [x=116.624452, y=39.862060], Point [x=117.351480, y=38.750124], Point [x=116.538540, y=39.754119]] 根据坐标获取:Point [x=116.000000, y=39.000000] 120.0 KILOMETERS 范围的数据:GeoResults: [averageDistance: 102.8394 KILOMETERS, results: GeoResult [content: RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119]), distance: 95.8085 KILOMETERS, ],GeoResult [content: RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060]), distance: 109.8703 KILOMETERS, ]] RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119]) RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060]) 根据位置:starbucks 获取: 120.0 KILOMETERS 范围的数据:GeoResults: [averageDistance: 7.03605 KILOMETERS, results: GeoResult [content: RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060]), distance: 0.0 KILOMETERS, ],GeoResult [content: RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119]), distance: 14.0721 KILOMETERS, ]] RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060]) RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119]) names:[starbucks, yidiandian, xicha] 对应的hash:[wx4fvbem6d0, wwgkqqhxzd0, wx4f5vhb8b0] 删除名称为:[yidiandian, xicha] 的坐标信息数量:2
10、简单限流
为了保证项目的安全稳定运行,防止被恶意的用户或者异常的流量打垮整个系统,一般都会加上限流,比如常见的sential
、hystrix
,都是实现限流控制;如果项目用到了Redis,也可以利用Redis,来实现一个简单的限流功能;
功能所需命令
-
INCR:将 key 中储存的数字值增一 -
Expire:设置key的有效期
Redis-cli操作
127.0.0.1:6379> INCR r:f:user1 (integer) 1 # 第一次 设置一个过期时间 127.0.0.1:6379> EXPIRE r:f:user1 5 (integer) 1 127.0.0.1:6379> INCR r:f:user1 (integer) 2 # 等待5s 再次增加 发现已经重置了 127.0.0.1:6379> INCR r:f:user1 (integer) 1
SpringBoot示例:
import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; import java.util.concurrent.TimeUnit; /** * @title: 基于Redis的简单限流 * @projectName ehang-spring-boot * @description: TODO * @date 2022/8/2 9:43 */ @SpringBootTest @Slf4j public class FreqTest { // 单位时间(秒) private static final Integer TIME = 5; // 允许访问上限次数 private static final Integer MAX = 100; @Autowired RedisTemplate redisTemplate; @Test public void test() throws Exception { String userName = "user1"; int tag = 1; boolean frequency = frequency(userName); log.info("第{}次是否放行:{}", tag, frequency); for (int i = 0; i < 100; i++) { tag += 1; frequency(userName); } frequency = frequency(userName); log.info("第{}次是否放行:{}", tag, frequency); Thread.sleep(5000); frequency = frequency(userName); log.info("模拟等待5s后,第{}次是否放行:{}", tag, frequency); } /** * 校验访问频率 * * @param uniqueId 用于限流的唯一ID 可以是用户ID、或者客户端IP等 * @return true:放行 false:拦截 */ private boolean frequency(String uniqueId) { String key = "r:q:" + uniqueId; Long increment = redisTemplate.opsForValue().increment(key); if (increment == 1) { redisTemplate.expire(key, TIME, TimeUnit.SECONDS); } if (increment <= MAX) { return true; } return false; } }
运行日志:
user1 第1次请求是否放行:true user1 第101次请求是否放行:false 模拟等待5s后,user1 第101次请求是否放行:true
11、全局ID
在分布式系统中,很多场景下需要全局的唯一ID,由于Redis是独立于业务服务的其他应用,就可以利用Incr
的原子性操作来生成全局的唯一递增ID
功能所需命令
-
INCR:将 key 中储存的数字值增一
Redis-cli 客户端测试
127.0.0.1:6379> incr g:uid (integer) 1 127.0.0.1:6379> incr g:uid (integer) 2 127.0.0.1:6379> incr g:uid (integer) 3
文章评论