LBS(基于位置的做事)
查找附近的人有个更大的专有名词叫做LBS(基于位置的做事),LBS是指是指通过电信移动运营商的无线电通讯网络或外部定位办法,获取移动终端用户的位置信息,在GIS平台的支持下,为用户供应相应做事的一种增值业务。因此首先得获取用户的位置,获取用户的位置有基于GPS、基于运营商基站、WIFI等办法,一样平常由客户端获取用户位置的经纬度坐标上传至运用做事器,运用做事器对用户坐标进行保存,客户端获取附近的人数据的时候,运用做事器基于要求人的地理位置合营一定的条件(间隔,性别,生动韶光等)去数据库进行筛选和排序。
10年架构师领你架构-发展之路-(附口试题(含答案))根据经纬度如何得出两点之间的间隔?我们都知道平面坐标内的两点坐标可以利用平面坐标间隔公式来打算,但经纬度是利用三度空间的球面来定义地球上的空间的球面坐标系统,假定地球是正球体,关于球面间隔打算公式如下:
d(x1,y1,x2,y2)=rarccos(sin(x1)sin(x2)+cos(x1)cos(x2)cos(y1-y2))
详细推断过程有兴趣的推举这篇文章:根据经纬度打算地面两点间的间隔-数学公式及推导
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 } })
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会更方便,以上只是建议,详细实现方案还要视详细业务去进行方案评审。
以上内容希望帮助到大家,很多PHPer在进阶的时候总会碰着一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、做事器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微做事、Nginx等多个知识点高等进阶干货须要的可以免费分享给大家,须要的可以点击查看详细资料内容信息