手上有一个微信公众号项目有这样的一个需求,需要在用户打开微信公众号的时候将附近10公里的店铺按照距离用户当前位置的距离远近由近到远排序,展示在页面中,我们的前端程序猿比较low所以只能把这个计算放在后端。
我们来分析一下这个需求,这里我们需要从后台获取到店铺的列表,这里店铺位置的经纬度是入库的,所以可以随查询带出。用户当前的经纬度可以通过h5获取到,在公众号首页被打开的时候,将用户经纬度传递给后台。后台拿着这个经纬度后,遍历取出的店铺列表获取到店铺的经纬度进行计算。然后将结果返回给前端。
这里一个点是无法预知用户何时打开公众号,所以每次都即时计算距离,而且这个距离是不能入库的。因为每个用户所在位置不一样,这个距离就不一样。这里我们先来看一下项目原来的关键代码。
Service层:
/**
* 查询全部列表并根据会员当前经纬度排序
*
* @param point 经纬度坐标
* @param page 页码
* @param size 每页大小
* @return 店铺页面
*/
public Page<Store> findAll(Point point, int page, int size) {
List<Store> stores = storeDao.findAll();
//point有值才进行循环从而设置距离dis
if (point.getLatitude() != null && point.getLongitude() != null) {
for (Store store : stores) {
//这里改写了store中的set
store.setDis(point);
}
stores.sort(Comparator.comparing(Store::getDis));
}
Pageable pageable = PageRequest.of(page - 1, size);
Page<Store> storePage = List2PageUtil.list2Page(stores, pageable);
return storePage;
}
我们再看Store类。
/**
* 通过百度的公式来计算两点之间的距离
* @param point 用户当前经纬度坐标
*/
public void setDis(Point point) {
double radLat1 = rad(Double.parseDouble(point.getLatitude()));
double radLat2 = rad(Double.parseDouble(this.latitude));
double a = radLat1 - radLat2;
double b = rad(Double.parseDouble(point.getLongitude())) - rad(Double.parseDouble(this.longitude));
double s = 2 * Math.asin(Math.sqrt(Math.abs(Math.pow(Math.sin(a/2),2)
Math.cos(radLat1)*Math.cos(radLat2)*Math.pow(Math.sin(b/2),2))));
s = s * 6378.137;
s = Math.round(s * 1000);
this.dis = new BigDecimal(s);
}
这里通过百度的公式来算了当前店铺和用户当前位置的距离。为了计算距离在Store对应的表里面增加了一个dis字段,然后保持空值。等到计算完了之后才把这个值加到Store对象中,可以说是相当繁琐了。
接下来我们想通过Redis提供的GeoHash来优化这些代码。关于什么是GeoHash,我稍后再写一个帖子来讲解。Geo特性是Redis3.2版本之后的一个新特性,这个功能可以将给定的地理位置信息存储起来并对这些信息进行操作。这里猜想可以利用Redis来解决这个问题。这里我们通过一个测试类来验证我们的猜想。
private RedisTemplate redisTemplate=null;
@Test
public void test() {
redisTemplate=new RedisTemplate();
//添加店铺坐标
GeoOperations geoOperations = redisTemplate.opsForGeo();
geoOperations.add("store",new Point(118.11789, 24.479466) , "麦当劳(湖滨东路餐厅)");
geoOperations.add("store", new Point(118.088762, 24.467328), "陶乡涮涮锅(厦禾店)");
geoOperations.add("store", new Point(118.163978, 24.483079), "汕头八合里海记牛肉店(加州店)");
geoOperations.add("store", new Point(118.038997, 24.485467), "海底捞火锅(阿罗海城市广场店)");
geoOperations.add("store", new Point(118.178674, 24.49141), "必胜客(金山路餐厅)");
//假设用户当前坐标 118.129715, 24.49874 我们搜10公里以内的
Circle circle=new Circle(new Point(118.129715, 24.49874),new Distance(10,Metrics.KILOMETERS));
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
.newGeoRadiusArgs().includeDistance()
.includeCoordinates().sortAscending().limit(10);
GeoResults<RedisGeoCommands.GeoLocation<String>> results=redisTemplate.opsForGeo().radius("store",circle,args);
for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : results) {
System.out.println(result);
}
}
这里最早redisTemplate使用了自动注入但是注入不成功,报指针。于是采用在test方法里面new,结果运行依旧有问题。
java.lang.NullPointerException
at org.springframework.data.redis.core.AbstractOperations.rawKey(AbstractOperations.java:112)
at org.springframework.data.redis.core.DefaultGeoOperations.add(DefaultGeoOperations.java:57)
at com.robot.merchant.StoreServiceTest.test(StoreServiceTest.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
打印redisTemplate并不为空,想到可能redisTemplate不空,但是里面可能某个值是空的。百度后发现需要添加redis相关的配置。修改方法:
private RedisTemplate redisTemplate = null;
@Test
public void test() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setPort(6379);
config.setHostName("192.168.23.128");
JedisConnectionFactory factory = new JedisConnectionFactory(config);
factory.afterPropertiesSet();
redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(factory);
redisTemplate.afterPropertiesSet();
//添加店铺坐标
GeoOperations geoOperations = redisTemplate.opsForGeo();
geoOperations.add("store", new Point(118.11789, 24.479466), "麦当劳(湖滨东路餐厅)");
geoOperations.add("store", new Point(118.088762, 24.467328), "陶乡涮涮锅(厦禾店)");
geoOperations.add("store", new Point(118.163978, 24.483079), "汕头八合里海记牛肉店(加州店)");
geoOperations.add("store", new Point(118.038997, 24.485467), "海底捞火锅(阿罗海城市广场店)");
geoOperations.add("store", new Point(118.178674, 24.49141), "必胜客(金山路餐厅)");
//假设用户当前坐标 118.129715, 24.49874 我们搜10公里以内的
Circle circle = new Circle(new Point(118.129715, 24.49874), new Distance(10, Metrics.KILOMETERS));
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
.newGeoRadiusArgs().includeDistance()
.includeCoordinates().sortAscending().limit(10);
GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo().radius("store", circle, args);
for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : results) {
System.out.println(result);
}
}
运行报找不到JedisPoolConfig的字节码
Error:(21, 42) java: 无法访问redis.clients.jedis.JedisPoolConfig
找不到redis.clients.jedis.JedisPoolConfig的类文件
导入jedis的依赖,再次运行测试方法。成功通过。
GeoResult [content: RedisGeoCommands.GeoLocation(name=麦当劳(湖滨东路餐厅), point=Point [x=118.117889, y=24.479466]), distance: 2.4553 KILOMETERS, ]
GeoResult [content: RedisGeoCommands.GeoLocation(name=汕头八合里海记牛肉店(加州店), point=Point [x=118.163981, y=24.483078]), distance: 3.8812 KILOMETERS, ]
GeoResult [content: RedisGeoCommands.GeoLocation(name=必胜客(金山路餐厅), point=Point [x=118.178674, y=24.491410]), distance: 5.022 KILOMETERS, ]
GeoResult [content: RedisGeoCommands.GeoLocation(name=陶乡涮涮锅(厦禾店), point=Point [x=118.088761, y=24.467328]), distance: 5.4216 KILOMETERS, ]
GeoResult [content: RedisGeoCommands.GeoLocation(name=海底捞火锅(阿罗海城市广场店), point=Point [x=118.038995, y=24.485466]), distance: 9.3004 KILOMETERS, ]
Process finished with exit code 0
这样证实我们可以通过redis来完成这个需求。这样就不再需要在set里面对距离进行计算了。
,