TENCHIANG / blog

issue blog
10 stars 1 forks source link

MongoDB优化 #28

Open TENCHIANG opened 5 years ago

TENCHIANG commented 5 years ago

名词对照

阿里云找到MongoDB远程连接地址

登陆阿里云, 从控制台选择云数据库MongoDB, 点击实例id进行管理 在连接信息 (Connection String URI)下面找到网络类型->公网->mongodb://root:****@dds-bpxxxxxx, 复制下来把星号替换为root密码

连接MongoDB

连接方式1也就是公网那个地址复制过来的是Primary和Secondary域名都写在一起的方式, 连接方式2也可以直接连Primary就行 Primary显示的是专有网络的域名 其实要连接对应的公网的域名 也就是连接方式1的第一个域名

mongo mongodb://root:****@dds-bpxxxxxxxxxxxx-pub.mongodb.rds.aliyuncs.com:3717,dds-bpyyyyyyyyyyyyyyy-pub.mongodb.rds.aliyuncs.com:3717/admin?replicaSet=mgset-1203073 # 连接方式1 第一个域名就是Primary的公网版本
mongo \
--port 3717 \
-u 'root' \
-p 'xxxxx' \
--authenticationDatabase 'admin' \
dds-bpxxxxxxx-pub.mongodb.rds.aliyuncs.com # 连接方式2

db # 当前数据库
show dbs # 列出所有数据库

use <db_name>
show collections # 显示当前数据库所有的collection(表)

db.getCollection('xxx').find().pretty() # 显示xxx表里所有的document 方式1
db.xxx.find().pretty() # 显示xxx表里所有的document 方式2

db.system.profile.find().pretty().count() # 统计当前数据库的满查询数量
db.system.profile.find({}, { ns: 1, millis: 1 }).limit(10).sort({ millis: -1 }).pretty() # 10个最慢查询
db.system.profile.find({}, { ns: 1, millis: 1 }).limit(10).sort({ ts: -1 }).pretty() # 最新的10个慢查询

db.stats() # 当前数据库状态

system.profile.command解释

{
    command: {
        find: 'items',
        filter: {
            sku: '123456'
        },
        ...,
        $db: 'test'
    }
}
// 等价于
use test
db.items.find({ sku: '123456' })

MongoDB慢查询优化

主要是开启Database Profiler记录慢查询, 然后根据db.system.profile.find()来优化代码, 建立索引 MongoDB 查询优化分析 监控mongo 状态慢查询 论MongoDB索引选择的重要性 这里是建议选择createdAt作为索引而不是_id, 因为这样会更快

MongoDB的find、getMore特性

MongoDB安全相关

MongoDB 用户名密码登录

MongoDB自带工具

数据库高级优化: 数据库拆分、读写分离

数据库拆分

  1. 水平拆分 化为更小单位, 如通过角色拆分用户
  2. 垂直拆分 直接拆分, 表之间通过主键关联

    读写分离

    • 读操作(查询)
    • 写操作(插入、更新)

读写指向不同节点, 节点之间数据同步(binlog) 因为读压力更大, 所以都是一主多从架构

更新document的代价

bson是字节数组, 前4字节是表示document的大小, 4字节最大能表示4GB, 也就是说, 单个document最大不超过4GB

  1. 改变单个值, bson大小不变, 最快, 如$inc
  2. 改变大小, 结构, 如$push
  3. 重写document, 迁移数据, 最慢

通过explain查看查询计划

查询计划: 就是如何执行查询 explain有3种模式:

  1. queryPlanner (默认) 有4个字段queryPlanner, ok, operationTime, $clusterTime
  2. executionStats 比前者更加详细的信息(加了executionStats字段: executionSuccess, nReturned, executionTimeMillis, totalKeysExamined, totalDocsExamined, executionStages), 下一节创建索引的时候有使用示例
  3. allPlansExecution

使用createIndex创建索引

以数据库jjqshopdbusers表为例

use jjqshopdb
db.users.count()
11

查看当前表的索引 可以看到, 默认就对_id进行了索引, 名为_id_

db.users.getIndexes()
[
    {
        "v" : 2,
        "key" : {
            "_id" : 1
        },
        "name" : "_id_",
        "ns" : "jjqshopdb.users"
    }
]

创建索引: createIndex, 当然ensureIndex也可以, 为了兼容老版本MongoDB 3.0以前 注意createIndex({ a: 1, b : 1 })注意不是创建了两个索引a_1b_1而是创建了一个联合索引a_1_b_1!!!

db.users.createIndex({ telephone: 1 }) // 给用户表建立电话的索引
db.users.getIndexes() // 验证
[
    {
        "v" : 2,
        "key" : {
            "_id" : 1
        },
        "name" : "_id_",
        "ns" : "jjqshopdb.users"
    },
    {
        "v" : 2,
        "key" : {
            "telephone" : 1
        },
        "name" : "telephone_1",
        "ns" : "jjqshopdb.users",
        "background" : true
    }
]

使用索引进行查询

db.users.find({ telephone: '176xxxx5209' }).explain('executionStats')
{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "jjqshopdb.users",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "telephone" : {
                "$eq" : "176xxxx5209"
            }
        },
        "winningPlan" : {
            "stage" : "FETCH",
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "telephone" : 1
                },
                "indexName" : "telephone_1", // 使用telephone_1索引!!!
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "telephone" : [ ]
                },
                "isUnique" : false,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "forward",
                "indexBounds" : {
                    "telephone" : [
                        "[\"176xxxx5209\", \"176xxxx5209\"]"
                    ]
                }
            }
        },
        "rejectedPlans" : [ ]
    },
    "executionStats" : {
        "executionSuccess" : true,
        "nReturned" : 1, // 返回1个文档
        "executionTimeMillis" : 0, // 非常快 几乎是 0ms
        "totalKeysExamined" : 1,  // 使用了1个索引
        "totalDocsExamined" : 1, // 只扫描1个文档 没用之前可能得扫描所有文档 如11
        "executionStages" : {
            "stage" : "FETCH",
            "nReturned" : 1,
            "executionTimeMillisEstimate" : 0,
            "works" : 2,
            "advanced" : 1,
            "needTime" : 0,
            "needYield" : 0,
            "saveState" : 0,
            "restoreState" : 0,
            "isEOF" : 1,
            "invalidates" : 0,
            "docsExamined" : 1,
            "alreadyHasObj" : 0,
            "inputStage" : {
                "stage" : "IXSCAN",
                "nReturned" : 1,
                "executionTimeMillisEstimate" : 0,
                "works" : 2,
                "advanced" : 1,
                "needTime" : 0,
                "needYield" : 0,
                "saveState" : 0,
                "restoreState" : 0,
                "isEOF" : 1,
                "invalidates" : 0,
                "keyPattern" : {
                    "telephone" : 1
                },
                "indexName" : "telephone_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "telephone" : [ ]
                },
                "isUnique" : false,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "forward",
                "indexBounds" : {
                    "telephone" : [
                        "[\"176xxxx5209\", \"176xxxx5209\"]"
                    ]
                },
                "keysExamined" : 1,
                "seeks" : 1,
                "dupsTested" : 0,
                "dupsDropped" : 0,
                "seenInvalidated" : 0
            }
        }
    },
    "ok" : 1,
    "operationTime" : Timestamp(1566976793, 1),
    "$clusterTime" : {
        "clusterTime" : Timestamp(1566976793, 1),
        "signature" : {
            "hash" : BinData(0,"Q9qoVAZebjbDaoqzMCayySTSpnY="),
            "keyId" : NumberLong("6683112302490681460")
        }
    }
}

删除索引 注意dropIndex({ a: 1, b : 1 })注意不是删除了两个索引a_1b_1而是删除了一个联合索引a_1_b_1!!! 注意删除索引删不了_id_索引, 只能删除表

db.users.dropIndex('telephone_1') # 索引名
db.users.dropIndex({ telephone: -1 }) # 字段名

查看MongoDB的源代码

比如 查看find命令的源代码

db.orders.find
function (query, fields, limit, skip, batchSize, options) {
    var cursor = new DBQuery(this._mongo,
                             this._db,
                             this,
                             this._fullName,
                             this._massageObject(query),
                             fields,
                             limit,
                             skip,
                             batchSize,
                             options || this.getQueryOptions());

    {
        const session = this.getDB().getSession();

        const readPreference = session._serverSession.client.getReadPreference(session);
        if (readPreference !== null) {
            cursor.readPref(readPreference.mode, readPreference.tags);
        }

        const readConcern = session._serverSession.client.getReadConcern(session);
        if (readConcern !== null) {
            cursor.readConcern(readConcern.level);
        }
    }

    return cursor;
}

MongoDB的shell命令实现原理

查看db.stats()的源代码, 会发现等价命令db.runCommand({dbstats: 1}) 也就是说可以用db.runCommand()调用任何命令 注意, 里面的this就相当于外面的db

db.stats
function (scale) {
        return this.runCommand({dbstats: 1, scale: scale});
    }
// 新版本的MongoDB已经不能直观看出db.runCommand的源代码了, 下面是老版本的
db.runCommand
function (obj, extra) {
    if (typeof obj == 'string') {
        const n = {}
        n[obj] = 1
        obj = n
        extra && typeof extra == 'object' && Object.keys(extra).forEach(key => {
            n[key] = extra[x]
        })
    }
    return this.$cmd.findOne(obj)
}
db.$cmd.findOne({ dbstats: 1 }) // 相当于db.stats()
db.$cmd.findOne('dbstat') // 同上

db.$cmd.findOne({ collstats: 'users' })

所以, 数据库shell命令其实就是在特殊集合$cmd上面的查询 也就是说, 可以在Driver里面通过runCommand运行MongoDB的命令啦