需求分析:
在APP上显示距离自己当前位置最近的5个项目,这个我们可以利用redis的GEO地理定位计算可以得出,数据库中存放项目的经纬度(坐标),通过geo计算得出距离。
关于 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 等选项的情况下, 命令返回一个二层嵌套数组, 内层的每个子数组就表示一个元素。在返回嵌套数组时, 子数组的第一个元素总是位置元素的名字。 至于额外的信息, 则会作为子数组的后续元素, 按照以下顺序被返回:以浮点数格式返回的中心与位置元素之间的距离, 单位与用户指定范围时的单位一致。
6.1 GEORADIUSBYMEMBER概念
这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点。
相关代码
接口层:
package com.shengbangtec.service;
import com.shengbangtec.model.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, String GEO_KEY);
/**
* <h2>获取给定城市的坐标</h2>
* @param cities 给定城市 key
* @return {@link Point}s
* */
List<Point> getCityPos(String[] cities,String GEO_KEY);
/**
* <h2>获取两个城市之间的距离</h2>
* @param city1 第一个城市
* @param city2 第二个城市
* @param metric {@link Metric} 单位信息, 可以是 null
* @return {@link Distance}
* */
Distance getTwoCityDistance(String city1, String city2, Metric metric,String GEO_KEY);
/**
* <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,String GEO_KEY);
/**
* <h2>根据给定地理位置获取指定范围内的地理位置集合</h2>
* */
GeoResults<RedisGeoCommands.GeoLocation<String>> getMemberRadius(
String member, Distance distance, RedisGeoCommands.GeoRadiusCommandArgs args,String GEO_KEY);
/**
* <h2>获取某个地理位置的 geohash 值</h2>
* @param cities 给定城市 key
* @return city geohashs
* */
List<String> getCityGeoHash(String[] cities,String GEO_KEY);
}
实现层:
package com.shengbangtec.service.impl;
import com.shengbangtec.model.CityInfo;
import com.shengbangtec.service.GeoService;
import lombok.extern.slf4j.Slf4j;
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;
@Service
@Slf4j
public class GeoServiceImpl implements GeoService {
/** redis 客户端 */
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Long saveCityInfoToRedis(Collection<CityInfo> cityInfos, String GEO_KEY) {
GeoOperations<String, String> ops = redisTemplate.opsForGeo();
Set<RedisGeoCommands.GeoLocation<String>> locations = new HashSet<>();
cityInfos.forEach(ci -> locations.add(new RedisGeoCommands.GeoLocation<String>(
ci.getProjectName(), new Point(ci.getLongitude(), ci.getLatitude())
)));
return ops.add(GEO_KEY, locations);
}
@Override
public List<Point> getCityPos(String[] cities,String GEO_KEY) {
GeoOperations<String, String> ops = redisTemplate.opsForGeo();
return ops.position(GEO_KEY, cities);
}
@Override
public Distance getTwoCityDistance(String city1, String city2, Metric metric,String GEO_KEY) {
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,String GEO_KEY
) {
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,String GEO_KEY
) {
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,String GEO_KEY) {
GeoOperations<String, String> ops = redisTemplate.opsForGeo();
return ops.hash(GEO_KEY, cities);
}
}
测试类
package com.shengbangtec;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.shengbangtec.config.AppConfig;
import com.shengbangtec.mapper.CompanyMapper;
import com.shengbangtec.mapper.ProjectMapper;
import com.shengbangtec.mapper.YdzfUserMapper;
import com.shengbangtec.model.CompanyDistance;
import com.shengbangtec.model.ProjectDistance;
import com.shengbangtec.model.CityInfo;
import com.shengbangtec.service.GeoService;
import com.shengbangtec.utils.MapUtil;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.lang3.StringUtils;
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.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import cn.smallbun.screw.core.Configuration;
import cn.smallbun.screw.core.engine.EngineConfig;
import cn.smallbun.screw.core.engine.EngineFileType;
import cn.smallbun.screw.core.engine.EngineTemplateType;
import cn.smallbun.screw.core.execute.DocumentationExecute;
import cn.smallbun.screw.core.process.ProcessConfig;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
private List<CityInfo> cityInfos;
@Autowired
private GeoService geoService;
@Autowired
private ProjectMapper projectMapper;
@Autowired
private CompanyMapper companyMapper;
@Before
public void init() {
cityInfos = projectMapper.getProjectDistance();
/* cityInfos.add(new CityInfo("北京西站", 116.32810279404169, 39.90083463437583));
cityInfos.add(new CityInfo("北京市小园地铁站", 116.11713134332637, 39.89585544013442));
cityInfos.add(new CityInfo("北京市金安桥地铁站",116.16743224512043 , 39.930961767459297));
cityInfos.add(new CityInfo("北京市草房地铁站", 116.39812867295258, 39.99261739912267));
cityInfos.add(new CityInfo("北京市回龙观地铁站", 116.4133836971231, 39.910924547299568));
cityInfos.add(new CityInfo("北京市天通苑地铁站", 116.42784821244477, 40.07495397605039));
cityInfos.add(new CityInfo("北京市西红门地铁站", 116.4133836971231, 39.910924547299568));
cityInfos.add(new CityInfo("北京市大红门地铁站", 116.27445437203589, 40.133277791661459));
cityInfos.add(new CityInfo("北京市角门西地铁站", 116.3827070131101, 39.85563107559189));
cityInfos.add(new CityInfo("北京市顺义地铁站", 116.66142426369096, 40.13635076223076));
cityInfos.add(new CityInfo("北京市军事博物馆地铁站", 116.330207434021, 39.914975754513907));
cityInfos.add(new CityInfo("北京市新宫地铁站", 116.36671340214936, 39.823488313799639));
cityInfos.add(new CityInfo("北京市健德门地铁站", 116.4133836971231, 39.910924547299568));
cityInfos.add(new CityInfo("北京市春天里地铁站", 116.46489199608192, 39.91046643479811));
*/
}
/**
* <h2>测试 saveCityInfoToRedis 方法</h2>
*/
@Test
public void testSaveCityInfoToRedis() {
System.out.println(geoService.saveCityInfoToRedis(cityInfos,AppConfig.PROJECT_KEY));
}
@Test
public void test() {
String city[] = {"北京西站"};
List<Point> cityPos = geoService.getCityPos(city, AppConfig.PROJECT_KEY);
System.out.println(cityPos.get(0) == null);
for (Point cityPo : cityPos) {
System.out.println(cityPo);
}
}
/**
* <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]),AppConfig.PROJECT_KEY
)));
}
/**
* <h2>测试 getTwoCityDistance 方法</h2>
*/
@Test
public void testGetTwoCityDistance() {
System.out.println(geoService.getTwoCityDistance("北京市光耀东方", "北京西站", null,AppConfig.PROJECT_KEY).getValue());
System.out.println(geoService.getTwoCityDistance("北京市光耀东方", "北京西站", Metrics.KILOMETERS,AppConfig.PROJECT_KEY).getValue());
System.out.println(geoService.getTwoCityDistance("北京市光耀东方", "北京市小园地铁站", Metrics.KILOMETERS,AppConfig.PROJECT_KEY).getValue());
}
/**
* <h2>测试 getPointRadius 方法</h2>
*/
@Test
public void testGetPointRadius() {
Point center = new Point(125.3306020759069, 43.82195350104314);
Distance radius = new Distance(20, Metrics.KILOMETERS);
Circle within = new Circle(center, radius);
// order by 距离 limit 2, 同时返回距离中心点的距离
RedisGeoCommands.GeoRadiusCommandArgs args =
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(2).sortAscending();
System.out.println(JSON.toJSONString(geoService.getPointRadius(within, args,AppConfig.COMPANY_KEY)));
}
/**
* <h2>测试 getMemberRadius 方法</h2>
*/
@Test
public void testGetMemberRadius() {
Distance radius = new Distance(200, Metrics.KILOMETERS);
System.out.println(JSON.toJSONString(geoService.getMemberRadius("suzhou", radius, null,AppConfig.PROJECT_KEY)));
// order by 距离 limit 2, 同时返回距离中心点的距离
RedisGeoCommands.GeoRadiusCommandArgs args =
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(2).sortAscending();
System.out.println(JSON.toJSONString(geoService.getMemberRadius("suzhou", radius, args,AppConfig.PROJECT_KEY)));
}
/**
* <h2>测试 getCityGeoHash 方法</h2>
*/
@Test
public void testGetCityGeoHash() {
System.out.println(JSON.toJSONString(geoService.getCityGeoHash(
Arrays.asList("anqing", "suzhou", "xxx").toArray(new String[3]),AppConfig.PROJECT_KEY
)));
}
}