基于redis(v3.2+)实现“附近的人”功能

背景介绍:目前随着电商、社交、游戏和代购等的流行,“附近的人”这一功能提供了一种便捷的方式允许同一地区或者一定距离范围内的用户进行相互交流的途径,一般都是在用户点击某个菜单或按钮时记录用户的坐标信息,拿微信的“附近的人”功能举例子,如下图所示,

当你在点击附近的人时微信服务端会提示获取你所在位置的经纬度,记录到服务端,右上角的小脚印就表示你的经纬度信息被记录。然后服务端会根据你的位置信息拉取附近同样在服务器端有位置记录的用户信息,按照距离进行排序。一般来说“附近的人”功能只要能否大体反应距你多少米或千米范围内有XX用户即可,这句话体现了两个知识点:对精度要求不高和一定范围内(具体指多少M或KM)的用户;目前“附近的人”实现方式有很多,各有利弊,本文基于Redis(v3.2+)实现,redis3.2版本起,提供了以geo为前缀的命令采用geohash用于存储地理位置坐标信息,并对储存的地理位置信息进行操作,常用命令如下:

# GEOADD 用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称##(member)添加到指定的 key 中。
GEOADD key longitude latitude member [longitude latitude member ...]

# geopos 用于从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil
GEOPOS key member [member ...]

# geodist 用于返回两个给定位置之间的距离
GEODIST key member1 member2 [m|km|ft|mi]

# georadius 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

# georadiusbymember 和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是georadiusbymember 的中心点是由给定的位置元素决定的, 而不是使用经度和纬度来决定中心点。
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

# GEOHASH 用于获取一个或多个位置元素的 geohash 值。
GEOHASH key member [member ...]

简单演示下,假设有两个位置,通过地图拾取坐标系统获取到北京南站( 116.387021,39.873306)和北京西站(116.327805,39.900766),然后在地图上大体测距两个地点距离为5.9公里。

那么下面看看,如果利用redis的GEO命令计算两个站之间的距离吧,执行过程如下,可以看出利用GEODIST命令计算出来的两个位置之间的距离和地图标注的距离大致一样,要记住Redis GEO采用geohash来保存地理位置坐标,误差肯定是存在的,在实现功能时要考虑如何消减误差产生的影响:

比如我目前位于北京动物园(116.344478,39.946361),我想看看6KM范围内是否有高铁站,两种思路:要么先GEOADD添加,然后再利用GEORADIUSBYMEMBER或者利用GEORADIUS命令来实现该功能都可以:

好的,现在开始编码:

1、新建一个SpringBoot项目,并引入spring-boot-redis依赖,项目结构和pom.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>thinking-in-spring-boot</groupId>
    <artifactId>first-app-by-gui</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>first-app-by-gui</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.60</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.22</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--		<dependency>-->
        <!--			<groupId>org.springframework.boot</groupId>-->
        <!--			<artifactId>spring-boot-loader</artifactId>-->
        <!--			<scope>provided</scope>-->
        <!--		</dependency>-->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2、向application.yml项目配置文件中新增redis配置:

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    password:
    timeout: 1000

3、编写Redis配置类(RedisConfig)。需要注意的是由于SpringBoot默认只支持对String类型的数据缓存操作,像redis的geo操作,就涉及到复杂数据类型,所以大多数情况下都需要单独编写一个Redis配置类通过丰富RedisTemplate功能来实现对String之外的类型缓存操作,另外一般不直接暴露RedisTemplate给业务代码,需要提供进一步封装,具体实现因项目而异,本次只做简单实现,不做特殊要求:

package com.dongnao.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@EnableCaching
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);

        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSeial.setObjectMapper(om);

        // 值采用json序列化
        template.setValueSerializer(jacksonSeial);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());

        // 设置hash key 和value序列化模式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jacksonSeial);
        template.afterPropertiesSet();
        return template;
    }

}

4、编写测试类,内容如下,示例代码中含有添加用户坐标信息到redis中,也有基于特定坐标降序或升序查询多少距离以内的用户列表操作,简单来说针对redis的geo命令,springboot-redis api中有对应封装,结合实际业务情形使用即可:

package com.dongnao;

import com.alibaba.fastjson.JSON;
import lombok.Data;
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.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.ArrayList;
import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class FirstAppByGuiApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    //GEO相关命令用到的KEY
    private final static String KEY = "position";

    @Test
    public void test() {
        // 1、初始化用户坐标数据
//        initData();
        // 获取距离(116.844478,39.146161)这个坐标点100公里以内的用户信息
        List<User> users = nearBySearch(100, 116.844478, 39.146161);
        System.out.println(JSON.toJSONString(users));
    }

    /**
     * 初始化用户坐标数据
     */
    private void initData() {
        redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation<>(
                "user01",
                new Point(116.344478, 39.946161))
        );
        redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation<>(
                "user02",
                new Point(116.345478, 39.946261))
        );
        redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation<>(
                "user03",
                new Point(116.346878, 39.946361))
        );
        redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation<>(
                "user04",
                new Point(116.34318, 39.946341))
        );
        redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation<>(
                "user05",
                new Point(116.344978, 39.946561))
        );
    }

    /**
     * 获取距指定位置distance公里范围内的用户信息
     *
     * @param distance 距离
     * @param userLng  经度
     * @param userLat  维度
     * @return
     */
    public List<User> nearBySearch(double distance, double userLng, double userLat) {
        List<User> users = new ArrayList<>();
        // 1、等价于GEORADIUS position 116.344478 39.9463616 6 "km" "WITHDIST" "WITHCOORD" "ASC"
        GeoResults<RedisGeoCommands.GeoLocation<Object>> reslut =
                redisTemplate.opsForGeo().radius(KEY,
                        new Circle(new Point(userLng, userLat), new Distance(distance, Metrics.KILOMETERS)),
                        RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
                                .includeDistance()
                                .includeCoordinates().sortAscending());
        //2、遍历并封装GEO结果
        List<GeoResult<RedisGeoCommands.GeoLocation<Object>>> content = reslut.getContent();
        content.forEach(a -> users.add(
                new User().setDistance(a.getDistance().getValue())
                        .setLatitude(a.getContent().getPoint().getX())
                        .setLongitude(a.getContent().getPoint().getY()).setUsername(a.getContent().getName().toString())));
        return users;
    }

    @Data
    class User {

        private String username;

        private Double latitude;

        private Double longitude;

        private Double distance;

        public User setUsername(String username) {
            this.username = username;
            return this;
        }

        public User setLatitude(Double latitude) {
            this.latitude = latitude;
            return this;
        }

        public User setLongitude(Double longitude) {
            this.longitude = longitude;
            return this;
        }

        public User setDistance(Double distance) {
            this.distance = distance;
            return this;
        }
    }

}

运行测试代码,结果如下,按照距离升序,返回用户信息:

[
  {
    "distance": 98.706,
    "latitude": 116.34688049554825,
    "longitude": 39.94636014167523,
    "username": "user03"
  },
  {
    "distance": 98.7481,
    "latitude": 116.34548038244247,
    "longitude": 39.946261287550016,
    "username": "user02"
  },
  {
    "distance": 98.7752,
    "latitude": 116.3444772362709,
    "longitude": 39.94615989870364,
    "username": "user01"
  },
  {
    "distance": 98.7968,
    "latitude": 116.34497612714767,
    "longitude": 39.94656038464682,
    "username": "user05"
  },
  {
    "distance": 98.8416,
    "latitude": 116.3431790471077,
    "longitude": 39.946339863905955,
    "username": "user04"
  }
]

讲完了使用,再来说说缺点:如上结果所示,只存储对象唯一识别信息,不便进行复杂对象存储和多条件查询等操作。技术就是这样,没有最好,只有更合适些的,办法总比困难多!

以上,完了!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
涉及内容:注意,学习此视频必须有一定基础的同学区块链相关知识、钱包相关知识、p2p相关知识、uniapp相关知识    01前言    02成果展示    03前言区块链概念和六层模型介绍    04翻译一个带币的js简单例子原理介绍    05区块链springboot工程搭建和区块相关实现    06区块链中加密算法相关介绍以及实现    07钱包相关实体类介绍    08redis数据库等配置和工具类的介绍    09区块链相关实体类介绍    10挖矿相关实体类和POW相关的介绍    11 p2p 点对点 server和client代码    12 p2p 原理的简单介绍    13 节点钱包相关启动实现    14 web控制层逻辑基础代码    15 web功能的整体介绍    16 web钱包功能-创建钱包账户的完整功能实现    17 web钱包功能-获取挖矿钱包信息和根据钱包地址获取信息    18 web钱包功能-获取当前节点所有钱包    19 全节点钱包轻钱包中心化钱包的概念    20 p2p三个节点的相关配置并启动    21 p2p 节点添加相关流程    22 p2p 节点列表相关实现    23 区块相关挖矿与挖矿奖励等讲解    24 区块链相关的查询操作    25 交易转账相关逻辑    26 三台机器节点运行 uniapp开发前准备    27 uniapp首页和我的页面实现    28 uniapp 节点钱包和节点钱包列表展示    29 uniapp添加节点,节点列表挖矿区块链查询等    30 uniapp我的钱包转账查询交易等    31 课程总结以及代码资料等相关说明

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值