Redis GEO 类型与 API 结合,地理位置优化的绝佳实践

news2024/12/31 5:57:08

在这里插入图片描述

🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
🌲文章所在专栏:MySQL、Redis、业务设计
🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
💬 向我询问任何您想要的东西,ID:vnjohn
🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏
😄 代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏

目录

  • 前言
  • MySQL 数据库
    • 表结构
    • 模拟数据
    • 数据库查询
      • 不加索引
      • 加索引
    • 直译函数
    • 小结
  • Redis 缓存
    • Redis GEO 客户端
    • 引入 Spring、Redisson 配置
    • RedisTemplate API 操作
    • Redisson API 操作
    • 小结
  • 总结

前言

在企业开发中,例如:附近服务门店/网点查询、附近服务工人派单查询,若没有合理去设计地理位置的这块查询性能提升的功能时,都是会去数据库层面采用函数计算出来,这种方式本来就存在一定的弊端

1、数据库层面是性能瓶颈,将所有的压力放在数据库中,必然会给系统带来灾难级的响应,例如:当同时访问的用户量递增时,数据库连接池打满 > CPU 飘升 > 系统长时间停留在数据库层面无法及时响应给用户
2、当服务门店/网点数据量越来越大时、服务工人数据越来越庞大时,在使用函数计算筛选出附近的数据,必然会造成数据库的全表扫描 >explain type:ALL
3、当最近的服务门店/网点、服务工人不满足用户的需求对象时,会一直向下拉取下一页的数据,直至筛选到满足自己的服务对象才停止,每一段的筛选都是一次性能极差的 SELECT

故而言之,因为这种问题的出现,不得已而从其他方面去考虑来提升地理位置这块的筛选动作,由数据库「磁盘存储经纬度」改为缓存「内存存储经纬度」来提升重复的查询操作

该文会演示从数据库层面 > 缓存层面,地理位置的优化提升改造

MySQL 数据库

现大部分企业都采用 MySQL 作为数据库存储,所以以 MySQL 8.0 为例,演练在它里面如何采用函数来完成地理位置的计算

表结构

CREATE TABLE `shop` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '门店id',
  `shop_no` varchar(64) NOT NULL COMMENT '门店编码',
  `shop_name` varchar(50) NOT NULL COMMENT '门店名称',
  `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '启用状态:1-启用、0-',
  `logo` varchar(255) DEFAULT NULL COMMENT '门店Logo',
  `introduce` text COMMENT '门店介绍',
  `longitude` double NOT NULL COMMENT '经度',
  `latitude` double NOT NULL COMMENT '纬度',
  `trade_start_time` time DEFAULT NULL COMMENT '营业开始时间',
  `trade_end_time` time DEFAULT NULL COMMENT '营业结束时间',
  `contacts` varchar(20) DEFAULT NULL COMMENT '联系人',
  `telephone` varchar(50) DEFAULT NULL COMMENT '商家联系电话',
  `province_id` bigint DEFAULT NULL COMMENT '省id',
  `province` varchar(255) DEFAULT NULL COMMENT '省',
  `city_id` bigint DEFAULT NULL COMMENT '市id',
  `city` varchar(255) DEFAULT NULL COMMENT '市',
  `area_id` bigint DEFAULT NULL COMMENT '区id',
  `area` varchar(255) DEFAULT NULL COMMENT '区',
  `address` varchar(255) DEFAULT NULL COMMENT '门店详细地址',
  `created_time` datetime DEFAULT NULL COMMENT '创建时间',
  `updated_time` datetime DEFAULT NULL COMMENT '更新时间',
  `is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uni_shop_no` (`shop_no`) COMMENT '门店编码唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=45 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商家信息';

首先创建一张商家表「门店/服务网点」涉及到地理位置比较重要的两个字段,longitude > 经度、latitude > 纬度

经度的最大值:180°
纬度的最大值:90°

模拟数据

使用存储函数,模拟生成十万条商家数据

CREATE DEFINER = `root` @`localhost` PROCEDURE `batchInsert` ( IN args INT ) BEGIN
	DECLARE
		-- 开启事务
		i INT DEFAULT 1;
	START TRANSACTION;
	WHILE
			i <= args DO
			INSERT INTO shop ( shop_no, shop_name, `status`, longitude, latitude ) 
		VALUE
			(
				ROUND( RAND() * 99999 ),
				concat( "商家-", i ),1,
				-- 随机生成经纬度
				(RAND() * ( 179.077090052913654 - 0.477040512464626 )) + 0.477040512464626,
				(RAND() * ( 89.9172823750000134 - - 1.8840792500000134 )) + - 1.8840792500000134 
			);
		SET i = i + 1;
	END WHILE;
	COMMIT;
END
call batchInsert(100000);

数据库查询

不加索引

先使用「经纬度」字段不加索引的方式执行 SQL

EXPLAIN SELECT
	* 
FROM
	( SELECT id, ST_DISTANCE_SPHERE ( POINT ( 114.112808, 22.544977 ), POINT ( longitude, latitude )) AS distance FROM shop WHERE `STATUS` = 1 ) temp 
WHERE
	ROUND( distance / 1000, 2 ) BETWEEN 0 AND 20 
ORDER BY distance ASC 
LIMIT 5 

执行计划结果如下:

在这里插入图片描述

加索引

alter table shop add index `idx_location` (`longitude`,`latitude`) USING BTREE;

再次执行 SQL,如下:

```sql
EXPLAIN SELECT
	* 
FROM
	( SELECT id, ST_DISTANCE_SPHERE ( POINT ( 114.112808, 22.544977 ), POINT ( longitude, latitude )) AS distance FROM shop WHERE `STATUS` = 1 ) temp 
WHERE
	ROUND( distance / 1000, 2 ) BETWEEN 0 AND 20 
ORDER BY distance ASC 
LIMIT 5 

执行计划结果如下:

在这里插入图片描述

直译函数

MySQL 官方直译 ST_DISTANCE_SPHERE 函数说明

语法:ST_Distance_Sphere(g1, g2 [, radius])
说明:

返回球体之间 Point 或 MultiPoint 参数之间的最小球面距离(以米为单位)可选 radius 参数应以米为单位给出
如果两个几何参数都是有效的笛卡尔参数 Point 或 MultiPoint SRID 0 中的值,则返回值是具有所提供半径的球体上两个几何之间的最短距离。如果省略,则默认半径为 6,370,986 米,点 X 和 Y 坐标分别解释为经度和纬度(以度为单位)

如果任何参数的经度或纬度超出范围,则会发生错误:

1、若经度值不在 (−180, 180] 范围内,则会发生 ER_GEOMETRY_PARAM_LONGITUDE_OUT_OF_RANGE 错误(在 MySQL 8.0.12 ER_LONGITUDE_OUT_OF_RANGE 之前)
2、若纬度值不在 [−90, 90] 范围内,则会发生 ER_GEOMETRY_PARAM_LATITUDE_OUT_OF_RANGE 错误(在 MySQL 8.0.12 ER_LATITUDE_OUT_OF_RANGE 之前)

小结

从以上数据库做地理位置筛选的结果来看,无论是否追加索引,似乎对数据库的查询性能来说,并没有提升

使用数据库做地理位置筛选,基于以下几种情况可以考虑使用该方式进行处理

商家「服务门店/网点」数据量不多
商家「服务门店/网点」模块提供给用户服务的入口较小

Redis 缓存

基于 Redis API 实现地理位置使用 GEO 有两种方式

1、org.springframework.data.redis.core.RedisTemplate
2、org.redisson.api.RedissonClient

Redis GEO 客户端

该篇节,先告知大家如何应用 Redis 客户端的 GEO 类型,API 会基于客户端的函数进行一次封装,先了解底层开始再到最后的高级 API 实践

1、查看 Redis 版本

redis-cli -v

2、连接 Redis 客户端

1、redis-cli
2、无密码直接登录,有密码通过:auth 明文密码

3、查看 GEO、ZSet 帮助文档

help @GEO
help @sorted-set

127.0.0.1:6379> help @GEO
  # GEO 指定的缓存 Key 追加 1~N 条经纬度地理位置信息	
  GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]
  summary: Add one or more geospatial items in the geospatial index represented using a sorted set
  since: 3.2.0
  # GEO 指定的缓存 Key 两个成员之间的距离
  # M|KM|FT|MI:米、公里、英里、英尺	
  GEODIST key member1 member2 [M|KM|FT|MI]
  summary: Returns the distance between two members of a geospatial index
  since: 3.2.0
  # GEO 指定缓存 Key 地理位置索引 > 标准地理散列字符串 
  GEOHASH key member [member ...]
  summary: Returns members of a geospatial index as standard geohash strings
  since: 3.2.0
  # GEO 指定缓存 Key 地理位置索引 > 成员对应的经纬度 
  GEOPOS key member [member ...]
  summary: Returns longitude and latitude of members of a geospatial index
  since: 3.2.0
  # GEO 指定缓存 Key:查询表示地理空间索引的排序集,以传入的经纬度来获取与点的给定最大距离匹配的成员,可按升序、降序排序
  GEORADIUS key longitude latitude radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
  summary: Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point
  since: 3.2.0
  # GEO 指定缓存 Key: 查询表示地理空间索引的排序集,以传入的指定成员经纬度来获取与点的给定最大距离匹配的成员,可按升序、降序排序
  GEORADIUSBYMEMBER key member radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
  summary: Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member
  since: 3.2.0
  # GEO 指定缓存 Key: 查询表示地理空间索引的排序集,以传入的指定成员经纬度来获取与点的给定最大距离匹配的成员,可按升序、降序排序,只支持可读
  GEORADIUSBYMEMBER_RO key member radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC]
  summary: A read-only variant for GEORADIUSBYMEMBER
  since: 3.2.10
  # GEO 指定缓存 Key:查询表示地理空间索引的排序集,以传入的经纬度来获取与点的给定最大距离匹配的成员,可按升序、降序排序,只支持可读
  GEORADIUS_RO key longitude latitude radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC]
  summary: A read-only variant for GEORADIUS
  since: 3.2.10
  # GEO 指定缓存 Key:查询表示地理空间索引的排序集,以获取「成员或指定经纬度」最大距离匹配的成员,可按升序、降低排序,不支持存储
  GEOSEARCH key FROMMEMBER member|FROMLONLAT longitude latitude BYRADIUS radius M|KM|FT|MI|BYBOX width height M|KM|FT|MI [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]
  summary: Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle.
  since: 6.2.0
  # GEO 指定缓存 Key:查询表示地理空间索引的排序集,以「获取成员或指定经纬度」最大距离匹配的成员,可按升序、降低排序,支持存储至 ZSet Key 
  GEOSEARCHSTORE destination source FROMMEMBER member|FROMLONLAT longitude latitude BYRADIUS radius M|KM|FT|MI|BYBOX width height M|KM|FT|MI [ASC|DESC] [COUNT count [ANY]] [STOREDIST]
  summary: Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle, and store the result in another key.
  since: 6.2.0

引入 Spring、Redisson 配置

1、maven 依赖配置

<properties>
    <spring.boot.version>2.6.7</spring.boot.version>
    <redisson.version>3.17.5</redisson.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring.boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
    		<groupId>org.redisson</groupId>
    		<artifactId>redisson-spring-boot-starter</artifactId>
    		<version>${redisson.version}</version>
		</dependency>
    </dependencies>
</dependencyManagement>

2、Redis 核心配置类,如下:

/**
 * Redis 核心配置类
 *
 * @author vnjohn
 * @since 2023
 */
@Configuration
public class RedisConfig {
    @Resource
    private RedisConnectionFactory factory;

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setStringSerializer(new StringRedisSerializer());
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
    
    @Bean
    public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }

    @Bean
    public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
        return redisTemplate.opsForValue();
    }

    @Bean
    public GeoOperations<String, String> geoOperations(RedisTemplate<String, String> redisTemplate) {
        return redisTemplate.opsForGeo();
    }

    @Bean
    public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }

    @Bean
    public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }

    @Bean
    public ZSetOperations<String, String> zSetOperations(RedisTemplate<String, String> redisTemplate) {
        return redisTemplate.opsForZSet();
    }
}

在本文,我们会用到 GeoOperations、ZSetOperations 操作类去调用 API

RedisTemplate API 操作

RedisTemplate 操作工具类,如下:

	@Resource
    private GeoOperations<String, String> geoOperations;
	
	@Resource
    private ZSetOperations<String, String> zSetOperations;

// ============================ sorted-set =============================

	public ZSetOperations.TypedTuple<String> redisTemplateZSetPopMinScore(String key) {
        return zSetOperations.popMin(key);
    }
	
// ============================ Geo =============================

    /**
     * 新增 Geo 某个 Key 成员的经纬度信息
     *
     * @param key       Redis 缓存 Key
     * @param longitude 经度
     * @param latitude  纬度
     * @param member    成员
     */
    public void geoAdd(String key, Double longitude, Double latitude, String member) {
        Point point = new Point(longitude, latitude);
        geoOperations.add(key, point, member);
    }

    /**
     * 删除 Geo 某个 Key 成员的经纬度信息
     *
     * @param key    Redis 缓存 Key
     * @param member 成员
     */
    public void geoRemove(String key, String member) {
        geoOperations.remove(key, member);
    }

    /**
     * 以半径为单位,「千米」为计算单位展开,以倒序的方式展示对应的信息
     *
     * @param key         缓存 Key
     * @param longitude   经度
     * @param latitude    纬度
     * @param distanceNum 距离,单位:KM
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadiusWithKilometers(String key, Double longitude,
                                                                                    Double latitude, Double distanceNum) {
        return geoRadiusWithKilometers(key, longitude, latitude, distanceNum, null, Boolean.TRUE);
    }

    /**
     * 以半径为单位,「千米」为计算单位展开,以倒序的方式展示对应的信息
     *
     * @param key         缓存 Key
     * @param longitude   经度
     * @param latitude    纬度
     * @param limit       筛选条数
     * @param distanceNum 距离,单位:KM
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadiusWithKilometersLimit(String key, Double longitude, Double latitude,
                                                                                         Integer limit, Double distanceNum) {
        return geoRadiusWithKilometers(key, longitude, latitude, distanceNum, limit, Boolean.TRUE);
    }

    /**
     * 以半径为单位,「千米」为计算单位展开,以自定义顺序方式展示对应的信息
     *
     * @param key         缓存 Key
     * @param longitude   经度
     * @param latitude    纬度
     * @param limit       筛选条数
     * @param distanceNum 距离,单位:KM
     * @param ascOrder    是否按升序排
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadiusWithKilometers(String key, Double longitude, Double latitude,
                                                                                    Double distanceNum, Integer limit, Boolean ascOrder) {
        Point point = new Point(longitude, latitude);
        Distance radius = new Distance(distanceNum, Metrics.KILOMETERS);
        Circle within = new Circle(point, radius);
        RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeCoordinates().includeDistance();
        if (null != limit) {
            geoRadiusCommandArgs = geoRadiusCommandArgs.limit(limit);
        }
        geoRadiusCommandArgs = ascOrder ? geoRadiusCommandArgs.sortAscending() : geoRadiusCommandArgs.sortDescending();
        return geoOperations.radius(key, within, geoRadiusCommandArgs);
    }

    /**
     * 以半径为单位,「米」为计算单位展开,以倒序的方式展示对应的信息
     *
     * @param key         缓存 Key
     * @param longitude   经度
     * @param latitude    纬度
     * @param distanceNum 距离,单位:M
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadiusWithMeters(String key, Double longitude,
                                                                                Double latitude, Double distanceNum) {
        return geoRadiusWithMeters(key, longitude, latitude, distanceNum, true);
    }

    /**
     * 以半径为单位,「米」为计算单位展开,以自定义顺序方式展示对应的信息
     *
     * @param key         缓存 Key
     * @param longitude   经度
     * @param latitude    纬度
     * @param distanceNum 距离,单位:M
     * @param ascOrder    是否按升序排
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadiusWithMeters(String key, Double longitude,
                                                                                Double latitude, Double distanceNum,
                                                                                Boolean ascOrder) {
        Point point = new Point(longitude, latitude);
        Distance radius = new Distance(distanceNum, Metrics.NEUTRAL);
        Circle within = new Circle(point, radius);
        RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeCoordinates().includeDistance();
        geoRadiusCommandArgs = ascOrder ? geoRadiusCommandArgs.sortAscending() : geoRadiusCommandArgs.sortDescending();
        return geoOperations.radius(key, within, geoRadiusCommandArgs);
    }

	public Long redisTemplateStoreSortedSearchTo(String destName, String key, Double longitude, Double latitude,
                                                 Double distanceNum, Integer limit, Boolean ascOrder) {

        Distance distance = new Distance(distanceNum, Metrics.KILOMETERS);
        RedisGeoCommands.GeoSearchStoreCommandArgs geoSearchStoreCommandArgs = RedisGeoCommands.GeoSearchStoreCommandArgs.newGeoSearchStoreArgs();
        geoSearchStoreCommandArgs.limit(limit);
        geoSearchStoreCommandArgs.sort(ascOrder ? Sort.Direction.ASC : Sort.Direction.DESC);
        GeoReference geoReference = GeoReference.fromCoordinate(longitude, latitude);
        Long searchAndStore = geoOperations.searchAndStore(key, destName, geoReference, distance, geoSearchStoreCommandArgs);
        return searchAndStore;
    }

1、geoAdd 方法 -> GEOADD 函数
2、geoRemove 方法 -> ZREM 函数

GEO 存储起来以后放在 Redis 中是以 ZSet 结构进行存储的,所以将 GEO 某个元素删除时,就调用 ZREM 函数进行删除即可

3、geoRadiusWithKilometers、geoRadiusWithMeters 方法操作的都是相同的函数,只是筛选距离的单位不同,一个是千米、一个是米,它们对应的函数有两个,GEORADIUS — 筛选附近距离的满足元素、GEORADIUS_RO — 筛选附近距离的满足元素,只支持可读

具体的方法执行逻辑可以查看以下方法源码:RedisGeoCommands#GeoRadiusCommandArgs,该方法主要对我们传入的参数进行一次封装,转换为 Redis 中可识别的函数参数可选项

public GeoResults<GeoLocation<byte[]>> geoRadius(byte[] key, Circle within, GeoRadiusCommandArgs args) {
   List<Object> params = new ArrayList<Object>();
   params.add(key);
   params.add(convert(within.getCenter().getX()));
   params.add(convert(within.getCenter().getY()));
   params.add(within.getRadius().getValue());
   params.add(getAbbreviation(within.getRadius().getMetric()));
   
   RedisCommand<GeoResults<GeoLocation<byte[]>>> command;
   if (args.getFlags().contains(GeoRadiusCommandArgs.Flag.WITHCOORD)) {
       command = new RedisCommand<GeoResults<GeoLocation<byte[]>>>("GEORADIUS_RO", postitionDecoder);
       params.add("WITHCOORD");
   } else {
       MultiDecoder<GeoResults<GeoLocation<byte[]>>> distanceDecoder = new ListMultiDecoder2(new GeoResultsDecoder(within.getRadius().getMetric()), new GeoDistanceDecoder());
       command = new RedisCommand<GeoResults<GeoLocation<byte[]>>>("GEORADIUS_RO", distanceDecoder);
       params.add("WITHDIST");
   }
   
   if (args.getLimit() != null) {
       params.add("COUNT");
       params.add(args.getLimit());
   }
   if (args.getSortDirection() != null) {
       params.add(args.getSortDirection().name());
   }
   
   return read(key, ByteArrayCodec.INSTANCE, command, params.toArray());
}

引入 RedisTemplate API 有一些特性,我们在实际应用中可能应用不到,如:

1、当 GEO 中某个成员不知道它是否存在,当不存在时可以直接新增,存在时不做任何变更,RedisTemplate API 需要操作两次函数:geoRemove、geoAdd,而下面要讲解的 Redisson API 直接可以通过一个函数搞定,好处:减少一次与 Redis 之间的连接,提高操作效率
2、使用 Redisson 客户端,实现「搜索满足距离条件的成员列表」功能时更加的便捷

若 Redisson 版本不对时,会在操作 redisTemplateZSetPopMinScore 方法时,出现如下异常:

java.lang.StackOverflowError: null at org.springframework.data.redis.connection.DefaultedRedisConnection.zPopMin(DefaultedRedisConnection.java:973)
解决办法:将 Redisson 版本降低到 3.15.6

Redisson API 操作

Redisson 操作工具类,如下:

private static final StringCodec REDISSON_CODE_C = new StringCodec();

@Resource
private RedissonClient redissonClient;

// ============================ ZSet Redisson =============================

public String redissonZSetPopMinScore(String key) {
    RScoredSortedSet<Object> scoredSortedSet = redissonClient.getScoredSortedSet(key, REDISSON_CODE_C);
    return (String) scoredSortedSet.pollFirst();
}

// ============================ Geo Redisson =============================

/**
 * 获取 Redisson GEO 类型客户端实例
 *
 * @param key 缓存 Key
 * @return 基于 Redisson GEO 操作的客户端实例
 */
private RGeo<String> getRGeoClient(String key) {
    return redissonClient.getGeo(key, REDISSON_CODE_C);
}

/**
 * 若存在的话,替换 Geo 某个 Key 成员的经纬度信息
 */
public Boolean redissonGeoAddIfExists(String key, Object member, Double longitude, Double latitude) {
    RGeo<String> geo = getRGeoClient(key);
    return geo.addIfExists(new GeoEntry(longitude, latitude, member)) > 0;
}

/**
 * 删除指定 Key > 多个 Member 元素
 *
 * @param key     缓存 Key
 * @param members 成员列表
 */
public void redissonGeoRemove(String key, List<Long> members) {
    RGeo<String> geo = getRGeoClient(key);
    geo.removeAll(members);
}

/**
 * 新增 Geo 某个 Key 成员的经纬度信息
 *
 * @param key       缓存 Key
 * @param member    成员
 * @param longitude 经度
 * @param latitude  纬度
 */
public void redissonGeoAdd(String key, Object member, Double longitude, Double latitude) {
    RGeo<String> geo = getRGeoClient(key);
    geo.add(new GeoEntry(longitude, latitude, member));
}

/**
 * 搜索满足距离条件的成员列表
 *
 * @param key         缓存 Key
 * @param longitude   经度
 * @param latitude    纬度
 * @param distanceNum 距离:KM
 * @return 匹配到的成员记录及距离
 * @see GeoUnit geoUnit
 * 以半径为中心距离,「geoUnit」为计算单位展开,以距离优先展示对应的信息
 */
public Map<String, Double> searchWithDistance(String key, Double longitude, Double latitude, Double distanceNum) {
    return searchWithDistance(key, longitude, latitude, distanceNum, GeoUnit.KILOMETERS, null);
}

/**
 * 搜索满足距离条件的成员列表
 *
 * @param key         缓存 Key
 * @param longitude   经度
 * @param latitude    纬度
 * @param distanceNum 距离:KM
 * @return 匹配到的成员记录及距离
 * @see GeoUnit geoUnit
 * 以半径为中心距离,「geoUnit」为计算单位展开,以距离优先展示对应的信息
 */
public Map<String, Double> searchWithDistance(String key, Double longitude, Double latitude, Double distanceNum, Integer limit) {
    return searchWithDistance(key, longitude, latitude, distanceNum, GeoUnit.KILOMETERS, limit);
}

/**
 * 搜索满足距离条件的成员列表
 *
 * @param key         缓存 Key
 * @param longitude   经度
 * @param latitude    纬度
 * @param distanceNum 距离
 * @param geoUnit     距离单位
 * @param limit       筛选条数
 * @return 匹配到的成员记录及距离
 * @see GeoUnit geoUnit
 * 以半径为中心距离,「geoUnit」为计算单位展开,以距离优先展示对应的信息
 */
public Map<String, Double> searchWithDistance(String key, Double longitude, Double latitude, Double distanceNum, GeoUnit geoUnit, Integer limit) {
    RGeo<String> geo = getRGeoClient(key);
    GeoSearchArgs args;
    if (null != limit) {
        args = GeoSearchArgs.from(longitude, latitude).radius(distanceNum, geoUnit).order(GeoOrder.ASC).count(limit);
    } else {
        args = GeoSearchArgs.from(longitude, latitude).radius(distanceNum, geoUnit).order(GeoOrder.ASC);
    }
    return geo.searchWithDistance(args);
}

/**
 * 存储搜索满足条件的成员列表
 *
 * @param destName    存储 ZSet Key
 * @param key         搜索目标 Key
 * @param longitude   经度
 * @param latitude    纬度
 * @param distanceNum 距离
 */
public Boolean storeSortedSearchTo(String destName, String key, Double longitude, Double latitude,
                                   Double distanceNum) {
    return storeSortedSearchTo(destName, key, longitude, latitude, distanceNum, GeoUnit.KILOMETERS, null);
}

/**
 * 此处的应用场景:
 * 1、当用户下单以后,通过该方法将用户下单所在经纬度最近的工人都统计出来
 * 2、统计出来的数据再次进行一次比对,若工人未开启接单,那么该工人所在元素会被移除掉
 * 3、当工人端拒绝接单,那么该工人所在元素从 ZSET 中移除
 * 4、当工人端已接单并且开始服务,那么该用户所在的统计数据可被移除
 *
 * @param destName    存储 ZSet Key
 * @param key         搜索目标 Key
 * @param longitude   经度
 * @param latitude    纬度
 * @param distanceNum 距离
 * @param geoUnit     距离单位
 * @param limit       条数
 */
public Boolean storeSortedSearchTo(String destName, String key, Double longitude, Double latitude,
                                   Double distanceNum, GeoUnit geoUnit, Integer limit) {
    RGeo<String> geo = getRGeoClient(key);
    GeoSearchArgs args;
    if (null != limit) {
        args = GeoSearchArgs.from(longitude, latitude)
                            .radius(distanceNum, geoUnit)
                            .order(GeoOrder.ASC)
                            .count(limit);
    } else {
        args = GeoSearchArgs.from(longitude, latitude)
                            .radius(distanceNum, geoUnit)
                            .order(GeoOrder.ASC);
    }
    return geo.storeSortedSearchTo(destName, args) > 0;
}

Redisson 中对不同的编码还进行了优化,若知道当前存储或查询的元素属于非字符类型,可以通过以下类型来指定:

1、字符型:StringCodec,默认使用 UTF-8 编码方式
2、字节数组型:ByteArrayCodec
3、整型:IntegerCodec
4、浮点型:DoubleCodec

它们共同的父类为 BaseCodec,除了字符型,其他的编码类型都有实现各自的解码器

1、redissonGeoAdd 方法 -> GEOADD 函数
2、redissonGeoRemove 方法 -> ZREM 函数

与 RedisTemplate API 一致,GEO 存储起来以后放在 Redis 中是以 ZSet 结构进行存储的,所以将 GEO 某个元素删除时,就调用 ZREM 函数进行删除即可

3、redissonGeoAddIfExists -> GEOPOS、GEOADD 函数一起组合使用的

可观察该方法的实现:RedissonGeo#addIfExistsAsync,内部使用 Redis Lua 脚本实现了这两个函数的组合运用,当 GEOPOS 返回的数据为真时,那么就调用 GEOADD 函数将当前元素存入 GEO Key 中

4、searchWithDistance 方法,它对应的函数有两个,GEORADIUS — 筛选附近距离的满足元素、GEORADIUS_RO — 筛选附近距离的满足元素,只支持可读
5、storeSortedSearchTo 方法,将筛选出来的内容存储到一个新的 ZSet Key 中

应用场景如下:当用户在某个地点下单以后,需要筛选它附近可派单的工人,可筛选指定人数(只要满足服务距离条件)存储到新的 Key 中,当存储完成以后,即使第一个被派单的工人取消服务了,可以利用 ZSet 作为一个栈的结构,按照最近或最远的方式进行一个一个的弹出来 > Pop,结合 redissonZSetPopMinScore 方法天衣无缝!!

小结

1、若要使用 RedisTemplate API 中的 redisTemplateStoreSortedSearchTo 方法或者使用 Redisson API 中的 storeSortedSearchTo 方法,Redis 服务端的版本必须高于或等于 6.2.0

在这里插入图片描述

这两个方法对应 Redis 中的 GEOSEARCHSTORE 函数,可以使用 help GEOSEARCHSTORE 命令,结合帮助文档运用起来

2、在如何考虑是否引入一个新的组件,来减少对数据库造成的压力,就需要看地理位置这块筛选的工作数据量有多大了,数据量大的话,宁愿基于内存来完成地理位置筛选,也不要将查询数据压力放在基于磁盘的数据库

3、引入一个新的组件,必然而然会考虑到引入这个组件会带来哪些问题,那么又要解决好组件给我们的问题了,数据存储到内存中并不可靠,所以在对引入 Redis 组件时,我们要把它的持久化机制考虑进去,结合 Redis 保证地理位置查询性能高效、持久化机制保证数据可靠

Redis 持久化机制类型:AOF、RDB
1、采用 AOF 方式进行持久化,一行一行 Redis 命令会入文件,会导致文件过大,从而造成恢复数据速度会很慢,也会给机器磁盘带来存储压力,好处就是能保证数据基本不丢失
2、采用 RDB 方式进行持久化,会导致一部分数据在瞬时丢失,从而就导致了数据存储不可靠,好处就是恢复速率快
3、结合以上两种方式都有缺点,AOF+RDB 结合作为持久化方式,不仅仅用到了 AOF 数据可靠性也用到了 RDB 恢复数据的效率性

Redis 持久化机制 AOF、RDB、AOF+RDB 方式的详细内容,会在后续有文章进行介绍,敬请期待!!

总结

该篇博文,主要先是进行「地理位置」生产性能问题的全流程演化,从 MySQL -> +索引 -> 不 + 索引,使用了案例 SQL 进行执行计划的分析,从而得出了 MySQL 在特殊场景下不适用于做地理位置的筛选工作「因为它本身基于磁盘的,在大数据量情况下,不能肆意打压瓶颈」;随即采用了 Redis GEO 类型来优化了地理位置的筛选工作,结合 RedisTemplate、Redisson 客户端 API 实战函数进行讲解,从零到一教你如何运用程序结合 Redis GEO 数据类型完成地理位置的优化工程,希望此博文你能够喜欢!

🌟🌟🌟愿你我都能够在寒冬中相互取暖,互相成长,只有不断积累、沉淀自己,后面有机会自然能破冰而行!

博文放在 Redis 专栏里,欢迎订阅,会持续更新!

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1030989.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux下的网络编程——网络基础、socket编程(一)

前言&#xff1a; 前面我们学习了Linux的系统编程&#xff0c;从今天我们就要开始Linux网络编程的学习了&#xff0c;Linux网络编程中的知识点可能没有前面的Linux系统多一点&#xff0c;但是基础的网络知识我们还是需要了解的&#xff0c;并且网络编程中的socket编程的知识也…

打赏收款收银台多合一支付收款HTML源码

多合一打赏支付收款&#xff08;微信、QQ、支付宝&#xff09; 源码直接上传到服务器解压缩即可访问,或者用本地浏览器打开访问 PS&#xff1b;只需要将自己的收款二维码替换即可 该源码全开源无加密&#xff0c;所有操作均在本地无需调用外部资源防止失效&#xff01; 源码…

微前端架构的几种技术选型

微前端架构的几种技术选型随着SPA大规模的应用&#xff0c;紧接着就带来一个新问题&#xff1a;一个规模化应用需要拆分。 一方面功能快速增加导致打包时间成比例上升&#xff0c;而紧急发布时要求是越短越好&#xff0c;这是矛盾的。另一方面当一个代码库集成了所有功能时&am…

【校招VIP】java语言考点之反射

考点介绍&#xff1a; java的反射(reflection)机制是指在程序的运行状态中&#xff0c;可以构造任意一个类的对象&#xff0c;可以了解任意一个对象所属的类&#xff0c;可以了解任意一个类的成员变量和方法&#xff0c;可以调用任意一个对象的属性和方法。这种动态获取程序信息…

探索工业路由器如何助力无人驾驶方案的突破性解析

随着无人驾驶技术的发展&#xff0c;越来越多的企业和组织开始部署无人驾驶车辆来提高运输效率和安全性。在这些方案中&#xff0c;工业路由器被广泛应用于建立稳定、安全和高效的通信网络。在本篇文章中&#xff0c;我们将分享一个真实的无人驾驶方案部署案例&#xff0c;其中…

laravel设置与获取header请求头

laravel设置与获取header请求头 设置 <?phpnamespace App\Http\Controllers\Text;use Illuminate\Http\Request; use App\Http\Controllers\Controller;class TextController extends Controller {public function TextCC(Request $request){$token $request->header(j…

【函数进阶】

函数进阶 1 本节目标2 函数的定义和调用2.1 函数的定义方式2.2 函数的调用方式 3 this3.1 函数内 this 的指向3.2 改变函数内部 this 的指向3.2.1 call方法3.2.2 apply方法3.2.3 bind方法3.2.4 call apply bind 总结 4 严格模式4.1 什么是严格模式4.2 开启严格模式4.2.1 为脚本…

【校招VIP】交流技巧之面试时合理表达观点

考点介绍&#xff1a; 交流和表达是产品的面试最重要的考查点之一&#xff0c;也是产品必备工作技能。如果在面试中不能合理的与面试官沟通&#xff0c;或者不能把自己的思路和分析有逻辑的表达出来&#xff0c;都会对面试结果产生不好的影响。 交流技巧之面试时合理表达观点-…

【网络协议】Http-下

因为Http是无状态的&#xff0c;所以为了协助 Web 保持状态&#xff0c;Cookie 诞生了。 下面中是百度百科关于Cookie和Session的解释&#xff1a; Cookie&#xff1a;举例来说, 一个 Web 站点可能会为每一个访问者产生一个唯一的ID, 然后以 Cookie 文件的形式保存在每个用户的…

序列化 qmap

自定义的map不得行 但是qmap可以自己分配具体内存 这里没照完 待会照

通讯网关软件009——利用CommGate X2MQTT实现MQTT访问ODBC数据源

本文介绍利用CommGate X2MQTT实现MQTT访问ODBC数据源。CommGate X2MQTT是宁波科安网信开发的网关软件&#xff0c;软件可以登录到网信智汇(http://wangxinzhihui.com)下载。 【案例】如下图所示&#xff0c;实现上位机通过MQTT来获取ODBC数据源的数据。 【解决方案】设置网关机…

「Qt中文教程指南」如何创建基于Qt Widget的应用程序(一)

Qt 是目前最先进、最完整的跨平台C开发工具。它不仅完全实现了一次编写&#xff0c;所有平台无差别运行&#xff0c;更提供了几乎所有开发过程中需要用到的工具。如今&#xff0c;Qt已被运用于超过70个行业、数千家企业&#xff0c;支持数百万设备及应用。 本文描述了如何使用…

【Java 并发编程】CopyOnWriterArrayList 详解

CopyOnWriterArrayList 详解 1. ArrayList1.1 ArrayList 和 LinkedList 的区别1.2 ArrayList 如何保证线程安全 2. CopyOnWriteArrayList 原理3. CopyOnWriteArrayList 的优缺点3.1 优点3.2 缺点 4. 源码分析4.1 两个成员变量4.2 构造函数4.3 add(E e)4.4 add(int index, E ele…

(总目录)springboot - 实现zip文件上传并对zip文件解压, 包含上传oss

全文目录,一步到位 1.本文概述1.1 本文简介 2. 功能实现2.1 统一文件校验2.2 普通(多)文件上传[服务器]2.2.1 controller层2.2.2 service层2.2.3 业务impl实现类2.2.4 FileIOUtils工具包代码 2.3 zip文件的解压2.4 图片文件的压缩2.5 oss文件后端上传2.6 oss文件前端上传2.7 后…

传统企业如何实现数字化转型?

传统企业实现数字化转型是一个复杂且多方面的过程&#xff0c;涉及将数字技术和战略融入业务的各个方面&#xff0c;以推动创新、效率和竞争力。以下是传统企业实现数字化转型可以遵循的步骤和策略&#xff1a; 1.领导层的认可和愿景&#xff1a; 首先要确保最高领导层&#x…

SpringCloud Gateway搭建Gateway 微服务应用实例

&#x1f600;前言 本篇博文是关于SpringCloud Gateway搭建Gateway 微服务应用实例&#xff0c;希望你能够喜欢 &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以帮助到大家&#xff0c;您…

前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— JS基础(三)

思维导图 一、循环-for 1.1 for 循环-基本使用 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport"…

scryptTS文档搜索功能上线!

在 scryptTS 文档中搜索 随着 scryptTS 文档的内容越来越丰富&#xff0c;从大量资料中快速定位感兴趣的部分变得越来越困难。 现在&#xff0c;你可以使用搜索功能&#xff0c;快速查找想了解的内容。

金蝶云星空与聚水潭对接集成物料查询连通商品上传(新)(物料主数据同步策略)

金蝶云星空与聚水潭对接集成物料查询连通商品上传&#xff08;新&#xff09;(物料主数据同步策略) 数据源系统:金蝶云星空 金蝶K/3Cloud结合当今先进管理理论和数十万家国内客户最佳应用实践&#xff0c;面向事业部制、多地点、多工厂等运营协同与管控型企业及集团公司&#x…

肖sir__项目环境之全流程__005

一、测试流程&#xff08;h模型&#xff09; 1、需求文档&#xff08;产品&#xff09; 需求文档&#xff08;软件需求规格说明书srs&#xff09; &#xff08;1&#xff09;如何分析需求 a、显示需求&#xff08;主流程、功能&#xff0c;业务&#xff09; b、隐性需求&#x…