大家好,我是吕一明。

唉,作为一个程序员,好无聊呀!!!每天不是打代码,就是玩游戏。什么时候,我才能谈一场像这样甜甜的恋爱!

好寂寞,好空虚,好冷!没有妹子的日子,我真的好孤单。

不知道附近有没美女邂逅呢?现在网恋时代,以我的代码水平,找个女网友网恋还不是分分钟的事情!

打开附近的人,只搜索女生,哇,好多靓女的,就几百米距离!!

咦?附近的人,这功能怎么实现的?

不行,赶紧学习一下,万一等下妹子问我:你知道附近的人这功能是怎么实现的呢?我要是答不出来,那我不是显得我很差劲!

百度搜索关键字“附近的人 Java”,一通乱点击和浏览之后,我似乎明白了一些原理!

其实针对“附近的人”这一位置服务领域应用场景,常见实现中间件可使用 PostgreSQL、Redis 和 MongoDB。PostgreSQL和mongoDB有些人还没接触过,所以呀今天我们借助Redis+GeoHash来实现附近的人这个功能。

没错,自3.2版本之后,Redis基于geohash和有序集合Zset提供了地理位置相关功能。所以说论实际操作,我们使用好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。以此算法继续算下去,如图:

最终得到最终得二进制值为

10100000110111001110

计算过程如下:同理,经度113.3245904得到的二进制表示为:

11010000100101100001

2、将经纬度合并

然后将得到的经纬度二进制值的每位数按照以下规则:经度占偶数位,纬度占奇数位,合并后得到,注意从0开始数哈,0是偶数位:

11100 11000 00000 01101 00110 11110 00010 10110

3、按照Base32进行编码

然后将经纬度的二进制编码按5个为一组,转换为十进制,得到的十进制数为:

28 24 0 13 6 30 2 22

然后对照以下的Base32编码表: 

最后得到GeoHash字符串为

ws0e6y2q

将GeoHash换算成经纬度则规则反过来即可。

我们都知道GeoHash字符串代表的是一个区域范围,当字符串越长的时候,范围越小,位置就越精确,可以对照以下表格,编码长度为8时,精度在19米左右。

同时可以得出结论:GeoHash字符串编码越相似表示距离越近。利用字符串的前缀匹配,可以查询附近的地理位置。这样就实现了快速查询某个坐标附近的地理位置。

好了,终于弄明白了GeoHash的底层原理,接下来我们聊聊Redis Geo的实现。

* B站视频讲解:https://www.bilibili.com/video/BV1544y1Y7pS/

Redis GEO实现主要包含了以下两项技术:

1、使用geohash转化保存地理位置的坐标。

2、使用有序集合(zset)保存地理位置的集合。

常见的redis方法命令行有以下几条:

#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

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

登录页面操作这里我就省略了,登录完成之后,HttpSession中可以获取到key为“username”当前登录用户的昵称,相关操作如下:

  • IndexController

@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

经纬度:
<input type="text" id="ip-input" style="height: 19px;" readonly>
<input type="button" value="快速定位" onclick="showIp()"># 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默认获取的不是高精度的经纬度,而我也并没有设置获取高精度。这个可以通过查看页面源码发现。

有了用户名和用户的经纬度之后,我们就可以获取附近的店和人了。为了上线之后不记录广大用户的坐标定位,所以这里我们改成勘测用户距离各大城市的距离。因此我们需要提前初始化城市的经纬度坐标,在springboot项目中,就是项目启动时候初始化,因此我们写一个类ContextStartup实现ApplicationRunner接口,重写run方法实现城市初始化。

  • com.markerhub.config.ContextStartup

@Slf4j
@Component
public class ContextStartup implements ApplicationRunner {@AutowiredRedisService redisService;@Overridepublic 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

public List<Location> getCityList() {List<Location> 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

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Location implements Serializable {String name;double distance;double lng;double lat;
}

ok,有了这些基数数据之后,接下来我们只需要把用户的经纬度坐标传到后端,然后就可以计算用户距离各大城市的距离啦。当然了我们需要先把用户的坐标添加到redis中,

#geoadd:添加地理位置的坐标。
GEOADD key longitude latitude member [longitude latitude member ...]

然后通过member名称来比较距离:

#geodist:计算两个位置之间的距离。
GEODIST key member1 member2 [m|km|ft|mi]

那么,我们先在项目service中完成这两个方法的Java实现。首先是redis添加坐标,这个方法简单,我们只需要一行代码就能搞定,具体其实就是调用redisTemplate.opsForGeo().add这个方法而已。

  • com.markerhub.service.RedisService

@Service
public class RedisService {public final static String KEY = "user_distance";@AutowiredRedisTemplate 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

/*** 获取所有城市的距离*/
public List<Location> getCityDistance(String point) {List<Location> 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

@Controller
public class IndexController {@AutowiredRedisService redisService;/*** 测试距离*/@ResponseBody@PostMapping("/range")public List<Location> range(HttpSession session, double lng, double lat) {String username = (String) session.getAttribute("username");redisService.addLocation(username, lng, lat);List<Location> cityDistances = redisService.getCityDistance(username);return cityDistances;}...
}

然后在前端中我们只需调用该链接然后把结果循环展示出来即可。具体实现如下:

  • index.html

你与大城市距离:
<span id="nearcity"></span>...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() 中获取到用户的定位坐标之后调用即可。

最终结果如下:

有些人像知道怎么获取附近1000米距离内容的店或人,这个其实也简单,在redis中其实也就一个方法调用,可以在service中写出实现:

  • com.markerhub.service.RedisService#range

/*** 获取某坐标附近多少公里内的坐标*/
public List<Location> range(double distance, double lng, double lat) {List<Location> locations = new ArrayList<>();GeoResults<RedisGeoCommands.GeoLocation<Object>> reslut = redisTemplate.opsForGeo().radius(KEY, new Circle(new Point(lng, lat), new Distance(distance, Metrics.KILOMETERS)),RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending());List<GeoResult<RedisGeoCommands.GeoLocation<Object>>> 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方法即可轻松获取附近的店和人的坐标以及距离。 结束

B站视频讲解:https://www.bilibili.com/video/BV1544y1Y7pS/

项目代码:https://github.com/MarkerHub/youchat

好了,功能终于开发完毕,顿时觉得信心爆棚,面对妹子毫无胆怯。咦,这个头像不错,就她了,于是,我发出了第一句对白:“sout 你好呀,美女!”

欲知后事如何,关注公众号:MarkerHub,后续连续更新最新集哈。

程序员玩‘附近的人’,妹子还没泡,先学会了个专业技能!相关推荐

  1. 大厂程序员辞职创业,赚的还没原来多!

    随着现在科学技术的进步,AI行业的兴起.有公司甚至愿意为AI工程师开出百万年薪.关于这个现象,不仅我国人民喜闻乐见,美国人民也争论不休. 最近一位名叫Jack Wilson的程序员发文,丢出一个问题: ...

  2. 程序员找不到对象是因为还没遇到一个有远见的丈母娘

    当别人在放肆秀恩爱的时候,程序员单身狗们在角落里瑟瑟发抖.别人去网站相亲找到对象,程序员去相亲找到BUG.其实,你找不到对象是因为你还没遇到一个有远见的丈母娘. 都说程序员很难找到对象,就知道整天对着 ...

  3. 程序员曝光美团面试骗局:还没发offer就让自己离职,离职后却说没有hc,拒绝发offer!

    大多数人在职求职时,为了保险,都会等到offer到手才会提出离职.但一个新东方的程序员却遇到了这样一件糟心事:去美团面试,前两轮面试通过后,部门leader跟他说决定录用他,让他先离职再发offer, ...

  4. 想作为程序员工作 需要什么_您不想作为程序员玩的游戏

    想作为程序员工作 需要什么 by Amy M Haddad 通过艾米·M·哈达德(Amy M Haddad) 您不想作为程序员玩的游戏 (The game you don't want to play ...

  5. Java程序员都30岁了,还剩下5年“寿命”,这就是所谓的中年危机?

    Java程序员都30岁了,还剩下5年"寿命",这就是所谓的中年危机? 30岁时,我是一个程序员,离传说中的"退休"只有5年了,为了优雅从容的所谓"光荣 ...

  6. Java程序员,上班那点事儿--程序员也是一般人

    误区4:程序员不是一般人 本文为清华大学出版社<Java程序员,上班那点事儿>节选. 从一个做会计的女生对程序员的误解说起: 那天和一个做会计的女生聊天,问她对程序员或者编程工作的看法.她 ...

  7. [Linux] PHP程序员玩转Linux系列-nginx初学者引导

    1.PHP程序员玩转Linux系列-怎么安装使用CentOS 2.PHP程序员玩转Linux系列-lnmp环境的搭建 3.PHP程序员玩转Linux系列-搭建FTP代码开发环境 4.PHP程序员玩转L ...

  8. [Linux] PHP程序员玩转Linux系列-备份还原MySQL

    1.PHP程序员玩转Linux系列-怎么安装使用CentOS 2.PHP程序员玩转Linux系列-lnmp环境的搭建 3.PHP程序员玩转Linux系列-搭建FTP代码开发环境 前几天有个新闻,说是g ...

  9. 程序员soul 012期|妹子|重庆

    程序员soul 012期|妹子|重庆 我们的愿景是打造全国最大的程序员社群生态圈!我们的使命是为所有程序员都找到另一半! 出生年月: 1998年6月 籍贯: 重庆 所在城市: 重庆 学历:专科 身高: ...

最新文章

  1. Leetcode: Validate Binary Search Tree
  2. 东哥读书小记 之 《一个广告人的自白》
  3. Redis的两种持久化介绍与对比
  4. 12个新鲜出炉的Web开发框架
  5. 关于线程池的一段代码
  6. 浅谈串口DCB流控制设置
  7. java seek_java中seek()的用法
  8. 颠覆你的世界观-芝诺悖论
  9. 电源防反接和防倒灌 - 使用MOS 管和运放实现理想二极管
  10. 比赛推送:ML/NLP/推荐/CV,一大波比赛来袭!
  11. 深入浅出——MVP模式
  12. sap--TCODE 之 SE93 将事务代码分配给程序(转)
  13. bzoj 2563阿狸和桃子的游戏
  14. java 求两个list 集合的交集,重复的元素
  15. 最主流的五个大数据处理框架的优势对比
  16. python之pip常用命令
  17. C语言中的字符常量与变量
  18. 与,或,非,异或,左移,右移,位运算符号总结
  19. 测试淘宝站内的搜索系统
  20. 华为开发者联盟上架Android 安装包 包名重复问题

热门文章

  1. vue二维码生成可自定义logo
  2. 关于solidity解析abi方法,入参和结果字节码
  3. Python pandas库|任凭弱水三千,我只取一瓢饮(5)
  4. Jmeter二次开发准备-Jmeter插件开发
  5. JavaScript中查找关键词
  6. 江在川上曰:js中的JSON解析和序列化
  7. ForestBlog博客源码学习笔记
  8. 径向基神经网络(rbfn)进行函数插值,代码实现
  9. 从RTP包中分析OPUS码流
  10. matlab离散系统pid控制系统,离散系统的数字PID控制仿真