需求分析:
在美团外卖或者其他软件上都会有显示商家距离你有多少米,这个我们可以利用redis的GEO地理定位计算可以得出,数据库中存放商家的经纬度(坐标),通过geo计算得出距离。
代码已上传至gitHub---->https://github.com/GitHubzcc/redisExample
关于 Redis Geo介绍
- 1、Redis 的 Geo 是在 3.2 版本才有的
- 2、使用 geohash 保存地理位置的坐标
- 3、使用有序集合(zset)保存地理位置的集合
关于GEO的6个操作命令
- 1、GEOADD:增加某个地理位置的坐标
- 2、GEOPOS:获取某个地理位置的坐标
- 3、GEODIST:获取两个地理位置的距离
- 4、GEORADIUS:根据给定地理位置坐标获取指定范围内的地理位置集合
- 5、GEORADIUSBYMEMBER:根据给定地理位置获取指定范围内的地理位置集合
- 6、GEOHASH:获取某个地理位置的 geohash 值
命令详情解析
- 1.1 GEOADD命令
将给定的空间元素(纬度、经度、名字)添加到指定的键里面。 这些数据会以有序集合的形式被储存在键里面, 从而使得像 GEORADIUS
和 GEORADIUSBYMEMBER
这样的命令可以在之后通过位置查询取得这些元素。
命令demo: GEOADD key longitude latitude member [longitude latitude member ...]
命令描述:将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。
返回值:添加到sorted set元素的数目,但不包括已更新score的元素。
- 2.1 GEODIST概念
返回两个给定位置之间的距离。如果两个位置之间的其中一个不存在, 那么命令返回空值。
指定单位的参数 unit 必须是以下单位的其中一个:
m 表示单位为米、km 表示单位为千米、mi 表示单位为英里、ft 表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。
GEODIST 命令在计算距离时会假设地球为完美的球形,在极限情况下, 这一假设最大会造成 0.5% 的误差。
命令demo: GEODIST key member1 member2 [unit]
- 3.1 GEOPOS概念
从键里面返回所有给定位置元素的位置(经度和纬度)。因为 GEOPOS 命令接受可变数量的位置元素作为输入, 所以即使用户只给定了一个位置元素, 命令也会返回数组回复。GEOPOS 命令返回一个数组, 数组中的每个项都由两个元素组成: 第一个元素为给定位置元素的经度, 而第二个元素则为给定位置元素的纬度。 当给定的位置元素不存在时, 对应的数组项为空值。
命令demo: GEOPOS key member [member ...]
- 4.1 GEOHASH概念
返回一个或多个位置元素的 Geohash 表示。返回值:一个数组, 数组的每个项都是一个 geohash 。 命令返回的 geohash 的位置与用户给定的位置元素的位置一一对应。
命令demo: GEOHASH key member [member ...]
- 5.1 GEORADIUS概念
以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。范围可以使用以下其中一个单位:m 表示单位为米。km 表示单位为千米。mi 表示单位为英里。ft 表示单位为英尺。
在给定以下可选项时, 命令会返回额外的信息:
- WITHDIST : 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
- WITHCOORD : 将位置元素的经度和维度也一并返回。
- WITHHASH : 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:
- ASC : 根据中心的位置, 按照从近到远的方式返回位置元素。
- DESC : 根据中心的位置, 按照从远到近的方式返回位置元素。
在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT 选项去获取前 N 个匹配元素, 但是因为命令在内部可能会需要对所有被匹配的元素进行处理, 所以在对一个非常大的区域进行搜索时, 即使只使用 COUNT 选项去获取少量元素, 命令的执行速度也可能会非常慢。 但是从另一方面来说, 使用 COUNT 选项去减少需要返回的元素数量, 对于减少带宽来说仍然是非常有用的。
GEORADIUS 命令返回一个数组, 具体来说:在没有给定任何 WITH 选项的情况下, 命令只会返回一个像 ["New York","Milan","Paris"] 这样的线性(linear)列表。在指定了 WITHCOORD 、 WITHDIST 、 WITHHASH 等选项的情况下, 命令返回一个二层嵌套数组, 内层的每个子数组就表示一个元素。在返回嵌套数组时, 子数组的第一个元素总是位置元素的名字。 至于额外的信息, 则会作为子数组的后续元素, 按照以下顺序被返回:以浮点数格式返回的中心与位置元素之间的距离, 单位与用户指定范围时的单位一致。
命令demo: GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
- 6.1 GEORADIUSBYMEMBER概念
这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点。
命令: GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
测试代码TEST:
package com.eriz.redisExample;
import com.alibaba.fastjson.JSON;
import com.eriz.redisExample.Pos.service.GeoService;
import com.eriz.redisExample.Pos.vo.CityInfo;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author: eriz 471295986@qq.com
* @date: 2019/7/12 23:32
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class}, webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class ApplicationTests {
/**
* fake some cityInfos
*/
private List<CityInfo> cityInfos;
@Autowired
private GeoService geoService;
@Before
public void init() {
cityInfos = new ArrayList<>();
cityInfos.add(new CityInfo("hefei", 117.17, 31.52));
cityInfos.add(new CityInfo("anqing", 117.02, 30.31));
cityInfos.add(new CityInfo("huaibei", 116.47, 33.57));
cityInfos.add(new CityInfo("suzhou", 116.58, 33.38));
cityInfos.add(new CityInfo("fuyang", 115.48, 32.54));
cityInfos.add(new CityInfo("bengbu", 117.21, 32.56));
cityInfos.add(new CityInfo("huangshan", 118.18, 29.43));
}
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void redisTest() {
stringRedisTemplate.opsForValue().set("a", "testsdfsdfasdfadf");
}
/**
* <h2>测试 saveCityInfoToRedis 方法</h2>
*/
@Test
public void testSaveCityInfoToRedis() {
System.out.println(geoService.saveCityInfoToRedis(cityInfos));
}
/**
* <h2>测试 getCityPos 方法</h2>
* 如果传递的 city 在 Redis 中没有记录, 会返回什么呢 ? 例如, 这里传递的 xxx
*/
@Test
public void testGetCityPos() {
System.out.println(JSON.toJSONString(geoService.getCityPos(
Arrays.asList("anqing", "suzhou", "xxx").toArray(new String[3])
)));
}
/**
* <h2>测试 getTwoCityDistance 方法</h2>
*/
@Test
public void testGetTwoCityDistance() {
System.out.println(geoService.getTwoCityDistance("anqing", "suzhou", null).getValue());
System.out.println(geoService.getTwoCityDistance("anqing", "suzhou", Metrics.KILOMETERS).getValue());
}
/**
* <h2>测试 getPointRadius 方法</h2>
*/
@Test
public void testGetPointRadius() {
Point center = new Point(cityInfos.get(0).getLongitude(), cityInfos.get(0).getLatitude());
Distance radius = new Distance(200, Metrics.KILOMETERS);
Circle within = new Circle(center, radius);
System.out.println(JSON.toJSONString(geoService.getPointRadius(within, null)));
// order by 距离 limit 2, 同时返回距离中心点的距离
RedisGeoCommands.GeoRadiusCommandArgs args =
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(2).sortAscending();
System.out.println(JSON.toJSONString(geoService.getPointRadius(within, args)));
}
/**
* <h2>测试 getMemberRadius 方法</h2>
*/
@Test
public void testGetMemberRadius() {
Distance radius = new Distance(200, Metrics.KILOMETERS);
System.out.println(JSON.toJSONString(geoService.getMemberRadius("suzhou", radius, null)));
// order by 距离 limit 2, 同时返回距离中心点的距离
RedisGeoCommands.GeoRadiusCommandArgs args =
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(2).sortAscending();
System.out.println(JSON.toJSONString(geoService.getMemberRadius("suzhou", radius, args)));
}
/**
* <h2>测试 getCityGeoHash 方法</h2>
*/
@Test
public void testGetCityGeoHash() {
System.out.println(JSON.toJSONString(geoService.getCityGeoHash(
Arrays.asList("anqing", "suzhou", "xxx").toArray(new String[3])
)));
}
}
接口层:
package com.eriz.redisExample.Pos.service;
import com.eriz.redisExample.Pos.vo.CityInfo;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import java.util.Collection;
import java.util.List;
public interface GeoService {
/**
* <h2>把城市信息保存到 Redis 中</h2>
* @param cityInfos {@link CityInfo}
* @return 成功保存的个数
* */
Long saveCityInfoToRedis(Collection<CityInfo> cityInfos);
/**
* <h2>获取给定城市的坐标</h2>
* @param cities 给定城市 key
* @return {@link Point}s
* */
List<Point> getCityPos(String[] cities);
/**
* <h2>获取两个城市之间的距离</h2>
* @param city1 第一个城市
* @param city2 第二个城市
* @param metric {@link Metric} 单位信息, 可以是 null
* @return {@link Distance}
* */
Distance getTwoCityDistance(String city1, String city2, Metric metric);
/**
* <h2>根据给定地理位置坐标获取指定范围内的地理位置集合</h2>
* @param within {@link Circle} 中心点和距离
* @param args {@link RedisGeoCommands.GeoRadiusCommandArgs} 限制返回的个数和排序方式, 可以是 null
* @return {@link RedisGeoCommands.GeoLocation}
* */
GeoResults<RedisGeoCommands.GeoLocation<String>> getPointRadius(
Circle within, RedisGeoCommands.GeoRadiusCommandArgs args);
/**
* <h2>根据给定地理位置获取指定范围内的地理位置集合</h2>
* */
GeoResults<RedisGeoCommands.GeoLocation<String>> getMemberRadius(
String member, Distance distance, RedisGeoCommands.GeoRadiusCommandArgs args);
/**
* <h2>获取某个地理位置的 geohash 值</h2>
* @param cities 给定城市 key
* @return city geohashs
* */
List<String> getCityGeoHash(String[] cities);
}
实现层:
package com.eriz.redisExample.Pos.service.impl;
import com.eriz.redisExample.Pos.service.GeoService;
import com.eriz.redisExample.Pos.vo.CityInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.GeoOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author: eriz 471295986@qq.com
* @date: 2019/7/12 23:47
*/
@Service
public class GeoServiceImpl implements GeoService{
private final String GEO_KEY = "ah-cities";
/** redis 客户端 */
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Long saveCityInfoToRedis(Collection<CityInfo> cityInfos) {
//log.info("start to save city info: {}.", JSON.toJSONString(cityInfos));
GeoOperations<String, String> ops = redisTemplate.opsForGeo();
Set<RedisGeoCommands.GeoLocation<String>> locations = new HashSet<>();
cityInfos.forEach(ci -> locations.add(new RedisGeoCommands.GeoLocation<String>(
ci.getCity(), new Point(ci.getLongitude(), ci.getLatitude())
)));
//log.info("done to save city info.");
return ops.add(GEO_KEY, locations);
}
@Override
public List<Point> getCityPos(String[] cities) {
GeoOperations<String, String> ops = redisTemplate.opsForGeo();
return ops.position(GEO_KEY, cities);
}
@Override
public Distance getTwoCityDistance(String city1, String city2, Metric metric) {
GeoOperations<String, String> ops = redisTemplate.opsForGeo();
return metric == null ?
ops.distance(GEO_KEY, city1, city2) : ops.distance(GEO_KEY, city1, city2, metric);
}
@Override
public GeoResults<RedisGeoCommands.GeoLocation<String>> getPointRadius(
Circle within, RedisGeoCommands.GeoRadiusCommandArgs args
) {
GeoOperations<String, String> ops = redisTemplate.opsForGeo();
return args == null ?
ops.radius(GEO_KEY, within) : ops.radius(GEO_KEY, within, args);
}
@Override
public GeoResults<RedisGeoCommands.GeoLocation<String>> getMemberRadius(
String member, Distance distance, RedisGeoCommands.GeoRadiusCommandArgs args
) {
GeoOperations<String, String> ops = redisTemplate.opsForGeo();
return args == null ?
ops.radius(GEO_KEY, member, distance) : ops.radius(GEO_KEY, member, distance, args);
}
@Override
public List<String> getCityGeoHash(String[] cities) {
GeoOperations<String, String> ops = redisTemplate.opsForGeo();
return ops.hash(GEO_KEY, cities);
}
}