場景解決方案-附近的人(GeoHash的應用)
前言
附近的人
,這四個字的需求就大有文章可做了。很二逼的做法是,存每個人的經度緯度,然後遍歷資料庫所有資料,foreach迴圈,兩點距離座標公式。量少的時候,這個沒啥問題。量大了,掃描全表 + 經緯度距離運算分分鐘拖垮資料庫。那麼是否有方案可以解決這個痛點呢,今年就來說下 Geohash
實現思路
想要不拖垮資料,要做到能走索引。就是跟你無關的點,不要掃描。減少掃描行數來實現減輕資料庫的壓力。那麼減少掃描行數肯定要想到索引。可是經緯度有兩個欄位,且查詢條件無論怎麼寫都沒辦法走索引。那麼唯一能想到的就是二維變一維。 geohash基本原理是將地球理解為一個二維平面,將平面遞迴分解成更小的子塊,每個子塊在一定經緯度範圍內擁有相同的編碼,這種方式簡單粗暴,可以滿足對小規模的資料進行經緯度的檢索。 兩個點的距離越近,他們的編碼字首部分就相同,字首部分相同越多,代表距離越近
。然後我們做資料庫掃描的時候 可以 WHERE geohash Like 'code%'
這樣就起到了走索引從而優化了執行效率。
程式碼思路(PHP)
class Geohash { private $coding = "0123456789bcdefghjkmnpqrstuvwxyz"; private $codingMap = array(); public function Geohash() { for($i = 0; $i < 32; $i++) { $this->codingMap[substr($this->coding, $i, 1)] = str_pad(decbin($i), 5, "0", STR_PAD_LEFT); } } public function decode($hash) { $binary = ""; $hl = strlen($hash); for($i = 0; $i < $hl; $i++) { $binary .= $this->codingMap[substr($hash, $i, 1)]; } $bl = strlen($binary); $blat = ""; $blong = ""; for ($i = 0; $i < $bl; $i++) { if ($i%2) { $blat = $blat.substr($binary, $i, 1); } else { $blong = $blong.substr($binary, $i, 1); } } $lat = $this->binDecode($blat, -90, 90); $long = $this->binDecode($blong, -180, 180); $latErr = $this->calcError(strlen($blat), -90, 90); $longErr = $this->calcError(strlen($blong), -180, 180); $latPlaces = max(1, -round(log10($latErr))) - 1; $longPlaces = max(1, -round(log10($longErr))) - 1; $lat = round($lat, $latPlaces); $long = round($long, $longPlaces); return array($lat,$long); } public function encode($lat,$long) { $plat = $this->precision($lat); $latbits = 1; $err = 45; while($err > $plat) { $latbits++; $err/ = 2; } $plong = $this->precision($long); $longbits = 1; $err = 90; while($err > $plong) { $longbits++; $err /= 2; } $bits = max($latbits,$longbits); $longbits = $bits; $latbits = $bits; $addlong = 1; while (($longbits+$latbits) % 5 != 0) { $longbits += $addlong; $latbits += !$addlong; $addlong = !$addlong; } $blat = $this->binEncode($lat, -90, 90, $latbits); $blong = $this->binEncode($long, -180, 180, $longbits); $binary = ""; $uselong = 1; while (strlen($blat)+strlen($blong)) { if ($uselong) { $binary = $binary.substr($blong, 0, 1); $blong = substr($blong, 1); } else { $binary = $binary.substr($blat, 0, 1); $blat = substr($blat, 1); } $uselong = !$uselong; } $hash = ""; for ($i = 0; $i < strlen($binary); $i += 5) { $n = bindec(substr($binary, $i, 5)); $hash = $hash . $this->coding[$n]; } return $hash; } private function calcError($bits, $min, $max) { $err = ($max - $min) / 2; while ($bits--) { $err /= 2; } return $err; } private function precision($number) { $precision = 0; $pt = strpos($number,'.'); if ($pt! == false) { $precision = -(strlen($number) - $pt - 1); } return pow(10, $precision) / 2; } private function binEncode($number, $min, $max, $bitcount) { if ($bitcount == 0) { return ""; } $mid = ($min + $max) / 2; if ($number > $mid) { return "1" . $this->binEncode($number, $mid, $max, $bitcount - 1); } else { return "0" . $this->binEncode($number, $min, $mid, $bitcount - 1); } } private function binDecode($binary, $min, $max) { $mid = ($min + $max) / 2; if (strlen($binary) == 0) { return $mid; } $bit = substr($binary, 0, 1); $binary = substr($binary, 1); if ($bit == 1) { return $this->binDecode($binary, $mid, $max); } else { return $this->binDecode($binary, $min, $mid); } } }
精度值
如圖,當前綴碼相同為7相差76米左右,為8相差19米,為9的話可以近似理解為那個人就在你身邊了。