交友系统设计:哪种地理空间邻近算法更快?

小熊学Java:https://javaxiaobear.cn

交友与婚恋是人们最基本的需求之一。随着互联网时代的不断发展,移动社交软件已经成为了人们生活中必不可少的一部分。然而,熟人社交并不能完全满足年轻人的社交与情感需求,于是陌生人交友平台悄然兴起。

我们决定开发一款基于地理位置服务(LBS)的应用,为用户匹配邻近的、互相感兴趣的好友,应用名称为“Liao”。

Liao 面临的技术挑战包括:面对海量的用户,如何为其快速找到邻近的人,可以选择的地理空间邻近算法有哪些?Liao 如何在这些算法中选择出最合适的那个?

1、需求分析

Liao 的客户端是一个移动 App,用户打开 App 后,上传、编辑自己的基本信息,然后系统(推荐算法)根据其地理位置和个人信息,为其推荐位置邻近的用户。用户在手机上查看对方的照片和资料,如果感兴趣,希望进一步联系,就向右滑动照片;如果不感兴趣,就向左滑动照片。

如果两个人都向右滑动了对方,就表示他们互相感兴趣。系统就通知他们配对成功,并为他们开启聊天功能,可以更进一步了解对方,决定是否建立更深入的关系。Liao 的功能用例图如下:

image-20231218221315843

用户规模分析

Liao 的目标用户是全球范围内的中青年单身男女,预估目标用户超过 10 亿,系统按 10 亿用户进行设计。

2、概要设计

Liao 的系统架构采用典型的微服务架构设计方案,用户通过网关服务器访问具体的微服务,如下图:

image-20231219231710903

可知,首先,用户所有请求都通过统一的网关服务器处理。网关服务器负责限流、防攻击、用户身份识别及权限验证、微服务调用及数据聚合封装等,而真正的业务逻辑则通过访问微服务来完成。Liao 的关键微服务有:用户微服务、图片微服务、配对微服务、聊天微服务、推荐微服务、邻近算法微服务等。Liao 的网关预计将承担每天百亿次规模的访问压力。

用户微服务管理用户的个人信息、兴趣爱好以及交友偏好等,此外也负责用户登录服务,只有登录用户才能访问系统。因为需要存储十亿条用户数据,所以用户数据库采用分片的MySQL 数据库。

图片微服务用于管理用户照片,提供用户照片存储及展示的功能。Liao 需要存储的图片数大约几百亿张。我们使用 Nginx 作为图片服务器,图片服务器可以线性扩容,每写满一台服务器(及其 Slave 服务器),就继续写入下一台服务器。服务器 IP、图片路径则记录在用户数据库中。同时,购买 CDN 服务,缓存热门的用户照片。

配对微服务负责将互相喜欢的用户配对,通知用户,并加入彼此的通讯录中。用户每次右划操作都调用该微服务。系统设置一个用户每天可以喜欢(右划)的人是有上限的,但是,对于活跃用户而言,长期积累下来,喜欢的人的数量还是非常大的,因此配对微服务会将数据发送给一个流式大数据引擎进行计算。

推荐微服务负责向用户展示其可能感兴趣的、邻近的用户。因此,一方面,推荐微服务需要根据用户操作、个人兴趣、交友偏好调用协同过滤等推荐算法进行推荐,另一方面必须保证推荐的用户在当前用户的附近。

3、详细设计

详细设计主要关注邻近位置算法,也就是,如何根据用户的地理位置寻找距其一定范围内的其他用户。

我们可以通过 Liao App 获取用户当前经、纬度坐标,然后根据经、纬度,计算两个用户之间的距离,距离计算公式采用半正矢公式:

image-20231219231858953

其中 r 代表地球半径, 表示纬度, 表示经度。

但是,当我们有 10 亿用户的时候,如果每次进行当前用户匹配都要和其他所有用户做一次距离计算,然后再进行排序,那么需要的计算量至少也是千亿级别,这样的计算量是我们不能承受的。通常的空间邻近算法有以下 4 种,我们一一进行分析,最终选择出最合适的方案。

1、SQL 邻近算法

我们可以将用户经、纬度直接记录到数据库中,纬度记录在 latitude 字段,经度记录在longitude 字段,用户当前的纬度和经度为 X,Y,如果我们想要查找和当前用户经、纬度距离 D 之内的其他用户,可以通过如下 SQL 实现。

select * from users where latitude between X-D and X+D and longtitude between

这样的 SQL 实现起来比较简单,但是如果有十亿用户,数据分片在几百台服务器上,SQL执行效率就会很低。而且我们用经、纬度距离进行近似计算,在高纬度地区,这种近似计算的偏差还是非常大的。

同时“between X-D and X+D”以及“between Y-D and Y+D”也会产生大量中间计算数据,这两个 betwen 会先返回经度和纬度各自区间内的所有用户,再进行交集 and 处理,如下图。

image-20231219232028935

我们的用户量非常大,而计算邻近好友又是一个非常高频的访问,同时,分片数据库进行集合计算需要在中间代理服务器或应用程序服务器完成计算,因此,这样的交集计算带来计算负载压力是我们的系统完全不能承受的。所以这个方案可以被放弃。

2、地理网格邻近算法

为了减少上述交集计算使用的中间数据量,我们将整个地球用网格进行划分,如下图:

image-20231219232111614

事实上,我们划分的网格远比图中示意的要密集得多,赤道附近,经、纬度方向每 10 公里一个网格。

这样每个用户必然会落入到一个网格中,我们在用户表中记录用户所在的网格ID(gridID),然后借助这个字段进行辅助查找,将查找范围限制在用户所在的网格(gridIDx0)及其周围 8 个网格(gridIDx1 ~ gridIDx8)中,可以极大降低中间数据量,SQL 如下:

select * from users where latitude between X-D and X+D and longtitude between

这条 SQL 要比上面 SQL 的计算负载压力小得多,但是对于高频访问的分片数据库而言,用这样的 SQL 进行邻近好友查询依然是不能承受的,同样距离精度也不满足要求。

但是基于这种网格设计思想,我们发现,我们可以不通过数据库就能实现邻近好友查询:我们可以将所有的网格及其包含的用户都记录在内存中。当我们进行邻近查询时,只需要在内存中计算用户及其邻近的 8 个网格内的所有用户的距离即可。

我们可以估算下所有用户经、纬度都加载到内存中需要的内存量:1G × 3 × 4B = 12GB(用户 ID、经度、纬度,都采用 4 个字节编码,总用户数 1G)。这个内存量是完全可以接受的。

实际上,通过恰当地选择网格的大小,我们不停访问当前用户位置周边的网格就可以由近及远不断得到邻近的其他用户,而不需要再通过 SQL 来得到。那么如何选择网格大小?如何根据用户位置得到其所在的网格?又如何得到当前用户位置周边的其他网格呢?我们看
下实践中更常用的动态网格和 GeoHash 算法。

3、动态网格算法

事实上,不管如何选择网格大小,可能都不合适。因为在陆家嘴即使很小的网格可能就包含近百万的用户,而在可可西里,非常大的网格也包含不了几个用户。

因此,我们希望能够动态设定网格的大小,如果一个网格内用户太多,就把它分裂成几个小网格,小网格内如果用户还是太多,继续分裂更小的网格,如下图:

image-20231219232347384

这是一个四叉树网格结构,开始的时候整个地球只有一个网格,当用户增加,超过阈值(500 个用户)的时候,就分裂出 4 个子树,4 个子树对应父节点网格的 4 个地理子网格。同时,将用户根据位置信息重新分配到 4 个子树中。同样,如图中所示,如果某个子树中的用户增加,超过了阈值,该子树继续分裂成 4 个子树。

因此,我们可以将全球用户分配在这样一个 4 叉树网格结构中,所有的用户都必然在这个4 叉树的叶子节点中,而且每个节点内包含的用户数不超过 500 个。那么,陆家嘴的网格可能就会很小,而可可西里的网格就会很大,太平洋对应的网格可能有几千公里。

当给定当前用户的经、纬度,查询邻近用户的时候,首先从根节点开始查找,如果根节点就是叶子节点,那么直接遍历根节点中的所有用户,计算距离即可。如果根节点不是叶子节点,那么根据给定的经、纬度判断其在网格中的位置,左上、右上、右下、左下,4 个位置,顺序对应 4 个子树,根据网格位置访问对应的子树。如果子树是叶子节点,那么在叶子节点中查找,如果不是叶子节点,继续上面的过程,直到叶子节点。

上面的过程只能找到当前用户所在网格的好友,如何查找邻近网格的其他用户呢?事实上,我们只需要将 4 叉树所有的叶子节点顺序组成一个双向链表,每个节点在链表上的若干个前驱和后继节点正好就是其地理位置邻近的节点。

动态网格也叫 4 叉树网格,在空间邻近算法中较为常用,也能满足 Liao 的需求。但是编程实现稍稍有点麻烦,而且如果网格大小设计不合适,导致树的高度太高,每次查找需要遍历的路径太长,性能结果也比较差。我们再看下性能和灵活性更好的 GeoHash 算法。

4、GeoHash 算法

除了动态网格算法,GeoHash 事实上是另外一种变形了的网格算法,同时也是 Redis 中Geo 函数使用的算法。GeoHash 是将网格进行编码,然后根据编码进行 Hash 存储的一种算法。

经、纬度数字的不同精度,意味着经、纬度的误差范围,比如保留经、纬度到小数点后第1 位,那么误差范围最大可能会达到 11 公里(在赤道附近)。也就是说,小数点后 1 位精度的经、纬度,其覆盖范围是一个 11km * 11km 的网格。

那么,我们用小数点后 1 位精度的经、纬度做 key,网格内的用户集合做 value,就可以构建一个 Hash 表的 <key, value> 对。通过查找这个 KV 对及其周围 8 个网格的 KV 对,计算这些 value 内所有用户和当前用户的距离,就可以找到邻近 11 公里内的所有用户。

实践中,redis 的 GeoHash 并不会直接用经、纬度做 key,而是采用一种基于 Z 阶曲线的编码方式,将二维的经、纬度,转化为一维的二进制数字,再进行 base32 编码,具体过程如下:

  1. 首先,分别针对经度和纬度,求取当前区间(对于纬度而言,开始的区间就是[-90, 90], 对于经度而言,开始区间就是[-180, 180])的平均值,将当前区间分为两个区间。然后用用户的经、纬度和区间平均值进行比较,用户经、纬度必然落在两个区间中的一个,如果大于平均值,那么取 1,如果小于平均值,那么取 0。

  2. 继续求取当前区间的平均值,进一步将当前区间分为两个区间。如此不断重复,可以在经度和纬度方向上,得到两个二进制数。这个二进制数越长,其所在的区间越小,精度越高。

    下图表示经、纬度 <43.60411, -5.59041> 的二进制编码过程,最终得到纬度 12 位编码,经度 13 位编码。

    image-20231219232619732 image-20231219232637085

    得到两个二进制数后,再将它们合并成一个二进制数。合并规则是,从第一位开始,奇数位为经度,偶数位为纬度,上面例子合并后的结果为 01101 11111 11000 00100 00010,共 25 位二进制数。

  3. 将 25 位二进制数划分成 5 组,每组 5 个二进制数,对应的 10 进制数是 0-31,采用Base32 编码,可以得到一个 5 位字符串,Base32 编码表如下:

    image-20231219232717325

    编码计算过程如下:

    image-20231219232736133

  4. 最后得到一个字符串“ezs42”,作为 Hash 表的 key。25 位二进制的 GeoHash 编码,其误差范围大概 2.4 公里,即对应一个2.4km * 2.4km 的网格。网格内的用户都作为value 放入到 Hash 表中。

一般说来,通过选择 GeoHash 的编码长度,实现不同大小的网格,就可以满足我们邻近交友的应用场景了。但是在 Redis 中,需要面对更通用的地理位置计算场景,所以 Redis中的 GeoHash 并没有用 Hash 表存储,而是用跳表存储。

Redis 使用 52 位二进制的 GeoHash 编码,误差范围 0.6 米。Redis 将编码后的二进制数按照 Z 阶曲线的布局,进行一维化展开。即将二维的经、纬度上的点,用一条 Z 型曲线连接起来,Z 阶曲线布局示例如下图。

image-20231219232819378

事实上,所谓的 Z 阶曲线布局,本质其实就是基于 GeoHash 的二进制排序。将这些经过编码的 2 进制数据用跳表存储。查找用户的时候,可以快速找到该用户,沿着跳表前后检索,得到的就是邻近的用户。

5、Liao 的最终算法选择

Liao 的邻近算法最终选择使用 Hash 表存储的 GeoHash 算法,经度采用 13bit 编码,纬度采用 12bit 编码,即最后的 GeoHash 编码 5 个字符,每个网格 2.4km * 2.4km = 5km,将整个地球分为 个网格,去掉海洋和几乎无人生存的荒漠极地,需要存储的 Hash 键不到 500 万个,采用 Hash 表存储。Hash 表的 key 是 GeoHash 编码,value 是一个 List,其中包含了所有相同 GeoHash 编码的用户 ID。

查找邻近好友的时候,Liao 将先计算用户当前位置的 GeoHash 值(5 个字符),然后从Hash 表中读取该 Hash 值对应的所有用户,即在同一个网格内的用户,进行匹配,将满足匹配条件的对象返回给用户。如果一个网格内匹配的对象数量不足,计算周围 8 个网格的GeoHash 值,读取这些 Hash 值对应的用户列表,继续匹配。

4、总结

算法是软件编程中最有技术挑战性,也最能考验一个人编程能力的领域。所以很多企业面试的时候特别喜欢问算法类的问题,即使这些算法和将来的工作内容关系不大,面试官也可以凭借这些问题对候选人的专业能力和智力水平进行评判,而且越是大厂的面试越是如此。

架构和算法通常是一个复杂系统的一体两面,架构是关于整体系统是如何组织起来的,而算法则是关于核心功能如何处理的。我们专栏大多数案例也都体现了这种一体两面,很多案例设计都有一两个核心算法,比如短 URL 生成与预加载算法、缩略图生成与推荐算法、

本篇的空间邻近算法以及下一篇要讲的倒排索引与 PageRank 算法,都展现了这一点。一个合格的架构师除了要掌握系统的整体架构,也要能把握住这些关键的算法,才能在系统的设计和开发中做到心中有数、控制自如。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Go语言中的`sync`包同步原语

通过sync包掌握Go语言的并发 并发是现代软件开发的基本方面&#xff0c;而Go&#xff08;也称为Golang&#xff09;为并发编程提供了一套强大的工具。在Go中用于管理并发的基本包之一是sync包。在本文中&#xff0c;我们将概述sync包&#xff0c;并深入探讨其最关键的同步原语…

【adb】电脑通过ADB向手机设备传输文件

具体步骤如下&#xff1a; Step1 下载ADB工具 下载最新版本的 ADB工具 !!! 注意&#xff1a;一定要是最新版本的ADB&#xff0c;否则很可能导致无法识别到手机。 将下载的ADB解压以后的文件如下图所示&#xff1a; Step2 添加环境变量 将 ABD 的路径 D:\platformtools &am…

java进阶(二)-java小干货

java一些精干知识点分享 2. java小干货2.1循环遍历2.2可变参数2.3 list和数组转化2.3.1 数组转list2.3.2 list转数组 2.4 值传递和地址传递2.4.1值传递2.4.2 地址传递2.4.3易错点总结 2.5 数组数组帮助类Arrays 2.5 基本数据类型和包装类2.5集合2.6文件流2.7java代码块、内部类…

Nginx快速入门:实现企业安全防护|nginx部署https,ssl证书(七)

0. 引言 之前我们讲到nginx的一大核心作用就是实现企业安全防护&#xff0c;而实现安全防护的原理就是通过部署https证书&#xff0c;以此实现参数加密访问&#xff0c;从而加强企业网站的安全能力。 nginx作为各类服务的统一入口&#xff0c;只需要在入口处部署一个证书&…

第二十一章博客

计算机应用实现了多台计算机间的互联&#xff0c;使得它们彼此之间能够进行数据交流。网络应用程序就是在已连接的不同计算机上运行的程序&#xff0c;这些程序借助于网络协议&#xff0c;相互之间可以交换数据。编写网络应用程序前&#xff0c;首先必须明确所要使用的网络协议…

C++面试宝典第9题:找出第K大元素

题目 给定一个整数数组a,同时给定它的大小N和要找的K(1 <= K <= N),请根据快速排序的思路,找出数组中第K大的数(保证答案存在)。比如:数组a为[50, 23, 66, 18, 72],数组大小N为5,K为3,则第K大的数为50。 解析 这道题主要考察应聘者对于快速排序的理解,以及实…

开源项目解读 —— Self-Operating Computer Framework # 长期主义 # 价值

价值&#xff1a;生成主函数业务逻辑函数思维导图&#xff0c;帮助理解&#xff0c;PR到开源项目&#xff0c;希望帮助大家理解IPA工作原理&#xff0c;国内没有好的开源项目&#xff0c;我就来翻译分析解读&#xff0c;给大家抛砖引玉。思维导图用文心一言配合其思维导图插件实…

算法基础之数字三角形

数字三角形 核心思想&#xff1a;线性dp 集合的定义为 f[i][j] –> 到i j点的最大距离 从下往上传值 父节点f[i][j] max(f[i1][j] , f[i1][j1]) w[i][j] 初始化最后一层 f w #include <bits/stdc.h>using namespace std;const int N 510;int w[N][N],f[N][…

Dash中 基本的 callback 5

app.callback 在Dash中&#xff0c;app.callback 被用于创建交互性应用程序&#xff0c;它用于定义一个回调函数&#xff0c;该函数在应用程序中发生特定事件时被触发。回调函数可以修改应用程序的布局或更新图表等内容&#xff0c;从而实现动态交互。 下面是一个简单的 app.…

多维时序 | Matlab实现PSO-GCNN粒子群优化分组卷积神经网络多变量时间序列预测

多维时序 | Matlab实现PSO-GCNN粒子群优化分组卷积神经网络多变量时间序列预测 目录 多维时序 | Matlab实现PSO-GCNN粒子群优化分组卷积神经网络多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 Matlab实现PSO-GCNN粒子群优化分组卷积神经网络多…

Plantuml之序列图语法介绍(十七)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

【Rust与AI】LLM模型基本架构

本篇是《Rust与AI》系列的第二篇&#xff0c;上一篇我们主要介绍了本系列的概览和方向&#xff0c;定下了一个基调。本篇我们将介绍LLM的基本架构&#xff0c;我们会以迄今为止使用最广泛的开源模型LLaMA为例展开介绍。 LLM背景 Rust 本身是不挑 AI 模型的&#xff0c;但是 LLM…

Java进阶(第六期): Arrays类(数组工具)、冒泡排序、选择排序、二分查找、【正则表达式】、Java正则爬取信息

文章目录 一、Arrays1.1代码示例&#xff1a; 二、冒泡排序2.1 代码示例 三、选择排序3.1 代码示例 四、二分查找4.1 代码示例 &#xff08;这里采用乱序数组&#xff09; 五、正则表达式5.1 正则表达式的基本使用5.2 正则表达式爬取信息练习 Java进阶&#xff08;第六期&#…

vue3(七)-基础入门之事件总线与动态组件

一、事件总线 事件总线使用场景&#xff1a; 两个兄弟组件之间的传参&#xff0c;或者两个没有关联的组件之间的传参 html &#xff1a;引入 publicmsg 与 acceptmsg 自定义组件 (自定义组件名称必须小写) <body><div id"app"><publicmsg></…

CGAL的三维点集

CGAL提供了几种处理点集的算法&#xff0c;从形状检测到通过标准点集处理工具进行的表面重建。 虽然这些算法不强制使用特定的数据结构&#xff0c;但该软件包提供了一个3D点集结构&#xff0c;使用户更容易处理附加属性&#xff0c;如法向量、颜色、标签&#xff0c;并在其上调…

LabVIEW在横向辅助驾驶系统开发中的应用

LabVIEW在横向辅助驾驶系统开发中的应用 随着横向辅助驾驶技术的快速发展&#xff0c;越来越多的研究致力于提高该系统的效率和安全性。项目针对先进驾驶辅助系统&#xff08;ADAS&#xff09;中的横向辅助驾驶进行深入研究。在这项研究中&#xff0c;LabVIEW作为一个强大的系…

数禾使用 Knative 加速 AI 模型服务部署丨KubeCon China 2023

作者&#xff1a;李鹏&#xff08;阿里云&#xff09;、魏文哲&#xff08;数禾科技&#xff09;&#xff0c; 此文基于 KubeCon China 2023 分享整理 摘要 AI 服务的数据、训练、推理等都需要消耗大量的计算资源以及运维成本&#xff0c;在数禾科技的金融业务场景下&#xf…

uni-app 工程目录结构介绍

锋哥原创的uni-app视频教程&#xff1a; 2023版uniapp从入门到上天视频教程(Java后端无废话版)&#xff0c;火爆更新中..._哔哩哔哩_bilibili2023版uniapp从入门到上天视频教程(Java后端无废话版)&#xff0c;火爆更新中...共计23条视频&#xff0c;包括&#xff1a;第1讲 uni…

遇到DDOS怎么办,盾真的可以抗攻击吗

网络在以难以想象的速度发展&#xff0c;黑客们针对网络漏洞发起的攻击也从未停止&#xff0c;但复杂的网络环境让网络安全的维护更为艰难&#xff0c;如果游戏公司没有做好防御措施&#xff0c;黑客发起攻击只是时间问题。在网络攻击愈加多元化的今天&#xff0c;游戏行业可以…

基于SSM在线协同过滤汽车推荐销售系统

SSM毕设分享 基于SSM在线协同过滤汽车推荐销售系统 1 项目简介 Hi&#xff0c;各位同学好&#xff0c;这里是郑师兄&#xff01; 今天向大家分享一个毕业设计项目作品【】 师兄根据实现的难度和等级对项目进行评分(最低0分&#xff0c;满分5分) 难度系数&#xff1a;3分 工作…