详解mysql,java,redis三种地理位置算法

4 篇文章 0 订阅
4 篇文章 0 订阅

背景

  • 获取附近信息,以用户当前位置为中心点,指定范围为半径,查询出在该圆圈内的所有信息。
  • 计算两个位置之间的距离。

实现

MySQL 基于st_distance函数实现

mysql 5.6.1 加入了空间数据支持功能,新增了st_*相关函数,可以非常方便的计算两个地理坐标点的距离了。

SELECT
	p.* 
FROM
	(
	SELECT
		park_info.*,
		st_distance (
			point ( park_info.lng, park_info.lat ),
		point ( 116.403694, 39.913828 ))* 111195 AS distance 
	FROM
		park_info park_info 
	) p 
WHERE
	p.distance < 10000

Java实现

@Slf4j
public class DistanceUtils {

    /**
     * 地球半径,单位 km
     */
    private static final double EARTH_RADIUS = 6378137;

    /**
     * 根据经纬度,计算两点间的距离
     *
     * @param longitude1 第一个点的经度
     * @param latitude1  第一个点的纬度
     * @param longitude2 第二个点的经度
     * @param latitude2  第二个点的纬度
     * @return 返回距离 单位米
     */
    public static double getDistance(double longitude1, double latitude1, double longitude2, double latitude2) {
        // 纬度
        double lat1 = Math.toRadians(latitude1);
        double lat2 = Math.toRadians(latitude2);
        // 经度
        double lng1 = Math.toRadians(longitude1);
        double lng2 = Math.toRadians(longitude2);
        // 纬度之差
        double a = lat1 - lat2;
        // 经度之差
        double b = lng1 - lng2;
        // 计算两点距离的公式
        double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) +
                Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(b / 2), 2)));
        // 弧长乘地球半径, 返回单位: 米
        s =  s * EARTH_RADIUS;
//        return Math.round(s * 10000) / 10000;
        return Math.round(s * 10000) / 10000;
    }

    public static void main(String[] args) {
        double distance = DistanceUtils.getDistance(109.49081, 36.596537, 111.128971, 37.524041);
        log.info("distance={}", distance);
        double distance1 = DistanceUtils.getDistance(109.500603,36.594125, 111.151761,37.52671);
        log.info("distance={}", distance1);
    }
}

Redis GEO实现

/**
 * 添加经纬度
 *
 * @param key 停车场地理位置键
 * @param lng 坐标
 * @param lat 坐标
 * @param t 停车场标示
 * @return
 */
public Long addGeo(String key, Double lng, Double lat, T t) {
    RedisGeoCommands.GeoLocation<T> geoLocation = new RedisGeoCommands.GeoLocation<>(t, new Point(lng, lat));
    return redisTemplate.opsForGeo().add(key, geoLocation);
}

/**
 * 批量添加
 * @param key
 * @param list
 * @return
 */
public Long batchAddGeo(String key, List<RedisGeoCommands.GeoLocation<T>> list) {
    return redisTemplate.opsForGeo().add(key, list);
}

/**
 * 删除经纬度
 *
 * @param key
 */
public void removeGeo(String key, T... t) {
    redisTemplate.opsForGeo().remove(key, t);
}

/**
 * 获取经纬度指定距离内的数据
 *
 * @param lng
 * @param lat
 * @param distance
 * @param key
 * @return
 */
public List<GeoResult<RedisGeoCommands.GeoLocation<T>>>
getCacheListByPointAndDistance(Double lng, Double lat, Double distance, String key) {
    Circle circle = new Circle(new Point(lng, lat), new Distance(distance, Metrics.KILOMETERS));
    RedisGeoCommands.GeoRadiusCommandArgs args =
            // includeCoordinates:返回结果包含坐标信息
            // includeDistance:返回结果包含具中心坐标距离信息
            // sortAscending:按照距离升序排序
            // sortDescending:按照距离降序排序
            // limit:返回结果数量限制
            RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates()
                    .sortAscending();
    List<GeoResult<RedisGeoCommands.GeoLocation<T>>> content =
            redisTemplate.opsForGeo().radius(key, circle, args).getContent();
    return content;
}

Elasticsearch Geo Distance Query

ES官方文档

版本
Elasticsearchspring-boot-starter-parentspring-boot-starter-data-elasticsearch
elasticsearch:7.3.02.4.102.4.10
实体映射关系
package com.chao.esapi.entity;

import java.time.LocalDateTime;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;

/**
 * 停车场信息表(ParkInfo)表实体类
 *
 * @author makejava
 * @since 2022-06-23 14:46:11
 */
@Data
@Accessors(chain = true)
@Document(indexName = "park_info", shards = 2, replicas = 2)
public class ParkInfo implements Serializable {
    private static final long serialVersionUID = -38954507185282757L;
    /** ID */
    @Id
    @Field(store = true, type = FieldType.Long)
    private Long id;

    /** 停车场名称 */
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String parkName;

    /** 停车场类型:1-路侧 2-封闭式 3-立体 */
    @Field(type = FieldType.Integer)
    private Integer parkType;

    /** 停车场地址 */
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String address;

    /** 经度 */
    private Double lon;

    /** 纬度 */
    private Double lat;

    /** 总车位数 */
    @Field(type = FieldType.Integer)
    private Integer totalSpace;

    /** 已经使用车位数 */
    @Field(type = FieldType.Integer)
    private Integer remainingSpace;

    /** 是否启用:1-是;0-否 */
    @Field(type = FieldType.Integer)
    private Integer isused;

    /** 创建时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
    @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

    /** 更新时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
    @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;

    @GeoPointField
    @TableField(exist = false)
    private GeoPoint location;

    @GeoPointField
    private String locationStr;
}
Repository
package com.chao.esapi.repository;

import com.chao.esapi.entity.ParkInfo;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

/**
 * @author :晁天王
 * @date :Created in 2022/6/23 14:52
 * @description:
 * @modified By:
 * @version: $
 */
@Repository
public interface ParkInfoRepository extends ElasticsearchRepository<ParkInfo, Long> {
}

Test
package com.chao.esapi.park;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.chao.esapi.entity.ParkInfo;
import com.chao.esapi.mapper.ParkInfoMapper;
import com.chao.esapi.repository.ParkInfoRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.sort.GeoDistanceSortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;

import java.util.List;
import java.util.stream.Collectors;

/**
 * @author :晁天王
 * @date :Created in 2022/6/23 14:56
 * @description:
 * @modified By:
 * @version: $
 */
@Slf4j
@SpringBootTest
public class GeoDistanceQueryTests {

    @Autowired
    private ParkInfoRepository parkInfoRepository;
    @Autowired
    private ParkInfoMapper parkInfoMapper;
    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @Test
    public void importParkInfo() {
        List<ParkInfo> parkInfoList = parkInfoMapper.selectList(Wrappers.lambdaQuery());
        parkInfoList = parkInfoList.stream().map(e -> e.setLocation(new GeoPoint(e.getLat(), e.getLon()))).collect(Collectors.toList());
        parkInfoRepository.saveAll(parkInfoList);
    }

    @Test
    public void findAll() {
        Iterable<ParkInfo> parkInfos = parkInfoRepository.findAll();
        parkInfos.forEach(e -> {
            log.info("[ES] findAll:{}", e.toString());
        });
    }

    @Test
    public void delete() {
        parkInfoRepository.deleteAll();
    }

    /**
     * 搜索中心点指定范围内的数据, 由近到远排序
     * @throws JsonProcessingException
     */
    @Test
    public void geoDistanceTest() throws JsonProcessingException {
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery()
                .must(QueryBuilders.matchAllQuery())
                .filter(
                        // 用于标识查询的可选名称字段
                        QueryBuilders.geoDistanceQuery("location")
                        // 中心点
                        .point(34.2575531, 108.9357986)
                        // 以指定位置为中心的圆的半径。落入这个圈子的点被认为是匹配的。distance可以以各种单位指定
                        .distance(1, DistanceUnit.KILOMETERS)
                        // 如何计算距离。可以是arc(默认)或plane(更快,但在长距离和靠近两极时不准确)。
                        .geoDistance(GeoDistance.PLANE)
                );

        // 按距离由近到远排序
        GeoDistanceSortBuilder geoDistanceSortBuilder = SortBuilders
                .geoDistanceSort("location", 34.2575531, 108.9357986)
                .unit(DistanceUnit.METERS)
                .order(SortOrder.ASC);

        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(queryBuilder)
                .withSort(geoDistanceSortBuilder)
                .build();
        SearchHits<ParkInfo> searchHits = elasticsearchRestTemplate.search(searchQuery, ParkInfo.class);
        ObjectMapper objectMapper = new ObjectMapper();
        String s = objectMapper.writeValueAsString(searchHits);
        log.info("[ES] geoDistanceQuery : {}", s);
        log.info("[ES] totalHits : {}", searchHits.getTotalHits());

        // 获取值返回
        searchHits.getSearchHits().forEach(e -> {
            log.info("[ES] rangeQuery HIT distance:{}, content:{}",e.getSortValues().get(0), e.getContent());
        });
    }
}

总结

实现方式优点缺点
MySQL实现简单造成数据库计算压力,效率低
Java实现简单,不依赖其它工具效率较低
Redis Geo效率高需要依赖Redis,需要维护Geo缓存数据
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值