introduction
Yesterday, a public account fan and I discussed an interview question. I personally found it more meaningful. I have organized and shared it with everyone here. I hope that my friends will not step on the road during the interview. The interview question is relatively simple: "Let you realize the function of a nearby person, what plan do you have?" This question is actually mainly to examine the breadth of technology for everyone. This article introduces several plans to give you a few ideas to avoid the interview process. The Chinese language is congested and affects the result of the interview. If there is any rigour, I hope the relatives will gently correct me!
“附近的人”
It is more commonly used in functional life, like restaurants near the take-out app, sharing vehicles near the bike app. Since the probability of being asked in common interviews is very high, we will analyze the functions of "nearby people" based on mysql数据库
, Redis
and in order, in order MongoDB
.
Popular science : To mark a location in the world, the common practice is to use latitude and longitude. The range of longitude is (-180, 180), and the range of latitude is (-90, 90). The east is positive and the west is negative. For example, the longitude and latitude (116.49141, 40.01229) of Wangjing Motorola Building are all positive numbers, because China is located in the northeast hemisphere.
1. The principle of "people nearby"
“附近的人”
Also known as LBS
(Location Based Services, based on location services), it is based on the user's current geographic location data and services to provide users with accurate value-added services.
The core idea of "people nearby" is as follows:
-
Focus on "me" to search for nearby users
-
Calculate the distance between others and "I" based on the current location of "I"
-
Sort by the distance between "Me" and others, and filter out the users or stores closest to me
2. What is the GeoHash algorithm?
Saying “附近的人”
before the concrete realization of functions, first to understand GeoHash
the algorithm, because behind will always deal with it. The best way to locate a position is to use a 经、纬度
logo, but 经、纬度
it is two-dimensional, and it is still very troublesome to calculate the position. If you can convert the two-dimensional 经、纬度
data into one-dimensional data by some method , then it is necessary It's much easier, so the GeoHash
algorithm came into being.
GeoHash
The algorithm converts the two-dimensional latitude and longitude into a character string. For example, the following 9 GeoHash
character strings represent 9 areas, and each character string represents a rectangular area. The other points (latitude and longitude) in this rectangular area are all represented by the same GeoHash
string.
For example : WX4ER
users in the area search for nearby restaurant data. Because the user GeoHash
strings in this area are all WX4ER
, it can be regarded WX4ER
as key
, restaurant information as value
cache; and if the GeoHash
algorithm is not used , users in the area request restaurant data, the user The transmitted latitude and longitude are different, so the cache is not only troublesome but also has a huge amount of data.
GeoHash
The longer the string, the more precise the position. The longer the string, the smaller the error in distance. The following chart geohash
code accuracy table:
geohash code length | width | height |
---|---|---|
1 | 5,009.4km | 4,992.6km |
2 | 1,252.3km | 624.1km |
3 | 156.5km | 156km |
4 | 39.1km | 19.5km |
5 | 4.9km | 4.9km |
6 | 1.2km | 609.4m |
7 | 152.9m | 152.4m |
8 | 38.2m | 19m |
9 | 4.8m | 4.8m |
10 | 1.2m | 59.5cm |
11 | 14.9cm | 14.9cm |
12 | 3.7cm | 1.9cm |
Moreover, the more similar the character strings are, the closer the distance is, and the closer the character string prefix matches, the closer the distance. For example, the longitude and latitude below represent three restaurants close to each other.
Merchant | Latitude and longitude | Geohash string |
---|---|---|
Skewer | 116.402843,39.999375 | wx4er9v |
Hot Pot | 116.3967,39.99932 | wx4ertk |
Barbecue | 116.40382,39.918118 | wx4erfe |
Let everyone simply understand what an GeoHash
algorithm is to facilitate the development of the GeoHash
content behind. The content of the algorithm is relatively deep. Interested friends can dig deeper by themselves. It does not take up too much space (in fact, I understand too shallow, crying ~).
3. Based on Mysql
This method is purely mysql
implementation-based and does not use GeoHash
algorithms.
1. Design ideas
Taking the user as the center, suppose a circle is given a distance of 500 meters as a radius, and all users in this circular area are "close people" that meet the user's requirements. But there is a problem that the circle has radians. It is too difficult to directly search the circular area. It is impossible to search directly by latitude and longitude.
However, if a square is placed on a round jacket, by obtaining the maximum and minimum values of the user's longitude and latitude (longitude, latitude + distance), and then using the maximum and minimum values as the filtering conditions, it is easy to search for user information in the square.
So the question is coming again, what should I do if there is more area swollen?
Let's analyze, the extra users in this part of the area, the distance to the dot must be greater than the radius of the circle, then we calculate the distance between the user's center point and all users in the square, and filter out all distances less than or equal to the radius Users, all users in the circular area meet the requirements “附近的人”
.
2. Analysis of pros and cons
Based on pure mysql
realization “附近的人”
, it is the obvious advantage of simple, as long as the user is able to save a table built longitude and latitude information can be. The shortcomings are also obvious, requiring a large amount of calculation of the distance between two points, which greatly affects performance.
3. Realization
Create a simple table to store the user's latitude and longitude attributes.
CREATE TABLE `nearby_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL COMMENT '名称',
`longitude` double DEFAULT NULL COMMENT '经度',
`latitude` double DEFAULT NULL COMMENT '纬度',
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
To calculate the distance between two points, a three-party library is used. After all, the wheels made by yourself are not particularly round, and may be square, ah ha ha ha ~
<dependency>
<groupId>com.spatial4j</groupId>
<artifactId>spatial4j</artifactId>
<version>0.5</version>
</dependency>
After the circumscribed square is obtained, the users in the square area are searched by the maximum, minimum, and longitude values of the square, and then the users who exceed the specified distance are eliminated, which is the final 附近的人
.
private SpatialContext spatialContext = SpatialContext.GEO;
/**
* 获取附近 x 米的人
*
* @param distance 搜索距离范围 单位km
* @param userLng 当前用户的经度
* @param userLat 当前用户的纬度
*/
@GetMapping("/nearby")
public String nearBySearch(@RequestParam("distance") double distance,
@RequestParam("userLng") double userLng,
@RequestParam("userLat") double userLat) {
//1.获取外接正方形
Rectangle rectangle = getRectangle(distance, userLng, userLat);
//2.获取位置在正方形内的所有用户
List<User> users = userMapper.selectUser(rectangle.getMinX(), rectangle.getMaxX(), rectangle.getMinY(), rectangle.getMaxY());
//3.剔除半径超过指定距离的多余用户
users = users.stream()
.filter(a -> getDistance(a.getLongitude(), a.getLatitude(), userLng, userLat) <= distance)
.collect(Collectors.toList());
return JSON.toJSONString(users);
}
private Rectangle getRectangle(double distance, double userLng, double userLat) {
return spatialContext.getDistCalc()
.calcBoxByDistFromPt(spatialContext.makePoint(userLng, userLat),
distance * DistanceUtils.KM_TO_DEG, spatialContext, null);
}
Since the sorting of the distance between users is implemented in business code, you can see that the SQL statement is also very simple.
<select id="selectUser" resultMap="BaseResultMap">
SELECT * FROM user
WHERE 1=1
and (longitude BETWEEN ${minlng} AND ${maxlng})
and (latitude BETWEEN ${minlat} AND ${maxlat})
</select>
Four, Mysql + GeoHash
1. Design ideas
The design idea of this method is simpler. When storing user location information, the corresponding geohash
character string is calculated according to the user's latitude and longitude attributes . Note : When calculating a geohash
string, you need to specify geohash
the precision of the string, that is geohash
, the length of the string, refer to the geohash
precision table above .
When you need to obtain 附近的人
, just use the current user geohash
string, the database WHERE geohash Like 'geocode%
queries geohash
users with similar strings through ' , and then calculates the distance between the current user and the searched user, and filters out all distances less than or equal to the specified distance (near 500 meters), that is 附近的人
.
2. Analysis of pros and cons
There is a problem with the GeoHash
algorithm implementation “附近的人”
, because the geohash
algorithm divides the map into rectangles and encodes each rectangle to get a geohash
string. But my current point is very close to the neighboring point, but it happens that we are in two areas, and the point in front of me is obviously not found, and it is really dark under the light.
How to solve this problem?
In order to avoid similar adjacent two points in different areas, we need to get the current point ( WX4G0
) near the area where the 8个
area geohash
code, together with screening compared.
3. Realization
A table should also be designed to store the user's latitude and longitude information, but the difference is that there is one more geo_code
field to store the geohash string. This field is calculated by the user's latitude and longitude attributes. It is recommended to add indexes for frequently used fields.
CREATE TABLE `nearby_user_geohash` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL COMMENT '名称',
`longitude` double DEFAULT NULL COMMENT '经度',
`latitude` double DEFAULT NULL COMMENT '纬度',
`geo_code` varchar(64) DEFAULT NULL COMMENT '经纬度所计算的geohash码',
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `index_geo_hash` (`geo_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
First, based on the user's latitude and longitude information, calculate the geoHash
code of the user's coordinates after specifying the accuracy , and then obtain the code of 8 directions around the user geoHash
to search the user in the database, and finally filter out the user who exceeds the given distance (within 500 meters).
private SpatialContext spatialContext = SpatialContext.GEO;
/***
* 添加用户
* @return
*/
@PostMapping("/addUser")
public boolean add(@RequestBody UserGeohash user) {
//默认精度12位
String geoHashCode = GeohashUtils.encodeLatLon(user.getLatitude(),user.getLongitude());
return userGeohashService.save(user.setGeoCode(geoHashCode).setCreateTime(LocalDateTime.now()));
}
/**
* 获取附近指定范围的人
*
* @param distance 距离范围(附近多远的用户) 单位km
* @param len geoHash的精度(几位的字符串)
* @param userLng 当前用户的经度
* @param userLat 当前用户的纬度
* @return json
*/
@GetMapping("/nearby")
public String nearBySearch(@RequestParam("distance") double distance,
@RequestParam("len") int len,
@RequestParam("userLng") double userLng,
@RequestParam("userLat") double userLat) {
//1.根据要求的范围,确定geoHash码的精度,获取到当前用户坐标的geoHash码
GeoHash geoHash = GeoHash.withCharacterPrecision(userLat, userLng, len);
//2.获取到用户周边8个方位的geoHash码
GeoHash[] adjacent = geoHash.getAdjacent();
QueryWrapper<UserGeohash> queryWrapper = new QueryWrapper<UserGeohash>()
.likeRight("geo_code",geoHash.toBase32());
Stream.of(adjacent).forEach(a -> queryWrapper.or().likeRight("geo_code",a.toBase32()));
//3.匹配指定精度的geoHash码
List<UserGeohash> users = userGeohashService.list(queryWrapper);
//4.过滤超出距离的
users = users.stream()
.filter(a ->getDistance(a.getLongitude(),a.getLatitude(),userLng,userLat)<= distance)
.collect(Collectors.toList());
return JSON.toJSONString(users);
}
/***
* 球面中,两点间的距离
* @param longitude 经度1
* @param latitude 纬度1
* @param userLng 经度2
* @param userLat 纬度2
* @return 返回距离,单位km
*/
private double getDistance(Double longitude, Double latitude, double userLng, double userLat) {
return spatialContext.calcDistance(spatialContext.makePoint(userLng, userLat),
spatialContext.makePoint(longitude, latitude)) * DistanceUtils.DEG_TO_KM;
}
Is 、 Redis + GeoHash
Redis 3.2
After the version, based on the geohash
data structure Zset
provides geographic location-related functions. Through the above two mysql
implementation methods, it 附近的人
is found that the function is obvious to read more and write less scenes, so the redis
performance will be greatly improved.
1. Design ideas
redis
The 附近的人
function is realized mainly through Geo
the six commands of the module.
GEOADD
: Add the given location object (latitude, longitude, name) to the specified key;GEOPOS
: Return the position (longitude and latitude) of all objects at a given position from the key;GEODIST
: Return the distance between two given positions;GEOHASH
: Returns a Geohash representation of one or more location objects;GEORADIUS
: Take the given latitude and longitude as the center, return all the position objects in the target collection whose distance from the center does not exceed the given maximum distance;GEORADIUSBYMEMBER
: Take the given position object as the center and return all the position objects whose distance does not exceed the given maximum distance.
Take GEOADD
commands and GEORADIUS
commands as simple examples:
GEOADD key longitude latitude member [longitude latitude member ...]
Among them, key
is the collection name, member
which is the object corresponding to the latitude and longitude.
GEOADD
Add multiple merchants' "hot pot restaurant" location information:
GEOADD hotel 119.98866180732716 30.27465803229662 火锅店
GEORADIUS
According to a given latitude and longitude as the center position of the object to get all of the distance from the center of the target set not to exceed a given maximum distance (500 meters), that is “附近的人”
.
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count] [STORE key] [STORedisT key]
Range unit: m
| km
| ft
| mi
-> meters | kilometers | feet | miles.
WITHDIST
: While returning the position object, the distance between the position object and the center is also returned. The unit of distance is consistent with the range unit given by the user.WITHCOORD
: Also return the longitude and latitude of the location object.WITHHASH
: In the form of a 52-bit signed integer, returns the ordered set score of the location object after the original geohash encoding. This option is mainly used for low-level applications or debugging, the actual effect is not great.ASC | DESC
: Returns the position object element from near to far | Returns the position object element from far to near.COUNT count
: Select the first N matching position object elements. (Return all elements if not set)STORE key
: Save the geographic location information of the returned result to the specified key.STORedisT key
: Save the distance of the returned result from the center point to the specified key.
For example, the following command: Get all restaurants within 500 meters of the current location.
GEORADIUS hotel 119.98866180732716 30.27465803229662 500 m WITHCOORD
Redis
An ordered set ( zset
) is used internally to store the user's location information. zset
Each element in the is an object with a position. The score
value of the element is a 52-bit geohash
value calculated by latitude and longitude .
2. Analysis of pros and cons
redis
The implementation 附近的人
efficiency is relatively high, the integration is relatively simple, and it also supports sorting the distance. However, there is a certain error in the results. To make the results more accurate, you need to manually calculate the distance between the user's center position and other user positions, and then filter again.
3. Realization
The following is the Java
redis
implementation version, the code is very concise.
@Autowired
private RedisTemplate<String, Object> redisTemplate;
//GEO相关命令用到的KEY
private final static String KEY = "user_info";
public boolean save(User user) {
Long flag = redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation<>(
user.getName(),
new Point(user.getLongitude(), user.getLatitude()))
);
return flag != null && flag > 0;
}
/**
* 根据当前位置获取附近指定范围内的用户
* @param distance 指定范围 单位km ,可根据{@link org.springframework.data.geo.Metrics} 进行设置
* @param userLng 用户经度
* @param userLat 用户纬度
* @return
*/
public String nearBySearch(double distance, double userLng, double userLat) {
List<User> users = new ArrayList<>();
// 1.GEORADIUS获取附近范围内的信息
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.收集信息,存入list
List<GeoResult<RedisGeoCommands.GeoLocation<Object>>> content = reslut.getContent();
//3.过滤掉超过距离的数据
content.forEach(a-> users.add(
new User().setDistance(a.getDistance().getValue())
.setLatitude(a.getContent().getPoint().getX())
.setLongitude(a.getContent().getPoint().getY())));
return JSON.toJSONString(users);
}
Six, MongoDB + 2d index
1. Design ideas
MongoDB
Realize people in the vicinity, mainly through its two geospatial indexes 2dsphere
and 2d
. The bottom layer of the two indexes is still based on the Geohash
construction. But Geohash
there are some differences from the international ones, please refer to the official documents for details .
2dsphere
The index only supports querying the geometry of spherical surfaces.
2d
The index supports planar geometry and some spherical queries. Although the 2d
index some support queries ball, but 2d
when the index for these spherical query, you may be wrong. So try to choose spherical query 2dsphere
the index.
Although the methods of the two indexes are different, as long as the coordinate span is not too large, the difference between the distances calculated by the two indexes is almost negligible.
2. Realization
First insert data into a number of position MongoDB
, collection
is named hotel
, the equivalent MySQL
of the table name. Two field name
names location
are longitude and latitude data pairs.
db.hotel.insertMany([
{'name':'hotel1', location:[115.993121,28.676436]},
{'name':'hotel2', location:[116.000093,28.679402]},
{'name':'hotel3', location:[115.999967,28.679743]},
{'name':'hotel4', location:[115.995593,28.681632]},
{'name':'hotel5', location:[115.975543,28.679509]},
{'name':'hotel6', location:[115.968428,28.669368]},
{'name':'hotel7', location:[116.035262,28.677037]},
{'name':'hotel8', location:[116.024770,28.68667]},
{'name':'hotel9', location:[116.002384,28.683865]},
{'name':'hotel10', location:[116.000821,28.68129]},
])
Next we have to location
create a field 2d
index, an index of accuracy by bits
specifying, bits
the greater the accuracy of the index is higher.
db.coll.createIndex({'location':"2d"}, {"bits":11111})
Use the geoNear
command to test whether the near
current coordinates (latitude and longitude), spherical
whether to calculate the spherical distance, distanceMultiplier
the radius of the earth, the unit is meters, the default is 6378137, the maxDistance
filter conditions (users within the specified distance), open radians need to be divided distanceMultiplier
, the distanceField
calculated distance between two points , Field alias (arbitrarily named).
db.hotel.aggregate({
$geoNear:{
near: [115.999567,28.681813], // 当前坐标
spherical: true, // 计算球面距离
distanceMultiplier: 6378137, // 地球半径,单位是米,那么的除的记录也是米
maxDistance: 2000/6378137, // 过滤条件2000米内,需要弧度
distanceField: "distance" // 距离字段别名
}
})
When you see the data that meets the conditions in the result, there is an extra field distance
. The alias you just set represents the distance between two points.
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e58"), "name" : "hotel10", "location" : [ 116.000821, 28.68129 ], "distance" : 135.60095397487655 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e51"), "name" : "hotel3", "location" : [ 115.999967, 28.679743 ], "distance" : 233.71915803517447 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e50"), "name" : "hotel2", "location" : [ 116.000093, 28.679402 ], "distance" : 273.26317035334176 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e57"), "name" : "hotel9", "location" : [ 116.002384, 28.683865 ], "distance" : 357.5791936927476 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e52"), "name" : "hotel4", "location" : [ 115.995593, 28.681632 ], "distance" : 388.62555058249967 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e4f"), "name" : "hotel1", "location" : [ 115.993121, 28.676436 ], "distance" : 868.6740526419927 }
to sum up
The focus of this article is not on specific implementation, it is to provide you with some design ideas. You may not have a deep understanding of a certain technology during the interview, but if your knowledge is wide, you can say a variety of designs If you can talk openly, the interviewer will be greatly appreciated, and the probability of getting an offer will be much higher. Moreover “附近的人”
, there are many scenarios where functions are used, especially the e-commerce platform is more widely used, so students who want to enter the big factory, such knowledge points should still be understood.
Code implementation draws a big brother open source project, there are demo the first three implementations, little interested partners can learn about, GitHub address: https://github.com/larscheng/larscheng-learning-demo/tree/master/NearbySearch
.
Small benefits:
Recently, many friends around me have been interviewing. I have collected some Java architecture, interview materials, and some paid courses. Hush ~, free for friends. If you need it, you can pay attention to my official account , reply [ 666 ], and collect it without routine