IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> 分布式文档存储MongoDB -> 正文阅读

[大数据]分布式文档存储MongoDB

MongoDB体系结构

? MongoDB是一款高性能的NoSQL(Not Only SQL 不仅仅SQL)数据库

NoSQL 和 MongoDB
  • NoSQL=Not Only SQL,支持类似SQL的功能, 与Relational Database相辅相成。其性能较高, 不使用SQL意味着没有结构化的存储要求(SQL为结构化的查询语句),没有约束之后架构更加灵 活
  • NoSQL数据库四大家族 列存储 Hbase,键值(Key-Value)存储 Redis,图像存储 Neo4j,文档存储 MongoDB
  • MongoDB 是一个基于分布式文件存储的数据库,由 C++ 编写,可以为 WEB 应用提供可扩展、 高性能、易部署的数据存储解决方案
  • MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库中功能最丰富、 最像关系数据库的。在高负载的情况下,通过添加更多的节点,可以保证服务器性能
MongoDB体系结构

在这里插入图片描述

MongoDB 和RDBMS(关系型数据库)对比
RDBMSMongoDB
database(数据库)database(数据库)
table(表)collection(集合)
row(行)document(BSON文档)
column(列)field(字段)
index(唯一索引、主键索引)index(支持地理位置索引、全文索引 、哈希索引)
join(主外键关联)embedded Document (嵌套文档)
primary key(指定1至N个列做主键)primary key (指定_id field做为主键)
什么是BSO
  • BSON是一种类json的一种二进制形式的存储格式,简称Binary JSON,它和JSON一样,支持内嵌的文 档对象和数组对象,但是BSON有JSON没有的一些数据类型,如Date和Binary Data类型。BSON可以 做为网络数据交换的一种存储形式,是一种schema-less的存储形式,它的优点是灵活性高,但它的缺点 是空间利用率不是很理想
  • {key:value,key2:value2} 这是一个BSON的例子,其中key是字符串类型,后面的value值,它的类型一般 是字符串,double,Array,ISODate等类型
  • BSON有三个特点:轻量性、可遍历性、高效性
BSON在MongoDB中的使用
  • MongoDB使用了BSON这种结构来存储数据和网络数据交换。把这种格式转化成一文档这个概念 (Document),这里的一个Document也可以理解成关系数据库中的一条记录(Record),只是这里的 Document的变化更丰富一些,如Document可以嵌套

  • MongoDB中Document 中 可以出现的数据类型

    数据类型说明解释说明Document举例
    String字符串UTF-8 编码的字符串才是 合法的{key:“cba”}
    Integer整型数值根据你所采用的服务器, 可分为 32 位或 64 位{key:1}
    Boolean布尔值用于存储布尔值(真/ 假){key:true}
    Double双精度浮点值用于存储浮点值{key:3.14}
    ObjectId对象ID用于创建文档的ID{_id:new ObjectId()}
    Array数组用于将数组或列表或多个 值存储为一个键{arr:[“a”,“b”]}
    Timestamp时间戳从开始纪元开始的毫秒数{ ts: new Timestamp() }
    Object内嵌文档文档可以作为文档中某个 key的value{o:{foo:“bar”}}
    Null空值表示空值或者未定义的对 象{key:null}
    Date或者 ISODate格林尼治时间日期时间,用Unix日期格 式来存储当前日期或时 间{birth:new Date()}
    Code代码可以包含JS代码{x:function(){}}
    File文件1、二进制转码(Base64)后 存储 (<16M)
    2、 GridFS(>16M)
    GridFS 用两个集合来存储一个文件:
    fs.files与 fs.chunks 真正存储需要使用
    mongofiles -d gridfs put song.mp3
MongoDB在Linux的安装
  • 物理机安装

    • 下载社区版 MongoDB 4.4.12,去官网下载对应的MongoDB 然后上传到Linux服务器
    • 下载地址 https://www.mongodb.com/try/download/community
    • 将压缩包解压即可
    tar -zxvf mongodb-linux-x86_64-rhel70-4.4.12.tgz
    #启动
    ./bin/mongod
    #指定配置文件方式启动
    ./bin/mongod -f mongo.conf
    

    mongo.conf 配置文件样例

    dbpath=/data/mongo/
    port=27017
    bind_ip=0.0.0.0
    fork=true
    logpath = /data/mongo/MongoDB.log
    logappend = true
    auth=false
    
  • Docker容器安装

    docker pull mongo:latest
    docker run -itd --name mongo -p 27017:27017 mongo --auth
    #参数说明
    #-p 27017:27017 :映射容器服务的 27017 端口到宿主机的 27017 端口。外部可以直接通过 宿主机 ip:27017 访问到 mongo 的服务
    #--auth:需要密码才能访问容器服务
    
  • MongoDB启动和参数说明

    参数            说明
    dbpath     数据库目录,默认/data/db
    port       监听的端口,默认27017
    bind_ip    监听IP地址,默认全部可以访问
    fork       是否已后台启动的方式登陆
    logpath    日志路径
    logappend  是否追加日志
    auth       是开启用户密码登陆
    config     指定配置文件
    
    
  • mongo shell 的启动

    #启动mongo shell
    ./bin/mongo
    #指定主机和端口的方式启动
    ./bin/mongo --host=主机IP --port=端口
    
    
  • GUI可视化工具下载

    • NoSQLBooster是MongoDB CLI界面中非常流行的GUI工具。它正式名称为MongoBooster。
    • 下载地址: https://www.mongobooster.com/downloads

MongoDB命令

  • 官方文档地址
    • https://docs.mongodb.com/v4.2/core/map-reduce/
基本操作命令
#查看数据库
show dbs;
#切换数据库 如果没有对应的数据库则创建
use 数据库名;
#创建集合
db.createCollection("集合名");
#查看集合
show tables;
show collections;
#删除集合
db.集合名.drop();
#删除当前数据库
db.dropDatabase()
MongoDB集合数据操作
  • 数据添加

    • 插入单条数据

      • db.集合名.insert(文档)

        db.chat_room_msg.insert({"msg":"测试单个插入"})
        
    • 插入多条数据

      • db.集合名.insert([文档,文档])

        db.chat_room_msg.insert([{"msg":"测试多个个插入1"},{"msg":"测试多个个插入2"}])
        
    • _id 类型字段说明

    • _id 类型是ObjectId 类型是一个12字节 BSON 类型数据

    • 前4个字节表示时间戳 ObjectId(“对象Id字符串”).getTimestamp() 来获取

    • 接下来的3个字节是机器标识码

    • 紧接的两个字节由进程id组成(PID)

    • 最后三字节是自增计数器,确保相同进程同一秒中产生的ObjectId是唯一的

  • 数据查询

    • 比较条件查询

      • db.集合名.find(条件)
      操作条件格式例子RDBMS的条件
      等于{key:value}db.col.find({字段名:值}).pretty()where 字段名=值
      大于{key:{$gt:value}}db.col.find({字段名:{$gt:值}}).pretty()where 字段名>值
      小于{key:{$lt:value}}db.col.find({字段名:{$lt:值}}).pretty()where 字段名<值
      大于等 于{key: {$gte:value}}db.col.find({字段名:{$gte: 值}}).pretty()where 字段名>= 值
      小于等 于{key:{$lte:value}}db.col.find({字段名:{$lte:值}}).pretty()where 字段名<= 值
      不等于{key:{$ne:value}}db.col.find({字段名:{$ne: 值}}).pretty()where 字段名!=值
    • 逻辑条件查询

      and 条件
      MongoDB 的 find() 方法可以传入多个键(key),每个键(key)以逗号隔开,即常规 SQL 的 AND 条件
          db.集合名.find({key1:value1, key2:value2}).pretty()
      or 条件
          db.集合名.find({$or:[{key1:value1}, {key2:value2}]}).pretty()
      not 条件
         db.集合名.find({key:{$not:{$操作符:value}}).pretty()
      
    • 分页查询

      • db.集合名.find({条件}).sort({排序字段:排序方式})).skip(跳过的行数).limit(一页显示多少数据)
数据更新
$set :设置字段值
$unset :删除指定字段
$inc:对修改的值进行自增
db.集合名.update(
	<query>,
	<update>,
	{
		upsert: <boolean>,
		multi: <boolean>,
		writeConcern: <document>
	}
)
参数说明:
query : update的查询条件,类似sql update查询内where后面的。
update : update的对象和一些更新的操作符(如$set,$inc...)等,也可以理解为sql update中
set后面的
upsert : 可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认
是false,不插入。
multi : 可选,MongoDB 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查
出来多条记录全部更新。
writeConcern :可选,用来指定mongod对写操作的回执行为比如写的行为是否需要确认。
举例:
db.集合名.update({条件},{$set:{字段名:值}},{multi:true})
db.chat_room_msg.update({"userId":12}, 
    {$set:{"msg":"测试修改"}} 
)

writeConcern 包括以下字段:
{ w: <value>, j: <boolean>, wtimeout: <number> }
w:指定写操作传播到的成员数量
比如:
w=1(默认):则要求得到写操作已经传播到独立的Mongod实例或副本集的primary成员的确认
w=0:则不要求确认写操作,可能会返回socket exceptions和 networking errors
w="majority":要求得到写操作已经传播到大多数具有存储数据具有投票的(data-bearing voting
)成员(也就是 members[n].votes 值大于0的成员)的确认
j:要求得到Mongodb的写操作已经写到硬盘日志的确认
比如:
j=true:要求得到Mongodb(w指定的实例个数)的写操作已经写到硬盘日志的确认。j=true本身并不保证
因为副本集故障而不会回滚。
wtimeout:指定write concern的时间限制,只适用于w>1的情况
wtimeout在超过指定时间后写操作会返回error,即使写操作最后执行成功,当这些写操作返回时,
MongoDB不会撤消在wtimeout时间限制之前执行成功的数据修改。
如果未指定wtimeout选项且未指定write concern级别,则写入操作将无限期阻止。 指定wtimeout值
为0等同于没有wtimeout选项。
数据删除
db.collection.remove(
	<query>,
	{
		justOne: <boolean>,
		writeConcern: <document>
	+}
)
参数说明:
query :(可选)删除的文档的条件。
justOne : (可选)如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值
false,则删除所有匹配条件的文档。
writeConcern :(可选)用来指定mongod对写操作的回执行为。
MongoDB聚合操作

聚合是MongoDB的高级查询语言,它允许我们通过转化合并由多个文档的数据来生成新的在单个文档 里不存在的文档信息。一般都是将记录按条件分组之后进行一系列求最大值,最小值,平均值的简单操 作,也可以对记录进行复杂数据统计,数据挖掘的操作。聚合操作的输入是集中的文档,输出可以是一 个文档也可以是多个文档。

  • MongoDB 聚合操作分类

    • 单目的聚合操作(Single Purpose Aggregation Operation)

      • 单目的聚合命令常用的有:count() 和 distinct()

        db.chat_room_msg.find({}).count()
        
    • 聚合管道(Aggregation Pipeline)

      db.COLLECTION_NAME.aggregate(AGGREGATE_OPERATION)
      如:
      db.preview.aggregate({$group:{_id:"$city",city_count:{$sum:1}}})
      
      /** 按照city 进行分组 统计每个city 中expectSalary的平均值  */
      db.preview.aggregate([{$group: { _id: "$city",avg_sal:{$avg:"$expectSalary"}}}])
      
      /** 按照city 进行分组 统计每个city的值放入一个数组中  */
      db.preview.aggregate([{$group: { _id: "$city",city_name:{$push:"$city"}}}])
      db.preview.aggregate([{$group: { _id: "$city",city_name:{$addToSet:"$city"}}}])
      
      

      MongoDB中聚合(aggregate)主要用于统计数据(诸如统计平均值,求和等),并返回计算后的数据结果。 表达式:处理输入文档并输出。表达式只能用于计算当前聚合管道的文档,不能处理其它的文档

      表达式描述
      $sum计算总和
      $avg计算平均值
      $min获取集合中所有文档对应值得最小值
      $max获取集合中所有文档对应值得最大值
      $push在结果文档中插入值到一个数组中
      $addToSet在结果文档中插入值到一个数组中,但数据不重复
      $first根据资源文档的排序获取第一个文档数据
      $last根据资源文档的排序获取最后一个文档数据
      • MongoDB 中使用 db.COLLECTION_NAME.aggregate([{},…]) 方法来构建和使用聚合管道,每个 文档通过一个由一个或者多个阶段(stage)组成的管道,经过一系列的处理,输出相应的结果。

      • MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作 是可以重复的。

      • 聚合中常用的几个操作

        • $group:将集合中的文档分组,可用于统计结果
        • $project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及 嵌套文档
        • m a t c h : 用 于 过 滤 数 据 , 只 输 出 符 合 条 件 的 文 档 。 match:用于过滤数据,只输出符合条件的文档。 matchmatch使用MongoDB的标准查询操作
        • $limit:用来限制MongoDB聚合管道返回的文档数
        • $skip:在聚合管道中跳过指定数量的文档,并返回余下的文档
        • $sort:将输入文档排序后输出
        • $geoNear:输出接近某一地理位置的有序文档
        /***
        *第一阶段:该$match阶段按字段过滤文档并将具有等于status的文档传递到下一阶段。status"A"
        *第二阶段:该$group阶段按字段对文档进行分组,cust_id以计算每个唯一值的总和cust_id。
        */
        db.orders.aggregate([
           { $match: { status: "A" } },
           { $group: { _id: "$cust_id", total: { $sum: "$amount" } } }
        ])
        
        db.preview.aggregate(
        [{$group : {_id: "$city", avgSal:{$avg:"$expectSalary"}}},
        {$project : {city: "$city", salary : "$avgSal"}}
        ])
        
        
        db.preview.aggregate(
        [{$group:{_id: "$city",count:{$sum : 1}}},
        {$match:{count:{$gt:1}}}
        ])
        
        
    • MapReduce 编程模型

      在这里插入图片描述

      • Pipeline查询速度快于MapReduce,但是MapReduce的强大之处在于能够在多台Server上并行执行复 杂的聚合逻辑。MongoDB不允许Pipeline的单个聚合操作占用过多的系统内存,如果一个聚合操作消 耗20%以上的内存,那么MongoDB直接停止操作,并向客户端输出错误消息

      • MapReduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结 果合并成最终结果(REDUCE)

        db.collection.mapReduce(
        	function() {emit(key,value);}, //map 函数
        	function(key,values) {return reduceFunction}, //reduce 函数
        	{
        		out: collection,
        		query: document,
        		sort: document,
        		limit: number,
        		finalize: <function>,
        		verbose: <boolean>
        	}
        )
        
      • 使用 MapReduce 要实现两个函数 Map 函数和 Reduce 函数,Map 函数调用 emit(key, value), 遍历 collection 中所有的记录, 将 key 与 value 传递给 Reduce 函数进行处理。

      • 参数说明

        • map:是JavaScript 函数,负责将每一个输入文档转换为零或多个文档,生成键值对序列,作为 reduce 函数参数
        • reduce:是JavaScript 函数,对map操作的输出做合并的化简的操作(将key-value变成keyvalues,也就是把values数组变成一个单一的值value)
        • out:统计结果存放集合
        • query: 一个筛选条件,只有满足条件的文档才会调用map函数
        • sort: 和limit结合的sort排序参数(也是在发往map函数前给文档排序),可以优化分组机制
        • limit: 发往map函数的文档数量的上限(要是没有limit,单独使用sort的用处不大)
        • finalize:可以对reduce输出结果再一次修改
        • verbose:是否包括结果信息中的时间信息,默认为fasle
        db.preview.mapReduce(
        	function() { emit(this.city,this.expectSalary); },
        	function(key, value) {return Array.avg(value)},
        	{
        		query:{expectSalary:{$gt: 15000}},
        		out:"cityAvgSal"
        	}
        )
        
        

MongoDB索引

什么是索引
  • 索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表 中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。索引的作 用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。索引目标是提高数据库的查 询效率,没有索引的话,查询会进行全表扫描(scan every document in a collection),数据量 大时严重降低了查询效率。默认情况下Mongo在一个集合(collection)创建时,自动地对集合 的_id创建了唯一索引

    在这里插入图片描述

索引类型
  • 单键索引 (Single Field)

    • MongoDB支持所有数据类型中的单个字段索引,并且可以在文档的任何字段上定义。 对于单个字段索引,索引键的排序顺序无关紧要,因为MongoDB可以在任一方向读取索引

    • 示例

      #排序方式: 1 为指定按升序创建索引,如果你想按降序来创建索引指定为 -1 即可
      db.集合名.createIndex({"字段名":排序方式}) 
      
    • 特殊的单键索引 过期索引 TTL (Time To Live)

      • TTL索引是MongoDB中一种特殊的索引, 可以支持文档在一定时间之后自动过期删除,目前TTL索引 只能在单字段上建立,并且字段类型必须是日期类型

        db.集合名.createIndex({"日期字段":排序方式}, {expireAfterSeconds: 秒数})
        
      • MongoDB过期索引删除时间不是精确的
          说明:删除过程是由后台程序每60s跑一次,而且删除也需要一些时间,索引存在误差

  • 复合索引(Compound Index)

    • 通常我们需要在多个字段的基础上搜索表/集合,这是非常频繁的。 如果是这种情况,我们可能会考虑 在MongoDB中制作复合索引。 复合索引支持基于多个字段的索引,这扩展了索引的概念并将它们扩展 到索引中的更大域

    • 制作复合索引时要注意的重要事项包括:字段顺序与索引方向

      db.集合名.createIndex( { "字段名1" : 排序方式, "字段名2" : 排序方式 } )
      
  • 多键索引(Multikey indexes)

    • 针对属性包含数组数据的情况,MongoDB支持针对数组中每一个element创建索引,Multikey indexes支持strings,numbers和nested documents

      在这里插入图片描述

  • 地理空间索引(Geospatial Index)

    • 针对地理空间坐标数据创建索引

    • 2dsphere索引,用于存储和查找球面上的点

    • 2d索引,用于存储和查找平面上的点

      db.company.insert(
         {
           loc : { type: "Point", coordinates: [ 116.482451, 39.914176 ] },
           name: "地铁",
           category : "Parks"
         }
      )
      
      db.company.insert(
         {
           loc : { type: "Point", coordinates: [ 116.492451, 39.934176 ] },
           name: "test1",
           category : "Parks"
         }
      )
      
      db.company.insert(
         {
           loc : { type: "Point", coordinates: [ 116.462451, 39.954176 ] },
           name: "test2",
           category : "Parks"
         }
      )
      
      
      db.company.insert(
         {
           loc : { type: "Point", coordinates: [ 116.562451, 38.954176 ] },
           name: "test3",
           category : "Parks"
         }
      )
      
      db.company.insert(
         {
           loc : { type: "Point", coordinates: [ 117.562451, 37.954176 ] },
           name: "test4",
           category : "Parks"
         }
      )
      
      #参数不是1或-1,为2dsphere 或者 2d。还可以建立组合索引。
      db.company.ensureIndex( { loc : "2dsphere" } )
      db.company.getIndexes()
      db.company.dropIndexes()
      
      db.company.find({
          "loc" : { 
              "$geoWithin" : {
                "$center":[[116.482451,39.914176],0.05]
              }
          }
      })
      /** 计算中心点最近的三个点 */
      db.company.aggregate([
         {
           $geoNear: {
              near: { type: "Point", coordinates: [116.482451,39.914176 ] },
              key: "loc",
              distanceField: "dist.calculated"
           }
         },
         { $limit: 3 }
      ])
      
      /** 2d 测试  */
      db.places.drop()
      db.places.insert({"name": "Temple1","tile": [32, 22]})
      db.places.insert({"name": "Temple2","tile": [30, 22]})
      db.places.insert({"name": "Temple3","tile": [28, 21]})
      db.places.insert({"name": "Temple4","tile": [34, 27]})
      db.places.insert({"name": "Temple5","tile": [34, 26]})
      db.places.insert({"name": "Temple6","tile": [39, 28]})
      
      db.places.find({})
       
      db.places.ensureIndex({"tile" : "2d"}, {"min" : -90, "max" : 90, "bits" : 20}) 
       
      db.places.find({"tile": {"$within": {"$box": [[0, 0], [30, 30]]}}})
      
      • $geoWithin运算符查询在 GeoJSON 多边形中找到的位置数据
      • $geoIntersects运算符查询与指定 GeoJSON 对象相交的位置。如果相交不为空,则位置与对象相交。这包括具有共享边缘的文档
      • $center(定义一个圆圈)
      • $geoNear管道运营商利用地理空间索引
  • 全文索引

    • MongoDB提供了针对string内容的文本查询,Text Index支持任意属性值为string或string数组元素的 索引查询。注意:一个集合仅支持最多一个Text Index,中文分词不理想 推荐ES

      db.集合.createIndex({"字段": "text"})
      db.集合.find({"$text": {"$search": "coffee"}})
      
  • 哈希索引 Hashed Index

    • 针对属性的哈希值进行索引查询,当要使用Hashed index时,MongoDB能够自动的计算hash值,无 需程序计算hash值。注:hash index仅支持等于查询,不支持范围查询

      db.集合.createIndex({"字段": "hashed"})
      
索引和explain 分析
  • 索引管理

    • 创建索引并在后台运行

      db.COLLECTION_NAME.createIndex({"字段":排序方式}, {background: true});
      
    • 获取针对某个集合的索引

      db.COLLECTION_NAME.getIndexes()
      
    • 索引的大小

      db.COLLECTION_NAME.totalIndexSize()
      
    • 索引的重建

      db.COLLECTION_NAME.reIndex()
      
    • 索引的删除

      db.COLLECTION_NAME.dropIndex("INDEX-NAME")
      db.COLLECTION_NAME.dropIndexes()
      注意: _id 对应的索引是删除不了的
      
  • explain 分析

    • 使用js循环 插入100万条数据 不使用索引字段 查询查看执行计划 ,然后给某个字段建立索引,使用索引 字段作为查询条件 再查看执行计划进行分析

    • explain()也接收不同的参数,通过设置不同参数我们可以查看更详细的查询计划

      • queryPlanner:queryPlanner是默认参数,具体执行计划信息参考下面的表格

      • executionStats:executionStats会返回执行计划的一些统计信息(有些版本中和 allPlansExecution等同)

      • allPlansExecution:allPlansExecution用来获取所有执行计划,结果参数基本与上文相同

        #示例
        db.resume.find({name:"test1"}).explain("executionStats")
        
    • queryPlanner 默认参数

      参数含义
      plannerVersion查询计划版本
      namespace要查询的集合(该值返回的是该query所查询的表)数据库.集合
      indexFilterSet针对该query是否有indexFilter
      parsedQuery查询条件
      winningPlan被选中的执行计划
      winningPlan.stage被选中执行计划的stage(查询方式),常见的有:COLLSCAN/全表 扫描:
      (应该知道就是CollectionScan,就是所谓的“集合扫描”, 和mysql中table scan/heap scan类似,
      这个就是所谓的性能最烂 最无奈的由来)、IXSCAN/索引扫描:
      (是IndexScan,这就说明 我们已经命中索引了)、FETCH/根据索引去检索文档、
      SHARD_MERGE/合并分片结果、IDHACK/针对_id进行查询等
      winningPlan.inputStage用来描述子stage,并且为其父stage提供文档和索引关键字
      winningPlan.stage的child stage如果此处是IXSCAN,表示进行的是index scanning
      winningPlan.keyPattern所扫描的index内容
      winningPlan.indexNamewinning plan所选用的index
      winningPlan.isMultiKey是否是Multikey,此处返回是false,如果索引建立在array上,此 处将是true
      winningPlan.direction此query的查询顺序,此处是forward,如果用了.sort({字段:-1}) 将显示backward
      filter过滤条件
      winningPlan.indexBoundswinningplan所扫描的索引范围,如果没有制定范围就是[MaxKey, MinKey],
      这主要是直接定位到mongodb的chunck中去查找数 据,加快数据读取
      rejectedPlans被拒绝的执行计划的详细返回,其中具体信息与winningPlan的返 回中意义相同,故不在此赘述)
      serverInfoMongoDB服务器信息
    • executionStats参数

      参数含义
      executionSuccess是否执行成功
      nReturned返回的文档数
      executionTimeMillis执行耗时
      totalKeysExamined索引扫描次数
      totalDocsExamined文档扫描次数
      executionStages这个分类下描述执行的状态
      stage扫描方式,具体可选值与上文的相同
      nReturned查询结果数量
      executionTimeMillisEstimate检索document获得数据的时间
      inputStage.executionTimeMillisEstimate该查询扫描文档 index所用时间
      works工作单元数,一个查询会分解成小的工作单元
      advanced优先返回的结果数
      docsExamined文档检查数目,与totalDocsExamined一致。检查了总共的document 个数,
      而从返回上面的nReturned数量
    • executionStats返回逐层分析

      • 第一层,executionTimeMillis最为直观explain返回值是executionTimeMillis值,指的是这条语句的执 行时间,这个值当然是希望越少越好,其中有3个executionTimeMillis

        • executionStats.executionTimeMillis 该query的整体查询时间
        • executionStats.executionStages.executionTimeMillisEstimate 该查询检索document获得数据的时 间
        • executionStats.executionStages.inputStage.executionTimeMillisEstimate 该查询扫描文档 index 所用时间
      • 第二层,index与document扫描数与查询返回条目数 这个主要讨论3个返回项 nReturned、 totalKeysExamined、totalDocsExamined,分别代表该条查询返回的条目、索引扫描条目、文档扫描 条目。 这些都是直观地影响到executionTimeMillis,我们需要扫描的越少速度越快。 对于一个查询, 我们最理想的状态是:nReturned=totalKeysExamined=totalDocsExamined

      • 第三层,stage状态分析 那么又是什么影响到了totalKeysExamined和totalDocsExamined?是stage的 类型

        • COLLSCAN:全表扫描
        • IXSCAN:索引扫描
        • FETCH:根据索引去检索指定document
        • SHARD_MERGE:将各个分片返回数据进行merge
        • SORT:表明在内存中进行了排序
        • LIMIT:使用limit限制返回数
        • SKIP:使用skip进行跳过
        • IDHACK:针对_id进行查询
        • SHARDING_FILTER:通过mongos对分片数据进行查询
        • COUNT:利用db.coll.explain().count()之类进行count运算
        • TEXT:使用全文索引进行查询时候的stage返回
        • PROJECTION:限定返回字段时候stage的返回
        • 对于普通查询,我希望看到stage的组合(查询的时候尽可能用上索引):
          • Fetch+IDHACK Fetch+IXSCAN
          • Limit+(Fetch+IXSCAN)
          • PROJECTION+IXSCAN
          • SHARDING_FITER+IXSCAN
        • 不希望看到包含如下的stage:
          • COLLSCAN(全表扫描)
          • SORT(使用sort但是无index)
          • COUNT 不使用index进行count)
      • allPlansExecution参数

        queryPlanner 参数和executionStats的拼接
        
慢查询分析
  • 开启内置的查询分析器,记录读写操作效率

    • db.setProfilingLevel(n,m),n的取值可选0,1,2,默认是不记录,为0

    • 0表示不记录

    • 1表示记录慢速操作,如果值为1,m必须赋值单位为ms,用于定义慢速查询时间的阈值

    • 2表示记录所有的读写操作

    • 查询是否记录慢查询操作

      db.getProfilingLevel()
      
    • 查询监控结果

      • db.system.profile.find().sort({millis:-1}).limit(3)
    • 分析慢速查询

      • 应用程序设计不合理、不正确的数据模型、硬件配置问题,缺少索引等
    • 解读explain结果 确定是否缺少索引

MongoDB 索引底层实现原理
  • MongoDB 是文档型的数据库,它使用BSON 格式保存数据,比关系型数据库存储更方便。比如之前关 系型数据库中处理用户、订单等数据要建立对应的表,还要建立它们之间的关联关系。但是BSON就不 一样了,我们可以把一条数据和这条数据对应的数据都存入一个BSON对象中,这种形式更简单,通俗易 懂。MySql是关系型数据库,数据的关联性是非常强的,区间访问是常见的一种情况,底层索引组织数 据使用B+树,B+树由于数据全部存储在叶子节点,并且通过指针串在一起,这样就很容易的进行区间 遍历甚至全部遍历。MongoDB使用B-树,所有节点都有Data域,只要找到指定索引就可以进行访问, 单次查询从结构上来看要快于MySql

  • B-树是一种自平衡的搜索树,形式很简单

    在这里插入图片描述

    • B-树的特点:
      • 多路 非二叉树
      • 每个节点 既保存数据 又保存索引
      • 搜索时 相当于二分查找
  • B+树结构

    在这里插入图片描述

    • B+ 树的特点:
    • 多路非二叉
    • 只有叶子节点保存数据
    • 搜索时 也相当于二分查找
    • 增加了 相邻节点指针
    • 两者的主要区别:
      • 一个是数据的保存位置,一个是相邻节点的指向
      • (1)B+树相邻接点的指针可以大大增加区间访问性,可使用在范围查询等,而B-树每个节点 key 和 data 在一起 适合随机读写 ,而区间查找效率很差
      • (2)B+树更适合外部存储,也就是磁盘存储,使用B-结构的话,每次磁盘预读中的很多数据是用不上 的数据。因此,它没能利用好磁盘预读的提供的数据。由于节点内无 data 域,每个节点能索引的范围 更大更精确
      • 注意这个区别相当重要,是基于(1)(2)的,B-树每个节点即保存数据又保存索引 树的深度 小,所以磁盘IO的次数很少,B+树只有叶子节点保存,较B树而言深度大磁盘IO多,但是区间访问比较 好

MongoDB应用

MongoDB的适用场景
  • 网站数据:Mongo 非常适合实时的插入,更新与查询,并具备网站实时数据存储所需的复制及高 度伸缩性
  • 缓存:由于性能很高,Mongo 也适合作为信息基础设施的缓存层。在系统重启之后,由Mongo 搭建的持久化缓存层可以避免下层的数据源过载
  • 大尺寸、低价值的数据:使用传统的关系型数据库存储一些大尺寸低价值数据时会比较浪费, 在此之前,很多时候程序员往往会选择传统的文件进行存储
  • 高伸缩性的场景:Mongo 非常适合由数十或数百台服务器组成的数据库,Mongo 的路线图中 已经包含对MapReduce 引擎的内置支持以及集群高可用的解决方案
  • 用于对象及JSON 数据的存储:Mongo 的BSON 数据格式非常适合文档化格式的存储及查询
MongoDB的行业具体应用场景
  • 游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存 储,方便查询、更新
  • 物流场景,使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以 MongoDB 内 嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来
  • 社交场景,使用 MongoDB 存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引 实现附近的人、地点等功能
  • 物联网场景,使用 MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这 些信息进行多维度的分析
  • 直播,使用 MongoDB 存储用户信息、礼物信息等
Java 访问MongoDB
  • maven 依赖

    <dependency>
         <groupId>org.mongodb</groupId>
         <artifactId>mongo-java-driver</artifactId>
         <version>3.10.1</version>
    </dependency>
    
  • 文档添加

    import com.mongodb.MongoClient;
    import com.mongodb.MongoClientURI;
    import com.mongodb.client.MongoCollection;
    import com.mongodb.client.MongoDatabase;
    import org.bson.Document;
    
    /**
     * 文档插入
     */
    public class DocumentInsertTest {
    
        public static void main(String[] args) {
            MongoClientURI url = new MongoClientURI("mongodb://admin:123456@192.168.47.128:27017/obs");
            MongoClient mongoClient = new MongoClient(url);
            // 获取数据库对象
            MongoDatabase mongoDatabase = mongoClient.getDatabase("obs");
            // 获取集合对象
            MongoCollection<Document> collection = mongoDatabase.getCollection("preview");
            //  构建Document 对象  并插入到集合中
            Document  document  = Document.parse("{name:'lisi',city:'北京',birth_day:new ISODate('2001-08-01'),expectSalary:18000}");
            collection.insertOne(document);
            mongoClient.close();
        }
    }
    
  • 文档查询

    import com.mongodb.MongoClient;
    import com.mongodb.MongoClientURI;
    import com.mongodb.client.FindIterable;
    import com.mongodb.client.MongoCollection;
    import com.mongodb.client.MongoDatabase;
    import org.bson.Document;
    
    /**
     * 查询操作
     */
    public class DocumentFindTest {
    
        public static void main(String[] args) {
            MongoClientURI url = new MongoClientURI("mongodb://admin:123456@192.168.47.128:27017/obs");
            MongoClient mongoClient = new MongoClient(url);
            // 获取数据库对象
            MongoDatabase mongoDatabase = mongoClient.getDatabase("obs");
            // 获取集合对象
            MongoCollection<Document> collection = mongoDatabase.getCollection("preview");
            // 要根据expectSalary 降序排列
            Document  sortDocument  =  new Document();
            sortDocument.append("expectSalary",-1);
            FindIterable<Document> findIterable =  collection.find().sort(sortDocument);
            for(Document document : findIterable){
                System.out.println(document);
            }
            mongoClient.close();
        }
    }
    
  • 文档过滤查询

    import com.mongodb.MongoClient;
    import com.mongodb.MongoClientURI;
    import com.mongodb.client.FindIterable;
    import com.mongodb.client.MongoCollection;
    import com.mongodb.client.MongoDatabase;
    import com.mongodb.client.model.Filters;
    import org.bson.Document;
    
    /**
     * 文档过滤信查询
     */
    public class DocumentFindFiltersTest {
    
        public static void main(String[] args) {
            MongoClientURI url = new MongoClientURI("mongodb://admin:123456@192.168.47.128:27017/obs");
            MongoClient mongoClient = new MongoClient(url);
            // 获取数据库对象
            MongoDatabase mongoDatabase = mongoClient.getDatabase("obs");
            // 获取集合对象
            MongoCollection<Document> collection = mongoDatabase.getCollection("preview");
            // 要根据expectSalary 降序排列
            Document  sortDocument  =  new Document();
            sortDocument.append("expectSalary",-1);
            FindIterable<Document>  findIterable =  collection.find(Filters.gt("expectSalary",21000)).sort(sortDocument);
            for(Document document : findIterable){
                System.out.println(document);
            }
            mongoClient.close();
        }
    }
    
    
Spring访问操作MongoDB
  • 基于maven新建工程 导入依赖的包

    <dependency>
         <groupId>org.springframework.data</groupId>
         <artifactId>spring-data-mongodb</artifactId>
         <version>2.0.9.RELEASE</version>
    </dependency>
    
  • 在配置文件中配置 MongoTemplate

    • applicationContext.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
           xmlns:mongo="http://www.springframework.org/schema/data/mongo"
           xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/data/mongo http://www.springframework.org/schema/data/mongo/spring-mongo.xsd">
    
        <mongo:db-factory id="mongoDbFactory" client-uri="mongodb://admin:123456@192.168.47.128:27017/obs"></mongo:db-factory>
        <bean id="mongoTemplate"  class="org.springframework.data.mongodb.core.MongoTemplate">
            <constructor-arg index="0" ref="mongoDbFactory"></constructor-arg>
        </bean>
        <context:component-scan base-package="org.example.spring"></context:component-scan>
    </beans>
    
  • 实现类注入 MongoTemplate 完成增删改查

        @Autowired
        private MongoTemplate mongoTemplate;
    
        @Override
        public void insertResume(Resume resume) {
            mongoTemplate.insert(resume,"resume");
        }
    
        @Override
        public Resume findByName(String name) {
            Query query = new Query();
            query.addCriteria(Criteria.where("name").is(name));
            List<Resume> datas = mongoTemplate.find(query,Resume.class,"resume");
            return  datas.isEmpty()?null:datas.get(0);
        }
    
        @Override
        public List<Resume> findList(String name) {
            Query  query = new Query();
            query.addCriteria(Criteria.where("name").is(name));
            List<Resume> datas = mongoTemplate.find(query,Resume.class,"resume");
            return  datas;
        }
    
        @Override
        public List<Resume> findListByNameAndExpectSalary(String name, double expectSalary) {
            Query  query = new Query();
            query.addCriteria(Criteria.where("name").is(name).andOperator(Criteria.where("expectSalary").is(expectSalary)));
            return  mongoTemplate.find(query,Resume.class,"resume");
        }
    
Spring Boot 访问操作MongoDB
  • 引入maven包

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>org.example</groupId>
        <artifactId>mongoDemo</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.3.3.RELEASE</version>
            <relativePath/>
        </parent>
    
    
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-mongodb</artifactId>
            </dependency>
        </dependencies>
    
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>6</source>
                        <target>6</target>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    
  • 配置相关配置文件

    • application.properties
    spring.data.mongodb.host=192.168.47.128
    spring.data.mongodb.port=27017
    spring.data.mongodb.database=obs
    spring.data.mongodb.username=admin
    spring.data.mongodb.password=123456
    
MongonDB事务
  • MongoDB 单文档原生支持原子性,也具备事务的特性,但是我们说起事务,通常是指在多文档中的实现,因此,MongoDB 在 4.0 版本支持了多文档事务,4.0 对应于复制集的多表、多行,后续又在 4.2 版本支持了分片集的多表、多行事务操作

  • 事务配置(开启事务管理)

    @Configuration
    public class MongoTransactionConfiguration {
     
        @Bean
        MongoTransactionManager mongoTransactionManager(MongoDbFactory factory) {
            return new MongoTransactionManager(factory);
        }
    }
    
    • 需要事务管理方法:

       @Transactional(rollbackFor = Exception.class)
      

MongoDB架构

MongoDB逻辑结构

在这里插入图片描述

  • MongoDB 与 MySQL 中的架构相差不多,底层都使用了可插拔的存储引擎以满足用户的不同需要。用 户可以根据程序的数据特征选择不同的存储引擎,在最新版本的 MongoDB 中使用了 WiredTiger 作为默 认的存储引擎,WiredTiger 提供了不同粒度的并发控制和压缩机制,能够为不同种类的应用提供了最 好的性能和存储率
  • 在存储引擎上层的就是 MongoDB 的数据模型和查询语言了,由于 MongoDB 对数据的存储与 RDBMS 有较大的差异,所以它创建了一套不同的数据模型和查询语言
MongoDB的数据模型
  • 描述数据模型
  • 内嵌
    • 内嵌的方式指的是把相关联的数据保存在同一个文档结构之中。MongoDB的文档结构允许一个字 段或者一个数组内的值作为一个嵌套的文档
  • 引用
    • 引用方式通过存储数据引用信息来实现两个不同文档之间的关联,应用程序可以通过解析这些数据引 用来访问相关数据
  • 如何选择数据模型
    • 选择内嵌
      • 数据对象之间有包含关系 ,一般是数据对象之间有一对多或者一对一的关系
      • 需要经常一起读取的数据
      • 有 map-reduce/aggregation 需求的数据放在一起,这些操作都只能操作单个 collection
    • 选择引用
      • 当内嵌数据会导致很多数据的重复,并且读性能的优势又不足于覆盖数据重复的弊端
      • 需要表达比较复杂的多对多关系的时候
      • 大型层次结果数据集 嵌套不要太深
MongoDB 存储引擎
  • 存储引擎概述

    • 存储引擎是MongoDB的核心组件,负责管理数据如何存储在硬盘和内存上。MongoDB支持的存储引擎有 MMAPv1 ,WiredTiger和InMemory。InMemory存储引擎用于将数据只存储在内存中,只将少量的元数据 (meta-data)和诊断日志(Diagnostic)存储到硬盘文件中,由于不需要Disk的IO操作,就能获取所需 的数据,InMemory存储引擎大幅度降低了数据查询的延迟(Latency)。从mongodb3.2开始默认的存储 引擎是WiredTiger,3.2版本之前的默认存储引擎是MMAPv1,mongodb4.x版本不再支持MMAPv1存储引 擎

      storage:
      	journal:
      		enabled: true
      	dbPath: /data/mongo/
      	##是否一个库一个文件夹
      	directoryPerDB: true
      	##数据引擎
      	engine: wiredTiger
      	##WT引擎配置
      	WiredTiger:
      		engineConfig:
      			##WT最大使用cache(根据服务器实际情况调节)
      			cacheSizeGB: 2
      			##是否将索引也按数据库名单独存储
      			directoryForIndexes: true
      			#(默认snappy)
      			journalCompressor: none
      			##表压缩配置
      			collectionConfig:
      				blockCompressor: zlib (默认snappy,还可选none、zlib)
      			##索引配置
      			indexConfig:
      				prefixCompression: true
      
  • WiredTiger存储引擎优势

    • WiredTiger是从 MongoDB 3.2 开始的默认存储引擎。它非常适合大多数工作负载,建议用于新部署。WiredTiger 提供文档级并发模型、检查点和压缩等功能
    • 文档空间分配方式
      • WiredTiger使用的是BTree存储 MMAPV1 线性存储 需要Padding
    • 并发级别
      • WiredTiger 文档级别锁 MMAPV1引擎使用表级锁
    • 数据压缩
      • snappy (默认) 和 zlib ,相比MMAPV1(无压缩) 空间节省数倍
    • 内存使用
      • WiredTiger 可以指定内存的使用大小
    • Cache使用
      • WT引擎使用了二阶缓存WiredTiger Cache, File System Cache来保证Disk上的数据的最终一 致性。而MMAPv1 只有journal 日志。
  • WiredTiger引擎包含的文件和作用

    在这里插入图片描述

    • 解析
      • WiredTiger.basecfg: 存储基本配置信息,与 ConfigServer有关系
      • WiredTiger.lock: 定义锁操作
      • table*.wt: 存储各张表的数据
      • WiredTiger.wt: 存储table* 的元数据
      • WiredTiger.turtle: 存储WiredTiger.wt的元数据
      • journal: 存储WAL(Write Ahead Log)
  • WiredTiger存储引擎实现原理

    • 写请求

      • WiredTiger的写操作会默认写入 Cache ,并持久化到 WAL (Write Ahead Log),每60s或Log文件达到2G 做一次 checkpoint (当然我们也可以通过在写入时传入 j: true 的参数强制 journal 文件的同步 , writeConcern { w: , j: , wtimeout: }) 产生快照文件。WiredTiger初始化时,恢复至最新的快照状态,然后再根据WAL 恢复数据,保证数据的完整性

        在这里插入图片描述

      • Cache是基于BTree的,节点是一个page,root page是根节点,internal page是中间索引节点,leaf page真正存储数据,数据以page为单位读写。WiredTiger采用Copy on write的方式管理写操作 (insert、update、delete),写操作会先缓存在cache里,持久化时,写操作不会在原来的leaf page 上进行,而是写入新分配的page,每次checkpoint都会产生一个新的root page

    • checkpoint流程

      • 对所有的table进行一次checkpoint,每个table的checkpoint的元数据更新至WiredTiger.wt
      • .对WiredTiger.wt进行checkpoint,将该table checkpoint的元数据更新至临时文件 WiredTiger.turtle.set
      • 将WiredTiger.turtle.set重命名为WiredTiger.turtle
      • 上述过程如果中间失败,WiredTiger在下次连接初始化时,首先将数据恢复至最新的快照状态,然后根 据WAL恢复数据,以保证存储可靠性
    • Journaling

      • 在数据库宕机时 , 为保证 MongoDB 中数据的持久性,MongoDB 使用了 Write Ahead Logging 向磁盘 上的 journal 文件预先进行写入。除了 journal 日志,MongoDB 还使用检查点(checkpoint)来保证 数据的一致性,当数据库发生宕机时,我们就需要 checkpoint 和 journal 文件协作完成数据的恢复工 作

        • 在数据文件中查找上一个检查点的标识符
        • 在 journal 文件中查找标识符对应的记录
        • 重做对应记录之后的全部操作

        在这里插入图片描述

MongoDB集群高可用

MongoDB主从复制架构原理
  • master-slave架构中master节点负责数据的读写,slave没有写入权限只负责读取数据

    在这里插入图片描述

    在主从结构中,主节点的操作记录成为oplog(operation log)。oplog存储在系统数据库local的 oplog.$main集合中,这个集合的每个文档都代表主节点上执行的一个操作。从服务器会定期从主服务器 中获取oplog记录,然后在本机上执行!对于存储oplog的集合,MongoDB采用的是固定集合,也就是说随 着操作过多,新的操作会覆盖旧的操作!

  • 主从结构没有自动故障转移功能,需要指定master和slave端,不推荐在生产中使用,mongodb4.0后不再支持主从复制!

复制集replica sets
  • 什么是复制集

    在这里插入图片描述

    复制集是由一组拥有相同数据集的mongod实例做组成的集群。
    复制集是一个集群,它是2台及2台以上的服务器组成,以及复制集成员包括Primary主节点,secondary从
    节点
    和投票节点。
    复制集提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性,保证数据的安全
    性
    
  • 为什么要使用复制集

    • 高可用
      • 防止设备(服务器、网络)故障
      • 提供自动failover 功能
      • 技术来保证高可用
    • 灾难恢复
      • 当发生故障时,可以从其他节点恢复 用于备份
    • 功能隔离
      • 我们可以在备节点上执行读操作,减少主节点的压力
      • 比如:用于分析、报表,数据挖掘,系统任务等
  • 复制集集群架构原理

    • 一个复制集中Primary节点上能够完成读写操作,Secondary节点仅能用于读操作。Primary节点需要记 录所有改变数据库状态的操作,这些记录保存在 oplog 中,这个文件存储在 local 数据库,各个Secondary 节点通过此 oplog 来复制数据并应用于本地,保持本地的数据与主节点的一致。oplog 具有幂等性,即无 论执行几次其结果一致,这个比 mysql 的二进制日志更好用

    • oplog的组成结构

      {
      "ts" : Timestamp(1446011584, 2),
      "h" : NumberLong("1687359108795812092"),
      "v" : 2,
      "op" : "i",
      "ns" : "test.nosql",
      "o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb",
      "score" : "10"}
      }
      ts:操作时间,当前timestamp + 计数器,计数器每秒都被重置
      h:操作的全局唯一标识
      v:oplog版本信息
      op:操作类型
      i:插入操作
      u:更新操作
      d:删除操作
      c:执行命令(如createDatabase,dropDatabase)
      n:空操作,特殊用途
      ns:操作针对的集合
      o:操作内容
      o2:更新查询条件,仅update操作包含该字段
      
      

      复制集数据同步分为初始化同步和keep复制同步。初始化同步指全量从主节点同步数据,如果Primary 节点数据量比较大同步时间会比较长。而keep复制指初始化同步过后,节点之间的实时同步一般是增量 同步

      • 初始化同步有以下两种情况会触发

        • Secondary第一次加入
        • Secondary落后的数据量超过了oplog的大小,这样也会被全量复制
      • MongoDB的Primary节点选举基于心跳触发。一个复制集N个节点中的任意两个节点维持心跳,每个节 点维护其他N-1个节点的状态

        在这里插入图片描述

        心跳检测:
        整个集群需要保持一定的通信才能知道哪些节点活着哪些节点挂掉。mongodb节点会向副本集中的其他节点
        每2秒就会发送一次pings包,如果其他节点在10秒钟之内没有返回就标示为不能访问。每个节点内部都会
        维护一个状态映射表,表明当前每个节点是什么角色、日志时间戳等关键信息。如果主节点发现自己无法与
        大部分节点通讯则把自己降级为secondary只读节点
        
      • 主节点选举触发的时机

        第一次初始化一个复制集
        Secondary节点权重比Primary节点高时,发起替换选举
        Secondary节点发现集群中没有Primary时,发起选举
        Primary节点不能访问到大部分(Majority)成员时主动降级
        
      • 当触发选举时,Secondary节点尝试将自身选举为Primary。主节点选举是一个二阶段过程+多数派协 议

        第一阶段:
        检测自身是否有被选举的资格 如果符合资格会向其它节点发起本节点是否有选举资格的
        FreshnessCheck,进行同僚仲裁
        第二阶段:
        发起者向集群中存活节点发送Elect(选举)请求,仲裁者收到请求的节点会执行一系列合法性检查,如果检
        查通过,则仲裁者(一个复制集中最多50个节点 其中只有7个具有投票权)给发起者投一票。
        pv0通过30秒选举锁防止一次选举中两次投票。
        pv1使用了terms(一个单调递增的选举计数器)来防止在一次选举中投两次票的情况。
        多数派协议:
        发起者如果获得超过半数的投票,则选举通过,自身成为Primary节点。获得低于半数选票的原因,除了常
        见的网络问题外,相同优先级的节点同时通过第一阶段的同僚仲裁并进入第二阶段也是一个原因。因此,当
        选票不足时,会sleep[0,1]秒内的随机时间,之后再次尝试选举
        
复制集搭建

在这里插入图片描述

  • 主节点配置 mongo_37017.conf

    # 主节点配置
    dbpath=/data/mongo/data/server1
    bind_ip=0.0.0.0
    port=37017
    fork=true
    logpath=/data/mongo/logs/server1.log
    replSet=cluster
    
  • 从节点1配置 mongo_37018.conf

    dbpath=/data/mongo/data/server2
    bind_ip=0.0.0.0
    port=37018
    fork=true
    logpath=/data/mongo/logs/server2.log
    replSet=cluster
    
  • 从节点2配置 mongo_37019.conf

    dbpath=/data/mongo/data/server3
    bind_ip=0.0.0.0
    port=37019
    fork=true
    logpath=/data/mongo/logs/server3.log
    replSet=cluster
    
  • 初始化节点配置

    • 启动三个节点 然后进入任意一个节点 运行如下命令:

      var cfg ={"_id":"cluster",
      		"protocolVersion" : 1,
      		"members":[
      			{"_id":1,"host":"192.168.47.128:37017","priority":10},
      			{"_id":2,"host":"192.168.47.128:37018"}
      		]
      	}
      rs.initiate(cfg)
      rs.status()
      
      

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kMMWtj20-1647008190640)(img\cluster-shell.png)]

  • 节点的动态增删

    增加节点
    rs.add("192.168.47.128:37019")
    删除slave 节点
    rs.remove("192.168.47.128:37019")
    
  • 复制集操作实现

    进入主节点 ----- 插入数据 ------ 进入从节点验证
    注意:默认节点下从节点不能读取数据。调用rs.secondaryOk()解决
    为了保证高可用,在集群当中如果主节点挂掉后,会自动 在从节点中选举一个 重新做为主节点。
    rs.status()
    节点说明:
    PRIMARY 节点: 可以查询和新增数据
    SECONDARY 节点:只能查询 不能新增 基于priority 权重可以被选为主节点
    ARBITER 节点: 不能查询数据 和新增数据 ,不能变成主节点
    
  • 复制集成员的配置参数

    参数字段类型说明取值说明
    _id整数_id:0复制集中的标示
    host字符串host:“主机:端口”节点主机名
    arbiterOnly布尔值arbiterOnly:true是否为仲裁(裁判)节点
    priority(权 重)整数priority=0|1默认1,是否有资格变成主节点,取值范围0-1000,0永远不 会变成主节点
    hidden布尔值hidden=true|fasle,0|1隐藏,权重必须为0,才可以设置
    votes整数votes= 0|1投票,是否为投票节点,0 不投票,1投票
    slaveDelay整数slaveDelay=3600从库的延迟多少秒
    buildIndexes布尔值buildIndexes=true|false,0|1主库的索引,从库也创建,_id索引无效
    • 案例:

      var cfg ={"_id":"lagouCluster",
      		"protocolVersion" : 1,
      		"members":[
      				{"_id":1,"host":"192.168.47.128:27017","priority":10},
      				{"_id":2,"host":"192.168.47.128:27018","priority":0},
      				{"_id":3,"host":"192.168.47.128:27019","priority":5},
      				{"_id":4,"host":"192.168.47.128:27020","arbiterOnly":true}
      			]
      		};
      // 重新装载配置,并重新生成集群节点。
      rs.reconfig(cfg)
      //重新查看集群状态
      rs.status()
      
      
    • 有仲裁节点复制集搭建

      和上面的配置步骤相同 只是增加了 一个特殊的仲裁节点
      注入节点 执行 rs.addArb("IP:端口");
      rs.addArb("192.168.47.128:37020")
      
分片集群 Shard Cluster
  • 什么是分片

    • 分片(sharding)是MongoDB用来将大型集合水平分割到不同服务器(或者复制集)上所采用的方法。 不需要功能强大的大型计算机就可以存储更多的数据,处理更大的负载
  • 为什么要分片

    • 存储容量需求超出单机磁盘容量
    • 活跃的数据集超出单机内存容量,导致很多请求都要从磁盘读取数据,影响性能
    • IOPS超出单个MongoDB节点的服务能力,随着数据的增长,单机实例的瓶颈会越来越明显
    • 副本集具有节点数量限制
    • 垂直扩展:增加更多的CPU和存储资源来扩展容量
    • 水平扩展:将数据集分布在多个服务器上。水平扩展即分片
  • 分片的工作原理

    在这里插入图片描述

    • 分片集群由以下3个服务组成

      • Shards Server: 每个shard由一个或多个mongod进程组成,用于存储数据
      • Router Server: 数据库集群的请求入口,所有请求都通过Router(mongos)进行协调,不需要在应用程 序添加一个路由选择器,Router(mongos)就是一个请求分发中心它负责把应用程序的请求转发到对应的 Shard服务器上
      • Config Server: 配置服务器。存储所有数据库元信息(路由、分片)的配置
    • 片键(shard key)

      • 为了在数据集合中分配文档,MongoDB使用分片主键分割集合
    • 区块(chunk)

      • 在一个shard server内部,MongoDB还是会把数据分为chunks,每个chunk代表这个shard server内部一部分数据。MongoDB分割分片数据到区块,每一个区块包含基于分片主键的左闭右开的 区间范围
    • 分片策略

      • 范围分片(Range based sharding)

        在这里插入图片描述

        • 范围分片是基于分片主键的值切分数据,每一个区块将会分配到一个范围
        • 范围分片适合满足在一定范围内的查找,例如查找X的值在[20,30)之间的数据,mongo 路由根据 Config server中存储的元数据,可以直接定位到指定的shard的Chunk中
        • 缺点: 如果shard key有明显递增(或者递减)趋势,则新插入的文档多会分布到同一个chunk,无 法扩展写的能力
      • hash分片(Hash based sharding)

        在这里插入图片描述

        • Hash分片是计算一个分片主键的hash值,每一个区块将分配一个范围的hash值
        • Hash分片与范围分片互补,能将文档随机的分散到各个chunk,充分的扩展写能力,弥补了范围 分片的不足,缺点是不能高效的服务范围查询,所有的范围查询要分发到后端所有的Shard才能找 出满足条件的文档
      • 合理的选择shard key

        • 从两个方面考虑,数据的查询和写入,最好的效果就是数据查询时能命中更少的分片,数据写入时 能够随机的写入每个分片,关键在于如何权衡性能和负载
分片集群的搭建

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NCjFVLxW-1647008190642)(img\shard-cluter.png)]

  • 配置 并启动config 节点集群

    • 节点1 config-27017.con
    # 数据库文件位置
    dbpath=config/config1
    #日志文件位置
    logpath=config/logs/config1.log
    # 以追加方式写入日志
    logappend=true
    # 是否以守护进程方式运行
    fork = true
    bind_ip=0.0.0.0
    port = 27017
    # 表示是一个配置服务器
    configsvr=true
    #配置服务器副本集名称
    replSet=configsvr
    
    
    • 节点2 config-27018.conf

      dbpath=config/config2
      #日志文件位置
      logpath=config/logs/config.log
      # 以追加方式写入日志
      logappend=true
      # 是否以守护进程方式运行
      fork = true
      bind_ip=0.0.0.0
      port = 27018
      # 表示是一个配置服务器
      configsvr=true
      #配置服务器副本集名称
      replSet=configsvr
      
    • 节点3 config-27019.conf

      # 数据库文件位置
      dbpath=config/config3
      #日志文件位置
      logpath=config/logs/config3.log
      # 以追加方式写入日志
      logappend=true
      # 是否以守护进程方式运行
      fork = true
      bind_ip=0.0.0.0
      port = 27019
      # 表示是一个配置服务器
      configsvr=true
      #配置服务器副本集名称
      replSet=configsvr
      
    • 启动配置节点

      ./bin/mongod -f config/config-27017.conf
      ./bin/mongod -f config/config-27018.conf
      ./bin/mongod -f config/config-27019.conf
      
    • 进入任意节点的mongo shell 并添加 配置节点集群 注意use admin

      ./bin/mongo --port 27017
      use admin
      var cfg ={"_id":"configsvr",
      	"members":[
      		{"_id":1,"host":"192.168.47.128:17017"},
      		{"_id":2,"host":"192.168.47.128:17018"},
      		{"_id":3,"host":"192.168.47.128:17019"}]
      };
      rs.initiate(cfg)
      
      
    • 配置shard集群

      • 配置文件(shard1集群 47017-47019)

        #节点一
        dbpath=shard/shard1/shard1-47017
        bind_ip=0.0.0.0
        port=47017
        fork=true
        logpath=shard/shard1/shard1-47017.log
        replSet=shard1
        shardsvr=true
        
        #节点二
        dbpath=shard/shard1/shard1-47018
        bind_ip=0.0.0.0
        port=47018
        fork=true
        logpath=shard/shard1/logs/shard1-47018.log
        replSet=shard1
        shardsvr=true
        
        #节点三
        dbpath=shard/shard1/shard1-47019
        bind_ip=0.0.0.0
        port=47019
        fork=true
        logpath=shard/shard1/logs/shard1-47019.log
        replSet=shard1
        shardsvr=true
        
        
        #启动每个mongod 然后进入其中一个进行集群配置
        var cfg ={"_id":"shard1",
        	"protocolVersion" : 1,
        	"members":[
        		{"_id":1,"host":"192.168.47.128:47017"},
        		{"_id":2,"host":"192.168.47.128:47018"},
        		{"_id":3,"host":"192.168.47.128:47019"}
        	]
        };
        rs.initiate(cfg)
        rs.status()
        
        
      • 配置文件(shard2集群57017-57019)

        #节点1
        dbpath=shard/shard2/shard2-57017
        bind_ip=0.0.0.0
        port=57017
        fork=true
        logpath=shard/shard2/logs/shard2-57017.log
        replSet=shard2
        shardsvr=true
        
        
        #节点二
        dbpath=shard/shard2/shard2-57018
        bind_ip=0.0.0.0
        port=57018
        fork=true
        logpath=shard/shard2/logs/shard2-47018.log
        replSet=shard2
        shardsvr=true
        
        #节点三
        dbpath=shard/shard2/shard2-57019
        bind_ip=0.0.0.0
        port=57019
        fork=true
        logpath=shard/shard2/logs/shard2-57019.log
        replSet=shard2
        shardsvr=true
        
        
        
        #启动每个mongod 然后进入其中一个进行集群配置
        var cfg ={"_id":"shard2",
        	"protocolVersion" : 1,
        	"members":[
        		{"_id":1,"host":"192.168.47.128:47017"},
        		{"_id":2,"host":"192.168.47.128:47018"},
        		{"_id":3,"host":"192.168.47.128:47019"}
        	]
        };
        rs.initiate(cfg)
        rs.status()
        
        
        
      • 配置和启动 路由节点

        • route-17017.conf
      port=17017
      bind_ip=0.0.0.0
      fork=true
      logpath=route/logs/route.log
      configdb=configsvr/192.168.47.128:27017,192.168.47.128:27018,192.168.47.128:27019
      
      • 启动路由节点使用 mongos (注意不是mongod)

        ./bin/mongos -f route/route-17017.conf
        
      • mongos(路由)中添加分片节点

        # 进入路由mongos
        mongo --port 17017
        sh.status()
        sh.addShard("shard1/192.168.47.128:47017,192.168.47.128:47018,192.168.47.128:
        47019");
        sh.addShard("shard2/192.168.47.128:57017,192.168.47.128:57018,192.168.47.128:
        57019");
        sh.status()
        
        
        
    • 开启数据库和集合分片(指定片键)

      • 使用mongos完成分片开启和分片大小设置

        #为数据库开启分片功能
        sh.enableSharding("resume")
        #为指定集合开启分片功能
        sh.shardCollection("resume.resume_datas",{"片键字段名如 name":索引说
        明})
        
      • 向集合中插入数据测试

        use resume;
        for(var i=1;i<= 1000;i++){
        	db.resume_datas.insert({"name":"test"+i,
        	salary:(Math.random()*20000).toFixed(2)});
        }
        
        
      • 验证分片效果

        • 别进入 shard1 和 shard2 中的数据库 进行验证

MogoDB安全认证

安全认证概述

MongoDB 默认是没有账号的,可以直接连接,无须身份验证。实际项目中肯定是要权限验证的,否则 后果不堪设想。从2016年开始 发生了多起MongoDB黑客赎金事件,大部分MongoDB安全问题 暴露出 了安全问题的短板其实是用户,首先用户对于数据库的安全不重视,其次用户在使用过程中可能没有养 成定期备份的好习惯,最后是企业可能缺乏有经验和技术的专业人员。所以对MongoDB进行安全认证 是必须要做的

用户相关操作
  • 切换到admin数据库对用户的添加

    use admin;
    db.createUser(userDocument):用于创建 MongoDB 登录用户以及分配权限的方法
    
    db.createUser(
    	{
    		user: "账号",
    		pwd: "密码",
    		roles: [
    			{ role: "角色", db: "安全认证的数据库" },
    			{ role: "角色", db: "安全认证的数据库" }
    		]
    	}
    )
    
    
    • user:创建的用户名称,如 admin、root 、lagou

    • pwd:用户登录的密码

    • roles:为用户分配的角色,不同的角色拥有不同的权限,参数是数组,可以同时设置多个

    • role:角色,MonngoDB 已经约定好的角色,不同的角色对应不同的权限

    • db:数据库实例名称,如 MongoDB 4.0.2 默认自带的有 admin、local、config、test 等,即为哪个数 据库实例 设置用户

      示例

      db.createUser(
      	{
      		user:"root",
      		pwd:"123321",
      		roles:[{role:"root",db:"admin"}]
      	}
      )
      
  • 修改密码

    db.changeUserPassword( ‘root’ , ‘rootNew’ );

  • 用户添加角色

    db.grantRolesToUser( ‘用户名’ , [{ role: ‘角色名’ , db: ‘数据库名’}])

  • 以auth 方向启动mongod

    ./bin/mongod -f conf/mongo.conf --auth

    也可以在mongo.conf 中添加auth=true 参数

  • 验证用户

    db.auth(“账号”,“密码”)

  • 删除用户

    db.dropUser(“用户名”)

角色
  • 数据库内置的角色

    read:允许用户读取指定数据库
    readWrite:允许用户读写指定数据库
    dbAdmin:允许用户在指定数据库中执行管理函数,如索引创建、删除,查看统计或访问
    system.profile
    userAdmin:允许用户向system.users集合写入,可以找指定数据库里创建、删除和管理用户
    clusterAdmin:只在admin数据库中可用,赋予用户所有分片和复制集相关函数的管理权限
    readAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读权限
    readWriteAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读写权限
    userAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的userAdmin权限
    dbAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的dbAdmin权限
    root:只在admin数据库中可用。超级账号,超级权限
    dbOwner:库拥有者权限,即readWrite、dbAdmin、userAdmin角色的合体
    
  • 各个类型用户对应的角色

    数据库用户角色:read、readWrite
    数据库管理角色:dbAdmin、dbOwner、userAdmin
    集群管理角色:clusterAdmin、clusterManager、clusterMonitor、hostManager
    备份恢复角色:backup、restore;
    所有数据库角色:readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、
    dbAdminAnyDatabase
    超级用户角色:root
    这里还有几个角色间接或直接提供了系统超级用户的访问(dbOwner 、userAdmin、
    userAdminAnyDatabase)
    
  • 单机安全认证实现流程

    • 创建管理员

      MongoDB 服务端开启安全检查之前,至少需要有一个管理员账号,admin 数据库中的用户都被视为管 理员 如果 admin 库没有任何用户的话,即使在其他数据库中创建了用户,启用身份验证,默认的连接方式 依然会有超级权限,即仍然可以不验证账号密码照样能进行 CRUD,安全认证相当于无效

      >use admin
      switched to db admin
      > db
      admin
      > db.createUser(
      ... {
      ... user:"root",
      ... pwd:"123456",
      ... roles:[{role:"root",db:"admin"}]
      ... })
      
      
    • 创建普通用户

      #zhangsan 拥有读写权限
      > db.createUser({
      ... user:"zhangsan",
      ... pwd:"123456",
      ... roles:[{role:"readWrite",db:"mydb1"}]
      ... })
      
      #lisi 拥有只读权限
      > db.createUser({
      ... user:"lisi",
      ... pwd:"123456",
      ... roles:[{role:"read",db:"mydb1"}]
      ... })
      
  • MongoDB 安全认证方式启动

    mongod --dbpath=数据库路径 --port=端口 --auth
    也可以在配置文件中 加入 auth=true
    
分片集群安全认证
  • 关闭所有的配置节点 分片节点 和 路由节点

    安装psmisc
    	yum install psmisc
    安装完之后可以使用killall 命令 快速关闭多个进程
    	killall mongod
    
    
  • 生成密钥文件 并修改权限

    openssl rand -base64 756 > data/mongodb/testKeyFile.file
    chmod 600 data/mongodb/keyfile/testKeyFile.file
    
  • 配置节点集群和分片节点集群开启安全认证和指定密钥文件

    auth=true
    keyFile=data/mongodb/testKeyFile.file
    
  • 在路由配置文件中 设置密钥文件

    keyFile=data/mongodb/testKeyFile.file
    
  • 启动所有的配置节点 分片节点 和 路由节点 使用路由进行权限验证

    建议shell脚本启动

    ./bin/mongod -f config/配置文件.conf
    ./bin/mongod -f shard/shard1/分片配置.conf
    ./bin/mongos -f route/路由.conf
    
  • Spring boot 连接安全认证的分片集群

    #spring.data.mongodb.uri=mongodb://账号:密码@IP:端口/数据库名
    spring.data.mongodb.host=192.168.47.128
    spring.data.mongodb.port=27017
    spring.data.mongodb.database=obs
    spring.data.mongodb.username=admin
    spring.data.mongodb.password=123456
    

直接提供了系统超级用户的访问(dbOwner 、userAdmin、
userAdminAnyDatabase)


- **单机安全认证实现流程**

- **创建管理员**

  MongoDB 服务端开启安全检查之前,至少需要有一个管理员账号,admin 数据库中的用户都被视为管 理员 如果 admin 库没有任何用户的话,即使在其他数据库中创建了用户,启用身份验证,默认的连接方式 依然会有超级权限,即仍然可以不验证账号密码照样能进行 CRUD,安全认证相当于无效

  ```
  >use admin
  switched to db admin
  > db
  admin
  > db.createUser(
  ... {
  ... user:"root",
  ... pwd:"123456",
  ... roles:[{role:"root",db:"admin"}]
  ... })
  
  ```

- **创建普通用户**

  ```
  #zhangsan 拥有读写权限
  > db.createUser({
  ... user:"zhangsan",
  ... pwd:"123456",
  ... roles:[{role:"readWrite",db:"mydb1"}]
  ... })
  
  #lisi 拥有只读权限
  > db.createUser({
  ... user:"lisi",
  ... pwd:"123456",
  ... roles:[{role:"read",db:"mydb1"}]
  ... })
  ```

  

- **MongoDB 安全认证方式启动**

mongod --dbpath=数据库路径 --port=端口 --auth
也可以在配置文件中 加入 auth=true


##### 分片集群安全认证

- **关闭所有的配置节点 分片节点 和 路由节点**

安装psmisc
yum install psmisc
安装完之后可以使用killall 命令 快速关闭多个进程
killall mongod


- **生成密钥文件 并修改权限**

openssl rand -base64 756 > data/mongodb/testKeyFile.file
chmod 600 data/mongodb/keyfile/testKeyFile.file


- **配置节点集群和分片节点集群开启安全认证和指定密钥文件**

auth=true
keyFile=data/mongodb/testKeyFile.file




- **在路由配置文件中 设置密钥文件**

keyFile=data/mongodb/testKeyFile.file


- **启动所有的配置节点 分片节点 和 路由节点 使用路由进行权限验证**

建议shell脚本启动

./bin/mongod -f config/配置文件.conf
./bin/mongod -f shard/shard1/分片配置.conf
./bin/mongos -f route/路由.conf


- **Spring boot 连接安全认证的分片集群**

#spring.data.mongodb.uri=mongodb://账号:密码@IP:端口/数据库名
spring.data.mongodb.host=192.168.47.128
spring.data.mongodb.port=27017
spring.data.mongodb.database=obs
spring.data.mongodb.username=admin
spring.data.mongodb.password=123456




  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-03-12 17:36:19  更:2022-03-12 17:37:01 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 8:44:44-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码