有偿问答
面经分享
技术探讨
资料领取
登录
程序员玩‘附近的人’,妹子还没泡,先学会了个专业技能!
社长
1年前
⋅ 727 阅读
唉,作为一个程序员,好无聊呀!!!每天不是打代码,就是玩游戏。什么时候,我才能谈一场像这样甜甜的恋爱! 好寂寞,好空虚,好冷!没有妹子的日子,我真的好孤单。 不知道附近有没美女邂逅呢?现在网恋时代,以我的代码水平,找个女网友网恋还不是分分钟的事情! 打开附近的人,只搜索女生,哇,好多靓女的,就几百米距离!! ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211120/1c0b2a1431f6457a825e60f99363851f.png) 咦?附近的人,这功能怎么实现的? 不行,赶紧学习一下,万一等下妹子问我:你知道附近的人这功能是怎么实现的呢?我要是答不出来,那我不是显得我很差劲! 百度搜索关键字“附近的人 Java”,一通乱点击和浏览之后,我似乎明白了一些原理! 其实针对“附近的人”这一位置服务领域应用场景,常见实现中间件可使用 PostgreSQL、Redis 和 MongoDB。PostgreSQL和mongoDB有些人还没接触过,所以呀今天我们借助Redis+GeoHash来实现附近的人这个功能。 没错,自3.2版本之后,Redis基于[geohash](https://en.wikipedia.org/wiki/Geohash)和[有序集合Zset](http://redisdoc.com/sorted_set/index.html)提供了地理位置相关功能。所以说论实际操作,我们使用好Redis的几条命令行就可以实现附近的人这个功能了。 为了更加深入学习,预防妹子问及附近的人的人具体实现原理,所以我必须加班加点把GeoHash算法弄明白。 唉,女人心,难猜呀! **GeoHash原理** 那么,GeoHash到底是啥呢?简单来说,GeoHash就是一种能将二维的经纬度转换成一串可以排序,可以比较的有意义的字符串编码的算法。 平常我们对某一位置的定位,一般都是使用经纬度来进行标记的。比如广州塔经纬度大概是北纬23.1066805,东经113.3245904,我们把这个数据存储到数据库中,当我们需要查询广州塔附近的店或人的时候,我们通常需要对经纬度之间进行批量运算才能得到结果,耗时太长,这就不太友好。为了解决这个问题,GeoHash算法把经纬度转化成一串可靠的字符串,而这串字符串它其实表示的是一个矩形的范围,并不是一个点。比如广州塔的经纬度可转化成ws0e6y2q,越靠前的编码表示的范围越大,ws0e6y2q就肯定在ws0e6y2范围内。有了这个特性,我们可以查询附近的店和人:select * from table where geohash like 'ws0e6y2%'。 那么,GeoHash算法到底是如和将经纬度一步步换算成一串字符串的呢?以及字符串长度代表的范围到底是多少呢? 换算GeoHash其实有三步: **1、将经纬度变成二进制** 比如广州塔的纬度是23.1066805。而纬度的范围是(-90,90),其中间值为0,当纬度在中间值的左边区值时候得0,右边得1。23.1066805在区间(0,90),因此得1。然后我们对(0,90)再取中间值45,23.1066805在区间(0,45),是(0,90)左边因此得0。以此算法继续算下去,如图: ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211120/d4eee037c6cb42c7b5f8ca389b4b16fe.png) 最终得到最终得二进制值为 ```plain 10100000110111001110 ``` 计算过程如下: 同理,经度113.3245904得到的二进制表示为: ```plain 11010000100101100001 ``` **2、将经纬度合并** 然后将得到的经纬度二进制值的每位数按照以下规则:经度占偶数位,纬度占奇数位,合并后得到,注意从0开始数哈,0是偶数位: ```plain 11100 11000 00000 01101 00110 11110 00010 10110 ``` **3、按照Base32进行编码** 然后将经纬度的二进制编码按5个为一组,转换为十进制,得到的十进制数为: ```plain 28 24 0 13 6 30 2 22 ``` 然后对照以下的Base32编码表: ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211120/cb3d5707791643f898b47d5d62c81bf3.png) 最后得到GeoHash字符串为 ```plain ws0e6y2q ``` 将GeoHash换算成经纬度则规则反过来即可。 我们都知道GeoHash字符串代表的是一个区域范围,当字符串越长的时候,范围越小,位置就越精确,可以对照以下表格,编码长度为8时,精度在19米左右。 ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211120/8b9e704a54de45439b457c05841b89dd.png) 同时可以得出结论:GeoHash字符串编码越相似表示距离越近。利用字符串的前缀匹配,可以查询附近的地理位置。这样就实现了快速查询某个坐标附近的地理位置。 好了,终于弄明白了GeoHash的底层原理,接下来我们聊聊Redis Geo的实现。 Redis GEO实现主要包含了以下两项技术: 1、使用geohash转化保存地理位置的坐标。 2、使用有序集合(zset)保存地理位置的集合。 常见的redis方法命令行有以下几条: ```plain #geoadd:添加地理位置的坐标。 GEOADD key longitude latitude member [longitude latitude member ...] #geopos:获取地理位置的坐标。 GEOPOS key member [member ...] #geodist:计算两个位置之间的距离。 GEODIST key member1 member2 [m|km|ft|mi] #georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。 GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key] #georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。 GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key] #geohash:返回一个或多个位置对象的 geohash 值。 GEOHASH key member [member ...] ``` 上面没有删除的方法,因为redis Geo底层使用的是zset,所以我们可以使用zset的相关命令来进行删除操作。 ok,相关的原理和命令操作我们都熟悉了之后,接下来我们进入项目实战,我们来做一个简单的实例来完成附近的店或人的功能。 我们分为以下几个步骤: 1. 用户登录,浏览器授权获取用户经纬度 2. 初始化内置城市的地理位置 3. 获取用户的城市距离、或附近1000米的店或人 项目实战: 首先我们需要在pom.xml中引入redis相关的集成包,然后我们需要做一些页面,所以我把freemarker的相关包也引入了进来: * pom.xml ```plain
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-freemarker
org.springframework.boot
spring-boot-starter-web
``` 登录页面操作这里我就省略了,登录完成之后,HttpSession中可以获取到key为“username”当前登录用户的昵称,相关操作如下: ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211120/f8bbdfb7f2724ea191799d0377f24a26.png) * IndexController ```plain @GetMapping({"/login"}) public String login() { return "login"; } @PostMapping({"/login"}) public String doLogin(String username, HttpSession session) { if (!StringUtils.hasLength(username)) { return "redirect:/login"; } session.setAttribute("username", username); return "redirect:/index"; } ``` 在获取用户附近的店和城市之前,我们先要获取用户的经纬度定位,通过百度搜索发现,可以通过js的navigator.geolocation来获取设备的当前位置,返回一个位置对象,用户可以从这个对象中得到一些经纬度的相关信息。这就完美了,于是我在前端页面中设置一个获取位置的按钮,点击按钮之后通过js获取到经纬度的信息: * index.html ```plain 经纬度:
# js方法: function showIp() { var ipinput = document.getElementById("ip-input"); if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( function (position) { # 获取到经纬度 console.log(position.coords.longitude); console.log(position.coords.latitude); ipinput.value = position.coords.longitude + "," + position.coords.latitude; # 获取用户距离各大城市的距离 showNear(position.coords.longitude, position.coords.latitude); }, function (e) { alert("请先在设置中允许浏览器获取地理位置!"); throw(e.message); } ) } } function showNear(longitude, latitude) { // ... } ``` 页面中,我们通过js获取到经纬度之后,把经纬度显示在了输入框中。当然了,过程中浏览器会发起是否允许页面获取位置的提示,我们需要允许操作,这样才能获取到定位。 当然了,你也不必担忧系统会泄漏你的定位,因为navigator.geolocation默认获取的不是高精度的经纬度,而我也并没有设置获取高精度。这个可以通过查看页面源码发现。 ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211120/910029893c35485393d6cfb0dc2df4bf.png) 有了用户名和用户的经纬度之后,我们就可以获取附近的店和人了。为了上线之后不记录广大用户的坐标定位,所以这里我们改成勘测用户距离各大城市的距离。因此我们需要提前初始化城市的经纬度坐标,在springboot项目中,就是项目启动时候初始化,因此我们写一个类ContextStartup实现ApplicationRunner接口,重写run方法实现城市初始化。 * com.markerhub.config.ContextStartup ```plain @Slf4j @Component public class ContextStartup implements ApplicationRunner { @Autowired RedisService redisService; @Override public void run(ApplicationArguments args) throws Exception { // 项目启动初始化城市的坐标 redisService.getCityList().forEach(e -> redisService.addLocation(e.getName(), e.getLng(), e.getLat()) ); log.info("城市坐标初始化成功~~"); } } ``` 当然了,我们已经在RedisService中记录了城市的坐标记录,本来这些数据我们应该放在数据库中的,为了减少操作,所以直接写死了: * com.markerhub.service.RedisService#getCityList ```plain public List
getCityList() { List
locations = new ArrayList<>(); // 通过坐标地图查个大概 locations.add(new Location().setName("北京").setLng(116.404763).setLat(39.913359)); locations.add(new Location().setName("上海").setLng(121.471341).setLat(31.23667)); locations.add(new Location().setName("广州").setLng(113.271429).setLat(23.135602)); locations.add(new Location().setName("深圳").setLng(114.066277).setLat(22.548723)); locations.add(new Location().setName("杭州").setLng(120.21436).setLat(30.251834)); locations.add(new Location().setName("武汉").setLng(114.309286).setLat(30.59971)); return locations; } ``` 当然了,Location这个传输工具类也是需要定义的: * com.markerhub.vo.Location ```plain @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) public class Location implements Serializable { String name; double distance; double lng; double lat; } ``` ok,有了这些基数数据之后,接下来我们只需要把用户的经纬度坐标传到后端,然后就可以计算用户距离各大城市的距离啦。当然了我们需要先把用户的坐标添加到redis中, ```plain #geoadd:添加地理位置的坐标。 GEOADD key longitude latitude member [longitude latitude member ...] ``` 然后通过member名称来比较距离: ```plain #geodist:计算两个位置之间的距离。 GEODIST key member1 member2 [m|km|ft|mi] ``` 那么,我们先在项目service中完成这两个方法的Java实现。首先是redis添加坐标,这个方法简单,我们只需要一行代码就能搞定,具体其实就是调用redisTemplate.opsForGeo().add这个方法而已。 * com.markerhub.service.RedisService ```plain @Service public class RedisService { public final static String KEY = "user_distance"; @Autowired RedisTemplate redisTemplate; /** * 添加坐标 */ public boolean addLocation(String name, double lng, double lat) { Long flag = redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation(name, new Point(lng, lat))); return flag != null && flag > 0; } } ``` 可以看到,我们把用户昵称最为坐标的位置名称(member)添加到指定的 key 中。接下来是测试用户到各大城市的距离,我们需要先获取所有城市的坐标,然后调用redisTemplate.opsForGeo().distance方法就可以通过位置名称计算出位置距离。具体实现如下: * com.markerhub.service.RedisService#getCityDistance ```plain /** * 获取所有城市的距离 */ public List
getCityDistance(String point) { List
locations = this.getCityList(); locations.forEach(e -> { Distance distance = redisTemplate.opsForGeo().distance(KEY, point, e.getName(), RedisGeoCommands.DistanceUnit.KILOMETERS); e.setDistance(distance.getValue()); }); return locations; } ``` 这样Location的列表中就赋值了位置距离。service搞定之后,我们在controller中进行调用: * com.markerhub.IndexController ```plain @Controller public class IndexController { @Autowired RedisService redisService; /** * 测试距离 */ @ResponseBody @PostMapping("/range") public List
range(HttpSession session, double lng, double lat) { String username = (String) session.getAttribute("username"); redisService.addLocation(username, lng, lat); List
cityDistances = redisService.getCityDistance(username); return cityDistances; } ... } ``` 然后在前端中我们只需调用该链接然后把结果循环展示出来即可。具体实现如下: * index.html ```plain 你与大城市距离:
... function showNear(longitude, latitude) { $.post('/range', { lng: longitude, lat: latitude }, function (res) { var html = ""; res.forEach(e => { html += (e.name + "->" + e.distance + "公里、") }) $("#nearcity").text(html); }); } ``` 这个方法,我们在showIp() 中获取到用户的定位坐标之后调用即可。 ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211120/4e74ec04bf1b43889b3f9980ad23c46d.png) 最终结果如下: ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211120/40701245754c481ca795de05a81f3231.png) 有些人像知道怎么获取附近1000米距离内容的店或人,这个其实也简单,在redis中其实也就一个方法调用,可以在service中写出实现: * com.markerhub.service.RedisService#range ```plain /** * 获取某坐标附近多少公里内的坐标 */ public List
range(double distance, double lng, double lat) { List
locations = new ArrayList<>(); GeoResults
> reslut = redisTemplate.opsForGeo() .radius(KEY, new Circle(new Point(lng, lat), new Distance(distance, Metrics.KILOMETERS)), RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs() .includeDistance() .includeCoordinates().sortAscending()); List
>> content = reslut.getContent(); content.forEach(a-> locations.add( new Location().setDistance(a.getDistance().getValue()) .setName(a.getContent().getName().toString()) .setLat(a.getContent().getPoint().getX()) .setLng(a.getContent().getPoint().getY()))); return locations; } ``` 通过redisTemplate.opsForGeo().radius方法即可轻松获取附近的店和人的坐标以及距离。 **结束** 好了,功能终于开发完毕,顿时觉得信心爆棚,面对妹子毫无胆怯。咦,这个头像不错,就她了,于是,我发出了第一句对白:“sout 你好呀,美女!” 欲知后事如何,关注公众号:MarkerHub,后续连续更新最新集哈。
阅读全部
全部评论:
0
条
我有话说:
@
发送
-- 目录 --
关注官方公众号:
Java问答社
接收最新有赏问答推送!
最新发布
1.
SpringBoot 接口数据加解密技巧,so easy!
2.
一个依赖搞定 Spring Boot 反爬虫,防止接口盗刷!
3.
Java8 Stream 极大简化了代码,它是如何实现的?
4.
马上大四了,秋招还是春招好?先找工作还是找实习?
5.
万字详解 Linux 常用指令(值得收藏)
6.
4年工作经验,多线程间的5种通信方式都说不出来,你敢信?
最新评论
部署文档没有了,您能提供下吗
部署文档没有了,能提供下吗
我测你的🐎
源码从哪里获取请问
想学
那篇石墨文档 没有权限查看哇