项目总结-通过经纬度将地球表面分块

来源:http://chenjingbo.iteye.com/blog/2002499

1 前言

    最近做了一个项目,需求是某一个母体,通过手机摇一摇寻找身边同时在摇的人,然后把自己的红包分给这些人.其实有点类似微信的约炮功能.都是基于地理位置找附近的人.两者的区别就是微信约炮只需要找附近的一个人,而母体裂变是一对多的.而且微信也没有母体的概念(也就是没有一个是主动方,一个是被动方)

 

2 需求分析

    找附近的人,这个需求说起来简单,其实还是挺麻烦的.首先,像这种活动(送红包),参与人数肯定不在少数.据我事后的统计,同时参与的人数峰值是每两分钟32W,大概是每秒3K上下.可以明确的是,我们不可能针对 每一次请求都计算所有用户之间的距离.那么,我们自然而然的就会想到,将用户进行分区.每次请求只关心自己区里的用户距离.而其中,当时我想到的分区方式有两种

写道
1 直接按照城市分区.每个城市作为一个池子.
2 将地球表面按照经纬度进行分割.每一块分割的区域作为一个池子.

     这两种方式的优略点也很明显. 第一种优点是,实现方便,现在高德,google,阿里云都支持通过经纬度直接获取对应的地址信息,缺点也相对较多,1 最大的缺点是效率,我所了解的根据经纬度查询地址的接口都是http接口,速度非常慢.2 无法控制区域大小.每个城市大小都是固定的.而且每个城市大小之间相差很大. 第二种的优点是可控,只需要完成一个相对合理的分区域算法,那么这个区域大小就由你自己控制,而且一般来说效率会非常高.缺点也同样明显,需要一个靠谱的分区域算法.

     最后,我选择的是第二种方式.那么下面就是考虑怎么根据经纬度做一个分区算法了.

 

3 如何分区

    经纬度的单位有度,分,秒,毫秒.那么可以认为

写道
经度从-180.000000~180000000
维度从-90.000000~90000000

     通过经纬度,我们需要确定当前的位置应该落入哪个区域中.从技术的角度来说,就是需要合并两个值生成一个值,这个值作为当前经纬度的一个序列值(这里说序列值主要是强调唯一性).我想到的法子是,

写道
经纬度分别转换成二进制,然后通过奇偶位插入的方式生成对于的序列值..

 类似的效果就是这样


对于的代码

 

Java代码   收藏代码
  1. private long encode(long lon,long lat){  
  2.     long area = 0;  
  3.     //先设置经度,避免负数,直接加180  
  4.     long newLon = lon + 180000000;  
  5.     for(int i = 0; i < 64; i += 2){  
  6.         if(theBitTrue(newLon,i)){  
  7.             area = setTheBitTrue(area,i);  
  8.         } else {  
  9.             area = setTheBitFalse(area,i);  
  10.         }  
  11.     }  
  12.     //再设置维度  
  13.     long newLat = lat + 90000000;  
  14.     for(int i = 0; i < 64; i += 2){  
  15.         if(theBitTrue(newLat,i)){  
  16.             area = setTheBitTrue(area,i+1);  
  17.         } else {  
  18.             area = setTheBitFalse(area,i+1);  
  19.         }  
  20.     }  
  21.     return area;  
  22. }  
  23.   
  24. /** 
  25.  * 判断num上  第point位上是否为1 
  26.  * point 从0开始. 
  27.  */  
  28. private boolean theBitTrue(long num,int point){  
  29.     long bits = 1l << point;  
  30.     return (bits & num) == bits;  
  31. }  
  32.   
  33. /** 
  34.  * 将num的第point位设置成1,然后返回 
  35.  */  
  36. private long setTheBitTrue(long num,int point){  
  37.     return num |= (1l << point);  
  38. }  
  39.   
  40. /** 
  41.  * 将num的第point位设置成0,然后返回 
  42.  */  
  43. private long setTheBitFalse(long num,int point){  
  44.     return num &= ~(1l << point);  
  45. }  

     分区弄好了,那么每一个区有多大呢.

写道
按照上面的算法,序列值最大应该是经度接近360度的时候,那应该是
360000000 = 10101011101010010101000000000 一共是29位.然后加入维度,那么应该是58位.这样的话,我们可以近似的认为区域的总数为
4^29 = 288230376151711744.
地球总表面积是510072000平方千米=5100720000000000000平方厘米.
那么,每一个区域的面积应该是
5100720000000000000/288230376151711744 = 17平方厘米.

 

    计算好每个最小区域的大小,那么就很容易了.比如你需要17平方千米作为范围,那么就是 17平方千米/17平方厘米 = 10^10 如果是170平方千米,那就是10^11. 这个系数作为可变参数调节就好了..然后对应的序列值%这个系数,就是对应的池子的最终序列值啦.

 

    这里再补充一种算法,是同事写的,其实核心思想类似.

Java代码   收藏代码
  1. // 编码经纬度  
  2.     private long encode(long lon, long lat) {  
  3.         long mergeBits = 0L;  
  4.         mergeBits = processBitset(mergeBits, 59, lon, -180000000180000000);  
  5.         mergeBits = processBitset(mergeBits, 58, lat, -9000000090000000);  
  6.         return mergeBits;  
  7.     }  
  8.   
  9.   
  10.     // 纬度 下限 上限  
  11.     private long processBitset(long mergeBits, int start, long lat, long floor, long ceiling) {  
  12.         double dfloor = floor, dceiling = ceiling;  
  13.         for (int i = start; i >= 0; i -= 2) {  
  14.             double mid = (dfloor + dceiling) / 2;  
  15.             if (lat >= mid) {      // 上半阙  
  16.                 mergeBits |= (1L << i);     // 该为置1  
  17.                 dfloor = mid;       // 下限抬高  
  18.             } else {               // 下半阙  
  19.                 mergeBits &= ~(1L << i);    // bit位设为0  
  20.                 dceiling = mid;     // 上限降低  
  21.             }  
  22.         }  
  23.         return mergeBits;  
  24.     }  

    这个算法里面有一些有趣的东西.比如为什么要设置59和58,还有他这个看上去的区域总数是4^30次.比之前的那个算法多了4倍的精确率.等等.但是其实并不是表面的那样.具体的不多解释啦.自己理解了.

 

   后续

     本来本篇文章到这里应该就已经结束了.但是在项目结束以后,我忽然想到另外一个问题..我们知道,上面说的根据经纬度划分区域,然后在区域内计算彼此之间距离的方式有一个非常大的弊端

写道
无法处理边界问题

     也就是说,就算某两个坐标 ,虽然距离很近 ,但是由于坐标A刚好落入区域1中,坐标B刚好落入区域2中,那么这两个坐标永远不会落在一个区域内.两个人也就摇不到一个房间内.那么继续思考,如何可以解决这个问题..最简单的方式(当然,也是最不考虑性能的方式)肯定是

 

写道
裂变者,每次都计算与所有求包者(而不是区域内)之间的距离

       相信大家会本能的觉得,这样做肯定不靠谱.我最开始也是这么认为的,但是我在深入考虑以后我发现,我忽视了一个很大的问题.

写道
裂变者关心的是与所有求包者的距离排序,而不是计算与每个求包者的真实的距离

      在上面,我们为每一个经纬度组合生成了一个彼此完全不一致的区域ID,其实,两个经纬度之间距离的大小,完全可以通过两个经纬度对应的最小区域ID异或操作得出.也就是说,

写道
坐标A与坐标B的相对距离,我们可以通过下面的方式生成
坐标A对应的序列值为long类型的数值 ida ,坐标B对应的序列值为 idb.两者之间的距离可以近似的认为是 ida^idb

      我们知道,异或操作对于计算机是非常快的..根据事后的数据观察,求包者请求的峰值是3k qps.每次请求有效期为10s,也就是说,同时有效的求包者应该是3W左右.那么,我们可以通过代码演示一下如果裂变者每次请求都与3W个求包者计算距离的消耗是多少

Java代码   收藏代码
  1. package location;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5. import java.util.Random;  
  6.   
  7. /** 
  8.  * 
  9.  */  
  10. public class LocationTest {  
  11.   
  12.     private static final int NUM = 30000;  
  13.     static List<Long> locationList = new ArrayList<Long>(NUM);  
  14.     static List<Long> temp = new ArrayList<Long>(NUM);  
  15.     static final long location;  
  16.   
  17.     static {  
  18.         Random random = new Random();  
  19.         location = random.nextLong();  
  20.         for (int i = 0; i < NUM; i++) {  
  21.             locationList.add(random.nextLong());  
  22.         }  
  23.     }  
  24.   
  25.   
  26.     public static void main(String[] args) {  
  27.         //为了预热  
  28.         for (Long locationA: locationList) {  
  29.             temp.add(locationA ^ location);  
  30.         }  
  31.         temp.clear();  
  32.         long start = System.currentTimeMillis();  
  33.         for (Long locationA: locationList) {  
  34.             temp.add(locationA ^ location);  
  35.         }  
  36.         long end = System.currentTimeMillis();  
  37.         System.out.println(end - start);  
  38.     }  
  39. }  

     我在本机运行了一下,结论是6ms.也就是说,我们可以认为,如果每个裂变者与所有求包者计算一次距离,只要短短的6ms.我们是否可以认为,不分区域是完全可行的.

     可惜下面的内容都是我在项目结束以后才想到的.不然可以在项目中试验一把,那就真的完美了.

 

 总结

    项目中后续当然还有其他的内容,比如池子获取到以后,然后再每次计算池子里的人与母体的距离然后进行排序.不过这个就相对容易很多了.根据经纬度计算距离的方法google一下就好了.很多..这里就不多说了.呵呵,当码农几年,第一次用了二进制的位运算,也挺有趣的.

 

 


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值