本想上周末写好这篇总结的,无奈一起开黑的时间总是过得很快,就偷了个懒,并且还学了个新菜:西红柿牛腩.jpg。所以就把总结的事情放到今天来了。 初次接触mongodb还是在研一刚入学的时候,那时候接触的东西少,很多概念都不清楚,到现在差不多已经有三年了,所以总结一下我用mongodb做了哪些事情还是比较有意义的。

我的使用场景

关于mongodb的介绍,我在这里就不赘述了。mongodb在我这里主要的用途有以下几点: 1. 海量瓦片数据存储管理 2. 矢量数据存储管理 3. 一般性json数据存储管理

下面会分别根据使用场景总结在使用过程中使用到的mongodb特性。

海量瓦片数据存储管理

这里说的海量瓦片数据,可以把它当作是大量小文件。具体的应用可以看服务器端缓存环境配置Mongodb副本集在nginx-gridfs中的使用相关的几篇博客。通过nginx+nginx-gridfs的简单配置和非常简单的lua逻辑控制脚本,就可以直接根据请求url来获取mongodb gridfs中对应的文件。 由于那时候才刚接触这些东西,有很多现在看来比较简单的问题,都绕了不少弯子,比如说:不知道mongodb如何备份数据(所以更别提还原备份了),每次迁移数据库都是直接拷贝db文件夹(不同数据库版本和不同的操作系统经常会导致失败);不知道数据库索引是什么用,在数据量较大的时候经常查询超时;不懂操作系统,在服务器上必须有图形界面才可以操作… 在用惯了这套组合之后,后边还在nginx和mongodb中间加了redis作为缓存,以提升数据索引效率、在一个全景地图项目中,使用gridfs来管理大量的全景图片、现在工作的项目中,也尝试使用mongodb来管理瓦片数据。

矢量数据存储和管理

这里说的矢量数据指的是ESRI公司开发的shapefile。在之前的使用过程中,使用mongodb管理矢量数据,主要是想要管理矢量数据中包含的空间和属性信息,所以将shapefile转为geojson之后再由mongodb管理是一个不错的选择。通常可以使用gdal来完成shapefile–>geojson这一步的转换工作。完成数据转换之后,mongodb可以很方便的存储和组织geojson数据。在进行空间数据的索引的时候,mongodb的2d和2dsphere索引提供的一系列操作能够完成很多任务: 2dsphere索引仅支持球面查询,2d索引支持平面查询和部分球面查询。这两个索引支持的操作主要有以下这些:

  1. $near 定义:在一次空间查询中,指定一个点,按照离指定点距离由近到远的顺序返回查询结果。这个指定点可以是一个geopoint或者是一个传统的坐标点。 在使用$near进行查询的时候,根据指定点类型的不同需要不同的空间索引,如果指定点是一个geopoint则需要查询的字段建立2dsphere索引,如果是一个传统的坐标点,则需要建立2d索引。 进行$near查询的时候,使用语法如下:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    {
       <location field>: {
         $near: {
           $geometry: {
              type: "Point" ,
              coordinates: [ <longitude> , <latitude> ]
           },
           $maxDistance: <distance in meters>,
           $minDistance: <distance in meters>
         }
       }
    }
    

    ⚠️ 这里需要注意的是,如果指定点是geopoint,可以在查询的时候指定minDistance;如果指定点是传统坐标点的时候,minDistance不可用,并且maxDistance以弧度为单位。 在分了片的数据库中,$near是不可用的,可以使用geoNear命令或者$geoNear进行聚合操作。

  2. $nearSphere 此方法和$near功能一致。
  3. $geoWithin 定义:将处于指定区域内的所有元素返回。指定区域可以是geojson格式的Polygon或者MultiPolygon或者是传统坐标对序列。 使用语法:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    {
       <location field>: {
          $geoWithin: {
             $geometry: {
                type: <"Polygon" or "MultiPolygon"> ,
                coordinates: [ <coordinates> ]
             }
          }
       }
    }或者是
    {
       <location field>: {
          $geoWithin: { <shape operator>: <coordinates> }
       }
    }
    

    根据指定区域构造的方式不同,使用不同的方法,如果指定区域是geojson格式的就用$geometry操作符,如果指定区域是传统坐标对,可用的操作符就有:$box$polygon$center$centerSphere。 ⚠️ $geoWithin不需要专门创建空间所以,但是有空间索引可以提高检索效率,并且2dsphere2d索引均支持$geoWithin。 $geoWithin返回的结果是无序的,所以它的查询通常要比$near或者是$geoNear快。 如果指定区域过大(超过半球面)的时候,需要指定mongodb的空间参考:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    db.places.find(
       {
         loc: {
           $geoWithin: {
              $geometry: {
                 type : "Polygon" ,
                 coordinates: [
                   [
                     [ -100, 60 ], [ -100, 0 ], [ -100, -60 ], [ 100, -60 ], [ 100, 60 ], [ -100, 60 ]
                   ]
                 ],
                 crs: {
                    type: "name",
                    properties: { name: "urn:x-mongodb:crs:strictwinding:EPSG:4326" }
                 }
              }
           }
         }
       }
    )
    

    在使用$box操作符的时候:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    {
      <location field>: {
         $geoWithin: {
            $box: [
              [ <bottom left coordinates> ],
              [ <upper right coordinates> ]
            ]
         }
      }
    }
    

    在使用$polygon操作符的时候:

    1
    2
    3
    4
    5
    6
    7
    
    {
       <location field>: {
          $geoWithin: {
             $polygon: [ [ <x1> , <y1> ], [ <x2> , <y2> ], [ <x3> , <y3> ], ... ]
          }
       }
    }
    

    在使用$center的时候:

    1
    2
    3
    4
    5
    
    {
       <location field>: {
          $geoWithin: { $center: [ [ <x>, <y> ] , <radius> ] }
       }
    }
    

    在使用$centerSphere的时候:

    1
    2
    3
    4
    5
    
    {
       <location field>: {
          $geoWithin: { $centerSphere: [ [ <x>, <y> ], <radius> ] }
       }
    }
    
  4. $geoIntersects 定义:返回和指定区域相交的元素,指定区域通常是一个geojson格式的Polygon。 使用语法:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    {
      <location field>: {
         $geoIntersects: {
            $geometry: {
               type: "<GeoJSON object type>" ,
               coordinates: [ <coordinates> ]
            }
         }
      }
    }
    

    ⚠️ 和$geoWithin一样,$geoIntersects不需要专门创建空间索引,但是空间索引能够提升检索效率,但是它仅支持2dsphere索引。 在进行超过半球面的检索时,同样需要指定空间参考:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    db.places.find(
       {
         loc: {
           $geoIntersects: {
              $geometry: {
                 type : "Polygon",
                 coordinates: [
                   [
                     [ -100, 60 ], [ -100, 0 ], [ -100, -60 ], [ 100, -60 ], [ 100, 60 ], [ -100, 60 ]
                   ]
                 ],
                 crs: {
                    type: "name",
                    properties: { name: "urn:x-mongodb:crs:strictwinding:EPSG:4326" }
                 }
              }
           }
         }
       }
    )
    

通过上述的几个方法,能过满足空间数据的常规索引需求。 上述两种场景主要是为了应对和空间信息相关的数据,在实际的使用中,更多的是普通的json格式数据。

一般性json格式数据存储管理

在更多的时候,使用mongodb主要是用来给web服务提供数据。现在前后端分离的开发方式,前后端之间的交互转变为数据的交互,我的套路就是vue+flask-restful+mongodb。 由于mongodb对json格式的数据存储有先天的优势,所以在写代码的时候会剩很多事,合理的使用mongodb的一些特性会有更大的便利。

aggregation

之前记录过mongodb的aggreagtion相关的内容:Mongodb Aggregation Piplelinemongodb最近频繁看的文档。 由于mongodb对跨表查询支持的不是很好,所以在数据存储的时候经常会尽可能的将所需要的数据都放在同一个document里边。但是在实际进行数据请求的时候,可能需要对获取到的数据进行对应的处理,这个时候mongodb的aggregation就能起到作用。直接利用数据库的能力完成数据处理,通常会比拿到数据之后再通过自己的代码完成数据处理要好得多。

ttl索引和explain()

这两个点在之前的一个博客里边提到过:读Mongodb权威指南有感。 ttl索引可用的面还是挺广的,用以记录缓存过期、验证码或者各类时效性的数据。 explain()方法在进行数据库性能调优的过程中也会有很好的效果。

数据备份和还原

整库的还原和备份,可以直接通过mongodb自带的工具mongodumpmongorestore来完成: 通过mongodump -d database_name -o /path/to/dump_dir --gzip来完成备份,mongorestore -d database_name --dir /path/to/dump_dir --gzip来完成还原。 当然,如果两个数据库实例在同一个局域网内,也可以直接在mongo中执行命令来完成database或者是collection的迁移: 使用cloneCollection命令或者db.cloneCollection()来完成collection的数据迁移:

1
2
3
4
5
6
7
db.runCommand( { cloneCollection: "users.profiles",
                 from: "mongodb.example.net:27017",
                 query: { 'active' : true }
               } )

db.cloneCollection('mongodb.example.net:27017', 'users.profile',
                    { 'active' : true } )

⚠️ mongos不支持上述两种方法,并且如果目标服务器的mongodb实例开启authorization这两个方法也是不可以用的。 类似的,使用下边两个方法来完成整个库的拷贝。

1
2
3
4
use importdb
db.cloneDatabase("hostname")

{ clone: "db1.example.net:27017" }

pymongo的使用

使用flask-restful提供数据服务,用到mongodb的地方我都是直接使用pymongo来操作数据库。在使用的过程中有几点,可以记下:

  1. 在使用flask的jsonify的时候,不能直接把mongodb返回的一整条数据jsonify,这是因为ObjectId不能直接转为json,通常我在查询数据的时候不返回ObjectId
  2. 在进行排序或者建立索引的时候,会用到ASCENDINGDESCENDING,升序和降序。需要from pymongo import DESCENDING, ASCENDING,当然还有其他一些索引类型,也都需要从pymongo中直接import
  3. 在数据量很大的时候,将整个数据集返回,然后逐条操作。整个过程可能会耗费较长的时间,这个时候需要设置no_cursor_timeout,就像这样:
    1
    2
    3
    4
    5
    
    def gen_playlists():
        playlists = col.find({}, {'_id': 0}, no_cursor_timeout=True)
        for playlist in playlists:
            yield playlist
        playlists.close()
    

    no_cursor_timeout默认设置为False,如果find()返回的结果集中的某个cursor在十分钟以内没有被操作,这个查询结果就会被关闭。在将no_cursor_timeout设置为True的时候,需要在数据操作完成之后手动关闭查询结果。

  4. 给mongodb设置用户名密码是保证数据安全的一个常用方法,这篇mongodb带密码验证使用博客里边有详细的说明。

未用到的特性

虽说是前前后后差不多已经用了三年的mongodb,但总体说来也就达到了会用的程度。mongodb的分片到现在也没有实际的应用经验,很多有用的索引类型也都没用过,并且也没有实实在在优化数据库性能的经验,所以还有很多要学的。大致从一下几点努力:

  1. 仔细阅读官方文档
  2. 在以后的小项目中尽量使用更多的数据库新特性
  3. 除了pymongo以外,其他的driver也接触使用
  4. 尝试学习mongodb内部实现细节,比如geohash算法的实现