https://zhuanlan.zhihu.com/p/31380780

LBS(基于位置的服务)

查找附近的人有个更大的专有名词叫做LBS(基于位置的服务),LBS是指是指通过电信移动运营商的无线电通讯网络或外部定位方式,获取移动终端用户的位置信息,在GIS平台的支持下,为用户提供相应服务的一种增值业务。因此首先得获取用户的位置,获取用户的位置有基于GPS、基于运营商基站、WIFI等方式,一般由客户端获取用户位置的经纬度坐标上传至应用服务器,应用服务器对用户坐标进行保存,客户端获取附近的人数据的时候,应用服务器基于请求人的地理位置配合一定的条件(距离,性别,活跃时间等)去数据库进行筛选和排序。

根据经纬度如何得出两点之间的距离?

我们都知道平面坐标内的两点坐标可以使用平面坐标距离公式来计算,但经纬度是利用三度空间的球面来定义地球上的空间的球面坐标系统,假定地球是正球体,关于球面距离计算公式如下:

具体推断过程有兴趣的推荐这篇文章:根据经纬度计算地面两点间的距离-数学公式及推导

PHP函数代码如下:

/**
* 根据两点间的经纬度计算距离
* @param $lat1
* @param $lng1
* @param $lat2
* @param $lng2
* @return float
*/
public static function getDistance($lat1, $lng1, $lat2, $lng2){
$earthRadius = 6367000; //approximate radius of earth in meters
$lat1 = ($lat1 * pi() ) / 180;
$lng1 = ($lng1 * pi() ) / 180;
$lat2 = ($lat2 * pi() ) / 180;
$lng2 = ($lng2 * pi() ) / 180;
$calcLongitude = $lng2 - $lng1;
$calcLatitude = $lat2 - $lat1;
$stepOne = pow(sin($calcLatitude / 2), 2) + cos($lat1) * cos($lat2) * pow(sin($calcLongitude / 2), 2);
$stepTwo = 2 * asin(min(1, sqrt($stepOne)));
$calculatedDistance = $earthRadius * $stepTwo;
return round($calculatedDistance);
}

MySQL代码如下:

SELECT
id, (
3959 * acos (
cos ( radians(78.3232) )
* cos( radians( lat ) )
* cos( radians( lng ) - radians(65.3234) )
+ sin ( radians(78.3232) )
* sin( radians( lat ) )
)
) AS distance
FROM markers
HAVING distance < 30
ORDER BY distance
LIMIT 0 , 20;

除了上面通过计算球面距离公式来获取,我们可以使用某些数据库服务得到,比如Redis和MongoDB:

Redis 3.2提供GEO地理位置功能,不仅可以获取两个位置之间的距离,获取指定位置范围内的地理信息位置集合也很简单。Redis命令文档

1.增加地理位置

GEOADD key longitude latitude member [longitude latitude member ...]

2.获取地理位置

GEOPOS key member [member ...]

3.获取两个地理位置的距离

GEODIST key member1 member2 [unit]

4.获取指定经纬度的地理信息位置集合

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

5.获取指定成员的地理信息位置集合

GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

MongoDB专门针对这种查询建立了地理空间索引。 2d和2dsphere索引,分别是针对平面和球面。 MongoDB文档

1.添加数据

db.location.insert( {uin : 1 , loc : { lon : 50 , lat : 50 } } )

2.建立索引

db.location.ensureIndex( { loc : "2d" } )

3.查找附近的点

db.location.find( { loc :{ $near : [50, 50] } )

4.最大距离和限制条数

db.location.find( { loc : { $near : [50, 50] , $maxDistance : 5 } } ).limit(20)

5.使用geoNear在查询结果中返回每个点距离查询点的距离

db.runCommand( { geoNear : "location" , near : [ 50 , 50 ], num : 10, query : { type : "museum" } } )

6.使用geoNear附带查询条件和返回条数,geoNear使用runCommand命令不支持find查询中分页相关limit和skip参数的功能

db.runCommand( { geoNear : "location" , near : [ 50 , 50 ], num : 10, query : { uin : 1 } })

PHP多种方式和具体实现

1.基于MySql

成员添加方法:

public function geoAdd($uin, $lon, $lat)
{
$pdo = $this->getPdo();
$sql = 'INSERT INTO `markers`(`uin`, `lon`, `lat`) VALUES (?, ?, ?)';
$stmt = $pdo->prepare($sql);
return $stmt->execute(array($uin, $lon, $lat));
}

查询附近的人(支持查询条件和分页):

public function geoNearFind($lon, $lat, $maxDistance = 0, $where = array(), $page = 0)
{
$pdo = $this->getPdo();
$sql = "SELECT
id, (
3959 * acos (
cos ( radians(:lat) )
* cos( radians( lat ) )
* cos( radians( lon ) - radians(:lon) )
+ sin ( radians(:lat) )
* sin( radians( lat ) )
)
) AS distance
FROM markers"; $input[':lat'] = $lat;
$input[':lon'] = $lon; if ($where) {
$sqlWhere = ' WHERE ';
foreach ($where as $key => $value) {
$sqlWhere .= "`{$key}` = :{$key} ,";
$input[":{$key}"] = $value;
}
$sql .= rtrim($sqlWhere, ',');
} if ($maxDistance) {
$sqlHaving = " HAVING distance < :maxDistance";
$sql .= $sqlHaving;
$input[':maxDistance'] = $maxDistance;
} $sql .= ' ORDER BY distance'; if ($page) {
$page > 1 ? $offset = ($page - 1) * $this->pageCount : $offset = 0;
$sqlLimit = " LIMIT {$offset} , {$this->pageCount}";
$sql .= $sqlLimit;
} $stmt = $pdo->prepare($sql);
$stmt->execute($input);
$list = $stmt->fetchAll(PDO::FETCH_ASSOC); return $list;
}

2.基于Redis(3.2以上)

PHP使用Redis可以安装redis扩展或者通过composer安装predis类库,本文使用redis扩展来实现。

成员添加方法:

public function geoAdd($uin, $lon, $lat)
{
$redis = $this->getRedis();
$redis->geoAdd('markers', $lon, $lat, $uin);
return true;
}

查询附近的人(不支持查询条件和分页):

public function geoNearFind($uin, $maxDistance = 0, $unit = 'km')
{
$redis = $this->getRedis();
$options = ['WITHDIST']; //显示距离
$list = $redis->geoRadiusByMember('markers', $uin, $maxDistance, $unit, $options);
return $list;
}

3.基于MongoDB

PHP使用MongoDB的扩展有mongo(文档)和mongodb(文档),两者写法差别很大,选择好扩展需要对应相应的文档查看,由于mongodb扩展是新版,本文选择mongodb扩展。

假设我们创建db库和location集合

设置索引:

db.getCollection('location').ensureIndex({"uin":1},{"unique":true})
db.getCollection('location').ensureIndex({loc:"2d"})
#若查询位置附带查询,可以将常查询条件添加至组合索引
#db.getCollection('location').ensureIndex({loc:"2d",uin:1})

成员添加方法:

public function geoAdd($uin, $lon, $lat)
{
$document = array(
'uin' => $uin,
'loc' => array(
'lon' => $lon,
'lat' => $lat,
),
); $bulk = new MongoDB\Driver\BulkWrite;
$bulk->update(
['uin' => $uin],
$document,
[ 'upsert' => true]
);
//出现noreply 可以改成确认式写入
$manager = $this->getMongoManager();
$writeConcern = new MongoDB\Driver\WriteConcern(1, 100);
//$writeConcern = new MongoDB\Driver\WriteConcern(MongoDB\Driver\WriteConcern::MAJORITY, 100);
$result = $manager->executeBulkWrite('db.location', $bulk, $writeConcern); if ($result->getWriteErrors()) {
return false;
}
return true;
}

查询附近的人(返回结果没有距离,支持查询条件,支持分页)

public function geoNearFind($lon, $lat, $maxDistance = 0, $where = array(), $page = 0)
{
$filter = array(
'loc' => array(
'$near' => array($lon, $lat),
),
);
if ($maxDistance) {
$filter['loc']['$maxDistance'] = $maxDistance;
}
if ($where) {
$filter = array_merge($filter, $where);
}
$options = array();
if ($page) {
$page > 1 ? $skip = ($page - 1) * $this->pageCount : $skip = 0;
$options = [
'limit' => $this->pageCount,
'skip' => $skip
];
} $query = new MongoDB\Driver\Query($filter, $options);
$manager = $this->getMongoManager();
$cursor = $manager->executeQuery('db.location', $query);
$list = $cursor->toArray();
return $list;
}

查询附近的人(返回结果带距离,支持查询条件,支付返回数量,不支持分页):

public function geoNearFindReturnDistance($lon, $lat, $maxDistance = 0, $where = array(), $num = 0)
{
$params = array(
'geoNear' => "location",
'near' => array($lon, $lat),
'spherical' => true, // spherical设为false(默认),dis的单位与坐标的单位保持一致,spherical设为true,dis的单位是弧度
'distanceMultiplier' => 6371, // 计算成公里,坐标单位distanceMultiplier: 111。 弧度单位 distanceMultiplier: 6371
); if ($maxDistance) {
$params['maxDistance'] = $maxDistance;
}
if ($num) {
$params['num'] = $num;
}
if ($where) {
$params['query'] = $where;
} $command = new MongoDB\Driver\Command($params);
$manager = $this->getMongoManager();
$cursor = $manager->executeCommand('db', $command);
$response = (array) $cursor->toArray()[0];
$list = $response['results'];
return $list;
}

注意事项:

1.选择好扩展,mongo和mongodb扩展写法差别很大

2.写数据时出现noreply请检查写入确认级别

3.使用find查询的数据需要自己计算距离,使用geoNear查询的不支持分页

4.使用geoNear查询的距离需要转化成km使用spherical和distanceMultiplier参数

上述demo可以戳这里:demo

总结

以上介绍了三种方式去实现查询附近的人的功能,各种方式都有各自的适用场景,比如数据行比较少,例如查询用户和几座城市之间的距离使用Mysql就足够了,如果需要实时快速响应并且普通查找范围内的距离,可以使用Redis,但如果数据量大并且多种属性筛选条件,使用mongo会更方便,以上只是建议,具体实现方案还要视具体业务去进行方案评审。

使用PHP实现查找附近的人的更多相关文章

  1. redis 查找附近的人

    儿童定位手表,有个交友功能,查找附近的人,用redis的geo来实现比较简单,其实是一个ZSET(有序集合) redis 版本要大于3.2 查看redis 版本    /usr/bin/redis-s ...

  2. Redis(6)——GeoHash查找附近的人

    像微信 "附近的人",美团 "附近的餐厅",支付宝共享单车 "附近的车" 是怎么设计实现的呢? 一.使用数据库实现查找附近的人 我们都知道, ...

  3. Redis实战篇(四)基于GEO实现查找附近的人功能

    如果现在要开发一个功能: 要为一款交友App实现查找附近的人,并按距离进行排序. 让你来开发这个功能,你会如何实现? MySQL 不合适 你可能想到,把用户用户的经纬度坐标使用MySQL等关系数据库( ...

  4. 终极二分查找--传说十个人写九个有bug

    之前写过一篇极为罗嗦的二分查找,非常得意地以为以后就可以避免踩坑了,但是今天才知道二分查找可以写的既简洁又鲁棒,唉!还是要多学习啊! 给一个按照从大到小的顺序排序好的数组a[]={1,2,3,4,7, ...

  5. IntelliJ IDEA - 查找代码提交人

    转载. https://blog.csdn.net/abcyyjjkk/article/details/88995503 如果Annocation不可用

  6. (转)javascript中的对象查找

    本文转自:http://otakustay.com/object-lookup-in-javascript/  ---很棒的一篇文章,作者的其他文章还暂时没读,但相信作者是一个谦虚 谨慎的好工程师 近 ...

  7. SpringBoot入门教程(五)Java基于MySQL实现附近的人

    “附近的人”这个功能估计都不陌生,与之类似的功能最开始是在各大地图应用上接触过,比如搜附近的电影院,附近的超市等等.然而真正让附近的人火遍大江南北的应该是微信"附近的人"这个功能, ...

  8. Redis 到底是怎么实现“附近的人”这个功能的?

    前言:针对“附近的人”这一位置服务领域的应用场景,常见的可使用PG.MySQL和MongoDB等多种DB的空间索引进行实现.而Redis另辟蹊径,结合其有序队列zset以及geohash编码,实现了空 ...

  9. IM里“附近的人”功能实现原理是什么?如何高效率地实现它?

    1.引言 基本上以陌生人社交为主的IM产品里,都会增加“附近的人”.“附近的xxx”这种以LBS(地理位置)为导向的产品特色(微信这个熟人社交产品里为啥也有“附近的人”?这当然是历史原因了,微信当初还 ...

随机推荐

  1. Python基础Day6

    一.代码块 一个模块(模块就是py文件),一个函数,一个类,一个文件都是一个代码块,一个整体是一个代码块. 交互模式的每一行都是一个代码块(交互模式:命令提示符),相当于每行都在不同的文件 二.id ...

  2. Bash基础——内置命令

    前言 Shell有很多内置在其源代码中的命令.由于命令是内置的,所以Shell不必到磁盘上搜索它们.内置命令执行速度更快,不同的Shell内置命令有所不同. 如何查找内置命令 之前查了好久怎么收索内置 ...

  3. MySQL数据库语法-单表查询练习

    MySQL数据库语法-单表查询练习 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 本篇博客主要是对聚合函数和分组的练习. 一.数据表和测试数据准备 /* @author :yinz ...

  4. Go语言的反射

    反射是语言里面是非常重要的一个特性,我们经常会看见这个词,但是对于反射没有一个很好的理解,主要是因为对于反射的使用场景不太熟悉. 一.理解变量的内在机制 1.类型信息,元信息,是预先定义好的,静态的. ...

  5. Nginx练习练习玩玩

    Date:2019-11-9 读前思考: 对于NGINX,一般会问什么? 面试官会从哪方面入手? 面试官到底想考察什么? 你做好准备了吗? 如果对于初学者,往往可以通过面试题来提升对某一个技术的了解和 ...

  6. aiops相关

    AIOPS的能力框架 AIOps平台能力体系 AIOps 常见应用场景 按照时间来分 AIOPS实施的关键技术 1.数据采集(硬件,业务指标等) 2.数据预处理(特征工程) 3.数据可视化 4.数据存 ...

  7. k8s的pod的资源调度

    1.常用的预选策略 2.优选函数 3.节点亲和调度 3.1.节点硬亲和性 3.2.节点软亲和性 4.Pod资源亲和调度 4.1.Pod硬亲和度 4.2.Pod软亲和度 4.3.Pod反亲和度 5.污点 ...

  8. python中实现一个简单的进度条

    import time def progress(n,a): m = int(n / a * 100) x = int(n / a * 10) time.sleep(0.2) #只是为了让进度条明显 ...

  9. Vue实现一个图片懒加载插件(转载)

    Vue是可以自定义指令的,最近学习过程中遇见了一个需要图片懒加载的功能,最后参考了别人的代码和思路自己重新写了一遍.以下将详细介绍如何实现自定义指令v-lazyload. 先看如何使用这个指令: &l ...

  10. go语言的内建变量类型

    string bool int int8  int16 int32 int64 uintptr   无符号int 类型  (u)int (u)int8 (u)int16 (u)int32 (u)int ...