• MongoDB整理笔记のjava MongoDB分页优化


        最近项目在做网站用户数据新访客统计,数据存储在MongoDB中,统计的数据其实也并不是很大,1000W上下,但是公司只配给我4G内存的电脑,让我程序跑起来气喘吁吁...很是疲惫不堪。

        最常见的问题莫过于查询MongoDB内存溢出,没办法只能分页查询。这种思想大家可能都会想到,但是如何分页,确实多有门道!

        网上用的最多的,也是最常见的分页采用的是skip+limit这种组合方式,这种方式对付小数据倒也可以,但是对付上几百上千万的大数据,却只能望而兴叹...

        经过网上各种查找资料,寻师问道的,发现了一种速度足以把skip+limit组合分页甩出几条街的方法。

        思路: 条件查询+排序+限制返回记录。边查询,边排序,排序之后,抽取第一次分页中的最后一条记录,作为第二次分页的条件,进行条件查询,以此类推....

        先上代码: 

     /**
         * 小于指定日期的所有根据UUID分组的访问记录
         * @param 指定日期
         * @return 所有访问记录的MAP
         */
        public static Multimap<String, Map<String, String>> getOldVisitors(String date){
            
            //每次查询的记录数
            int pagesize = 100000;
            
            //mongodb中的"_id"
            String objectId = "";
            
            //方法的返回值类型,此处用的google guava
            Multimap<String, Map<String, String>> mapless = null;
            
            //查询的条件
            BasicDBObject queryless = new BasicDBObject(),fields = new BasicDBObject(),field = new BasicDBObject();
            
            //初始化返回的mongodb集合操作对象,大家可以写个数据连接池
            dbCol = init();
            
            //查询指定字段,字段越少,查询越快,当然都是一些不必要字段
            field.put("uuid",1);
            
            fields.put("uuid", 1);
            
            fields.put("initTime", 1);
            
            //小于指定日期的条件
            String conditionless = TimeCond.getTimeCondless(date);
            
            queryless.put("$where", conditionless);
            
            DBCursor cursorless = dbCol.find(queryless,field);
            
            //MongoDB在小于指定日期条件下,集合总大小
            int countless = cursorless.count();
            
            //查询遍历的次数 circleCountless+1
            int circleCountless = countless/pagesize;
            
            //取模,这是最后一次循环遍历的次数
            int modless = countless%pagesize;
            
            //开始遍历查询
            for (int i = 1; i <=circleCountless+1; i++) {
                
                //文档对象
                DBObject obj = null;
                
                //将游标中返回的结果记录到list集合中,为什么放到list集合中?这是为后面guava 分组做准备
                List<Map<String, String>> listOfMaps = new ArrayList();
                
                //如果条件不为空,则加上此条件,构成多条件查询,这一步是分页的关键
                if (!"".equals(objectId)) {
                    
                      //我们通过文档对象obj.get("_id")返回的是不带ObjectId(),所以要求此步骤
                                   ObjectId id = new ObjectId(objectId);
                    
                       queryless.append("_id", new BasicDBObject("$gt",id));
                    
                }
                
                if (i<circleCountless+1) {
                    
                cursorless = dbCol.find(queryless,fields).sort(new BasicDBObject("_id", 1)).limit(pagesize);
                    
                }else if(i==circleCountless+1){//最后一次循环
                    
                    cursorless = dbCol.find(queryless,fields).limit(modless);
                }
                
                        //将游标中返回的结果记录到list集合中,为什么放到list集合中?这是为后面guava 分组做准备
                    while (cursorless.hasNext()) {
                        
                        obj = cursorless.next();
                        
                        listOfMaps.add((Map<String, String>) obj);
                        
                    }
                    //获取一次分页中最后一条记录的"_id",然后作为条件传入到下一个循环中
                    if (null!=obj) {
                        
                         objectId = obj.get("_id").toString();
                         
                        }
                //第一次分组,根据uuid分组,分组除今天之外的历史数据
            mapless = Multimaps.index(
                          listOfMaps,new Function<Map<String, String>, String>() {
                              public String apply(final Map<String, String> from) {
                                 
                                      return from.get("uuid");    
                          }
                     });
                  
              }    
        
            return mapless;
        }
    View Code

        这里为什么要用"_id"这个字段作为分页的条件?其实,我也用过其他字段,比如时间字段,时间字符串也是可以比大小的,但它的效率远不如"_id"高。

        关于MongoDB中的"_id",以前一直忽略它的作用,直接结果是让我耗了很多时间和精力,绕了大半圈,又回到了原点,有一种众里寻他千百度,蓦然回首,那人却在灯火阑珊处的感觉...

        MongoDB ObjectId

        “4e7020cb7cac81af7136236b”这个24位的字符串,虽然看起来很长,也很难理解,但实际上它是由一组十六进制的字符构成,每个字节两位的十六进制数字,总共用了12字节的存储空间。相比MYSQLint类型的4个字节,MongoDB确实多出了很多字节。不过按照现在的存储设备,多出来的字节应该不会成为什么瓶颈。不过MongoDB的这种设计,体现着空间换时间的思想。官网中对ObjectId的规范,如图所示:

         

        1)Time

        时间戳。将刚才生成的objectid的前4位进行提取“4e7020cb”,然后按照十六进制转为十进制,变为“1315971275”,这个数字就是一个时间戳。通过时间戳的转换,就成了易看清的时间格式。

        2)Machine

    机器。接下来的三个字节就是“7cac81”,这三个字节是所在主机的唯一标识符,一般是机器主机名的散列值,这样就确保了不同主机生成不同的机器hash值,确保在分布式中不造成冲突,这也就是在同一台机器生成的objectId中间的字符串都是一模一样的原因。

        3)PID

        进程ID。上面的Machine是为了确保在不同机器产生的objectId不冲突,而pid就是为了在同一台机器不同的mongodb进程产生了objectId不冲突,接下来的“af71”两位就是产生objectId的进程标识符。

        4)INC

        自增计数器。前面的九个字节是保证了一秒内不同机器不同进程生成objectId不冲突,这后面的三个字节“36236b”是一个自动增加的计数器,用来确保在同一秒内产生的objectId也不会发现冲突,允许256的3次方等于16777216条记录的唯一性。

        总的来看,objectId的前4个字节时间戳,记录了文档创建的时间;接下来3个字节代表了所在主机的唯一标识符,确定了不同主机间产生不同的objectId;后2个字节的进程id,决定了在同一台机器下,不同mongodb进程产生不同的objectId;最后通过3个字节的自增计数器,确保同一秒内产生objectId的唯一性。ObjectId的这个主键生成策略,很好地解决了在分布式环境下高并发情况主键唯一性问题,值得学习借鉴。

  • 相关阅读:
    JDK1.8源码(四)——java.util.Arrays类
    JDK1.8源码(三)——java.lang.String类
    Java基础(六)——集合
    设计模式(二)——单例模式
    当你忘了数据库的设计语句,看这一篇就能拾起记忆
    MyBatis之配置优化
    Mybatis中的一些配置,直接拿来就可以用
    网络编程---ip&端口
    java基础数据类型小知识
    学习了MarkDown文本工具
  • 原文地址:https://www.cnblogs.com/tomcatx/p/4245697.html
Copyright © 2020-2023  润新知