Redis 和 Memcached 是 kv 数据库,mongodb 是文档数据库,存储的是文档(Bson->Json的二进制化)。

安装

建议官网直接下载二进制包,解压后即可使用。

bin 目录下的二进制程序介绍如下:

mongo: 交互终端(客户端)
mongod: 数据库核心进程
mongos: 查询路由器,集群时用

mongoexport: 导出json,csv,tsv格式
mongoimport: 导入json,csv,tsv
mongodump: 官方备份工具,它可以从 MongoDB 数据库读取数据并生成 BSON 文件
mongorestore: 导入 mongodump 产生的 BSON 文件
bsondump: 将bson转换为json格式

诊断工具:
mongostats
mongotop
mongosniff

启动

# mongod --dbpath /data/mongodb/data --logpath /var/log/mongodb.log --fork

--bind_ip_all
--bind_ip <address>

客户端连接

# mongo [--host 1.1.1.1] [--port xxxx]

建库建表

> show dbs/databases;
admin   0.000GB
config  0.000GB
local   0.000GB

> use admin;
switched to db admin

> show collections/tables;
system.version

> db.system.version.find();
{ "_id" : "featureCompatibilityVersion", "version" : "4.2" }

建库:mongodb 中数据库是隐式创建的,可以直接 use,然后创建 collection,当库中有对象时,自动创建库。
建表:可以手动创建,也可以通过向一个不存在的表中直接 insert 隐式建表。

> use demodb;    // demodb 并不存在
> db.createCollection('user');        // 显式创建一个表
> db.emp.insert({name:'cy',age:22});    // 也可以直接向一个不存在的表中插入记录,表自动创建

删表删库:

> db.emp.drop();    // 删除 collection,表一个库中没有对象时,会自动删除库
> db.dropDatabase();    // 手动删除数据库

增删改查

INSERT

可以插入单条记录:

> db.user.insert({name:'lisi',age:22});
> db.user.find();

> db.user.insert({_id:2,name:'cy',salary:1200});
> db.user.find();

也可以通过数组一次插入多条记录:

> db.user.insert([{name:'xuw',age:32},{name:'lisi',salary:14000}]);

注意:mongodb 会为每条记录分配一个 _id 列,该列可以不指定值(自动生成),也可以手动指定。

REMOVE

语法:

db.collection.remove(query, justOne)

query:查询表达式,相当于 where 限定[重点]
justOne: boolean, true/false, 是否只删1条

示例:

> db.user.remove({name:'xuw'});
> db.user.remove({name:'xuw'},{justOne:true})

UPDATE

语法:

db.collection.update(query, update[, options])

query: 查询表达式
update: 新document
options: {upsert:true/false,multi:ture/false}

upsert,默认为false, 作用:无相应记录是否insert,与mysql中的replace同
multi,默认为false, 作用:是否作用于多条

示例:

> db.emp.update({name:'cfop'},{name:'xuwang'},{upsert:true});
> db.cydb.update({name:'lisi'},{name:'zhangsan'});    没有使用 set,把整条记录修改为指定值
> db.cydb.update({name:'lisi'},{$set:{name:'zhangsan'}});    使用了 set 后,只是把 name 属性进行修改

$set 表示设置字段的新值。

除了 $set,还有如下常用的运算符:
$unset:删除指定的列,例如:db.emp.update({name:'xuw'},{$unset:{'salary':''}})
$rename:重命名列,例如:db.emp.update({name:'xuw'},{$rename:{'age':'salary'}})
$inc:增长值,例如:db.emp.update({name:'cy'},{$inc:{'salary':2}})
$setOnInsert:当upsert时,设置字段的值,例如:db.emp.update({name:'cfop'},{$setOnInsert:{salary:40}},{upsert:true});

FIND

语法:

db.collections.find(query,{列1:1,列2:0})  <-- 通过1和0控制是否展示某列

示例:

> db.cydb.find()
> db.cydb.find({},{name:1})        <-- 只显示 _id 列和 name 列
> db.cydb.find({},{name:1,_id:0}) <-- 只显示 name 列,不显示 _id 列

查询表达式

简单的查询表达式

> db.cydb.find();
> db.cydb.find({salary:{$ne:3000}});  表示 salary <> 3000,同理可换成 gt, lt, gte, lte 等
> db.cydb.find({salary:{$in:[10000,12000]}});    同理可以使用 $nin,表示 not in 
> db.cydb.find({hobby:{$all:['running','walking']}});  $all 表示和数组所有单元匹配,这儿表示找出所有兴趣中包含 runing 和 walking 的记录
> db.cydb.find({salary:{$mod:[500,0]}})   满足某求余条件则为真,salary/500,余0的
> db.cydb.find({salary:{$exists:1}})    某列存在则为真,这儿表示找出存在salary列的记录
> db.cydb.find({salary:{$type:1}})  数据为某类型则为真,这儿表示 salary 类型是 1 的记录,1 表示 double 数字,更多类型代码参见官方文档

组合表达式

AND

工资大于3000:{salary:{$gt:3000}}
工资小于15000:{salary:{$lt:15000}}

使用 and 拼起来:{$and:[A,B]},即:
db.cydb.find({$and:[{salary:{$gt:3000}},{salary:{$lt:15000}}]})

$or 与 $nor 的使用方法类似,其中 $nor 表示"所有列举条件都不满足则为真"。

where 与 regex

$where js表达式为真则为真
$regex 正则表达式匹配则为真

需要注意的是,这两个运算符必须把二进制格式转换为 json,然后让 js 调用,性能会下降,但好处是可以把条件写得非常灵活复杂。

示例:

> db.cydb.find({$where:'this.salary > 10000'});
> db.cydb.find({name:{$regex:/^zh.*/}});

批量循环

> for (var i=1; i<=1000; i++) {
db.emp.insert({_id:i,salary:4000+i, age:10+i});
};

> db.emp.find().count();    // 返回 1000

游标操作

遍历方法1:

var mycursor = db.emp.find();
while (mycursor.hasNext()) {
    printjson(mycursor.next()); 
}

遍历方法2:

var mycursor = db.emp.find();
mycursor.forEach(function(obj) {printjson(obj)});

取数时,我们可以通过 skip 和 limit 来实现翻页效果:

var mycursor = db.emp.find().skip(995).limit(2);

如果没有 limit,则从 skip 处一直取到最后。

查看执行计划

> db.emp.find({salary:4100}).explain();

索引

查看表上当前索引:

> db.emp.getIndexes();    

创建单列索引:

> db.emp.ensureIndex({salary:1});

表示在 salary 列上创建索引,1表示正序排序(默认),-1表示倒序排列。

创建多列索引:

> db.emp.ensureIndex({salary:1,age:1});

删除索引:

> db.emp.dropIndex({salary:1})
> db.emp.dropIndexes();        <-- 删除所有自建索引(ID上的自动索引除外)

重建索引

> db.emp.reIndex()

唯一索引:

> db.emp.ensureIndex({salary:1},{unique:true});

子文档索引:

db.shop.insert({name:'Nokia',spc:{weight:120,area:'chengdu'}});
db.shop.insert({name:'Xiaomi',spc:{weight:110,area:'Beijing'}});

现需要查询地区是成都的记录?
> db.shop.find({'spc.area':'chengdu'});

给子文档加索引:
> db.shop.ensureIndex({'spc.area':1});

哈希索引:
哈希索引是将键通过内部的 hash 函数决定存放位置,是随机的。
对于机械硬盘来说,随机性能差,哈希索引对于范围查询不友好。

> db.emp.ensureIndex({salary:'hashed'})

稀疏索引:
针对某 field 做索引,如果某些文档不含有 field 列。
对于普通索引,会把该文档的 field 列的值认为 NULL,并建索引。
对于稀疏索引,将不会为该文档建立索引(忽略)。
根据{field:null}来查询,普通索引能查到,而稀疏索引则不能。

> db.emp.ensureIndex({salary:1},{sparse:true});

用户管理

查看当前用户:

> show users;

注意:用户是根数据库关联的,use 不同的数据库后, show users 会看到不同的结果。

为 admin 库创建用户:

> use admin
> db.createUser({user:'admin',pwd:'oracle',roles:[{role:'root',db:'admin'}]});     <-- 角色如果是空数组表示不分配角色

> show collections;
system.users    <-- 多出来的
system.version

Built-In Roles(内置角色):

  • 数据库用户角色:read、readWrite; 为某个数据库创建一个用户, 分配该数据库的读写权力
  • 数据库管理角色:dbAdmin、dbOwner、userAdmin;拥有创建数据库, 和创建用户的权力
  • 集群管理角色:clusterAdmin、clusterManager、clusterMonitor、hostManager;管理员组, 针对整个系统进行管理
  • 备份恢复角色:backup、restore;备份数据库, 还原数据库
  • 所有数据库角色:readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、dbAdminAnyDatabase; 拥有对admin操作的权限
  • 超级用户角色:root; 另外,dbOwner 、userAdmin、userAdminAnyDatabase 这几个角色间接或直接提供了系统超级用户的访问
  • 内部角色:__system

删除用户:

> db.dropUser('admin');

认证登录:

之前启动的 mongodb 是不需要认证的,加上 --auth 选项重新启动 mongod 打开认证。

# ./mongo 能连接成功,但是无法执行命令
> db.auth('admin','oracle');    认证后就可以执行命令了
# ./mongo -uadmin -poracle    <-- 也可以直接使用用户名和密码登录

给其它库添加用户:

> use cydb;
> db.createUser({user:'cydb',pwd:'oracle',roles:[{role:'readWrite',db:'cydb'}]});
> show tables;

修改用户密码:

> db.changeUserPassword('cydb','redhat');

数据导出导入

导出

语法:

mongoexport -h host -p port -u username -p passwd

-d database 要导出的库
-c collection 要导出的collection
-f 要导出的列
-q 查询条件(用引号包起来)
-o 导出文件名
--csv 导出csv格式

示例:

mongoexport -d cydb  -c emp -f salary -o /tmp/cydb.emp   <-- id列始终会被导出

导入

语法:

mongoimport -h host -p port -u username -p passwd

-d database 要导出的库
-c collection 要导出的collection
--type csv/json(默认)
--file 待导入文件
--headerline 跳过第1行

示例:

mongoimport -d cydb -c emp --type json --file /tmp/cydb.emp 

注意:导入 csv 文件时,要通过 -f col1,col2 来指定文件对应的列名,并且通过 --headerline 来跳过第一行。

二进制导出导入

导出

语法:

mongodump -h host -p port -u username -p passwd

-d database 要导出的库
-c collection 要导出的collection
-q 查询条件(用引号包起来)
-o 导出文件路径

示例:

mongodump -d cydb -c emp

默认会导出在当前目录下的 dump/dbname/ 目录下,.bson是数据文件,.json是索引定义。

导入

语法:

mongorestore  -h host -p port -u username -p passwd

-d database 要导入的库
-c collection 要导入的collection
--dir 库的备份目录
--drop 导入前清空旧数据

示例:

mongorestore -d cydb  --dir dump/cydb/ --drop

replication set

准备目录:

# mkdir /data/mongodb/data{1,2,3}

启动实例:

mongod --dbpath /data/mongodb/data1 --logpath /var/log/mongodb1.log --fork --port 27017 --bind_ip_all --replSet rsdemo
mongod --dbpath /data/mongodb/data2 --logpath /var/log/mongodb2.log --fork --port 27018 --bind_ip_all --replSet rsdemo
mongod --dbpath /data/mongodb/data3 --logpath /var/log/mongodb3.log --fork --port 27019 --bind_ip_all --replSet rsdemo

这3个实例都在同一个复制集,它们会互相通信(包括两个从节点之间)。

创建配置:
随便登录复制集中的一个节点

> use admin
> var rsconf = {
_id:'rsdemo',
members:[
    {
    _id:0,
    host:'192.168.111.128:27017'
    },
    {
    _id:1,
    host:'192.168.111.128:27018'
    },
    {
    _id:2,
    host:'192.168.111.128:27019'
    }
    ]
}

> printjson(rsconf);
{
    "_id" : "rsdemo",
    "members" : [
        {
            "_id" : 0,
            "host" : "192.168.111.128:27017"
        },
        {
            "_id" : 1,
            "host" : "192.168.111.128:27018"
        },
        {
            "_id" : 2,
            "host" : "192.168.111.128:27019"
        }
    ]
}

初始化:

> rs.initiate(rsconf)

查看复制集状态:

rsdemo:PRIMARY> rs.status()

删除节点:

rsdemo:PRIMARY> rs.remove('192.168.111.128:27019')

可以 remove 掉所有 secondry.

增加节点:

rsdemo:PRIMARY> rs.add('192.168.111.128:27019')

此时,复制集已经创建好,但是从仅与主通信,不能直接使用,如果要使用 slave,在要使用的slave上执行 rs.slaveOk(),然后在 slave 上就可以看到数据了。

自动故障迁移:

rsdemo:PRIMARY> db.shutdownServer()

rs.status() 可以看到,原主不可达,27018已经成为新主。

shard 分片

client ---> mongos 路由器 ---> configsvr ---> shardN(分片节点)
configsvr 不存储真正的数据,存储的是 meta 信息,即某条数据在哪个片上的信息。分片规则存放在 configsvr上。

产生两个分片实例

mkdir /data/mongodb/shard{1,2}
mongod --shardsvr --dbpath /data/mongodb/shard1 --logpath /var/log/mongodb_shard1.log --fork --port 27017 --bind_ip_all
mongod --shardsvr --dbpath /data/mongodb/shard2 --logpath /var/log/mongodb_shard2.log --fork --port 27018 --bind_ip_all

也可以是复制集:

mkdir /data/mongodb/shardset{1,2,3,4,5,6}   123做一个复制集,456做一个复制集

mongod --shardsvr --dbpath /data/mongodb/shardset1 --logpath /var/log/mongodb_shardset1.log --fork --port 27014 --bind_ip 192.168.111.128 --replSet shardset1
mongod --shardsvr --dbpath /data/mongodb/shardset2 --logpath /var/log/mongodb_shardset2.log --fork --port 27015 --bind_ip 192.168.111.128 --replSet shardset1
mongod --shardsvr --dbpath /data/mongodb/shardset3 --logpath /var/log/mongodb_shardset3.log --fork --port 27016 --bind_ip 192.168.111.128 --replSet shardset1

> var shardset1 = {
_id:'shardset1',
members:[
    {
    _id:0,
    host:'192.168.111.128:27014'
    },
    {
    _id:1,
    host:'192.168.111.128:27015'
    },
    {
    _id:2,
    host:'192.168.111.128:27016'
    }
    ]
}

> printjson(shardset1);
{
    "_id" : "shardset1",
    "members" : [
        {
            "_id" : 0,
            "host" : "192.168.111.128:27014"
        },
        {
            "_id" : 1,
            "host" : "192.168.111.128:27015"
        },
        {
            "_id" : 2,
            "host" : "192.168.111.128:27016"
        }
    ]
}

初始化

> rs.initiate(shardset1)

同样的方法将4,5,6做成复制集。

configsvr 实例:
MongoDB 3.4, config servers must be deployed as a replica set (CSRS).

mkdir /data/mongodb/configsrv{1,2}
mongod --configsvr --dbpath /data/mongodb/configsrv1 --logpath /var/log/mongodb_configsrv1.log --fork --port 27020 --bind_ip 192.168.111.128 --replSet rsconf
mongod --configsvr --dbpath /data/mongodb/configsrv2 --logpath /var/log/mongodb_configsrv2.log --fork --port 27021 --bind_ip 192.168.111.128 --replSet rsconf

任意连接一个 configsvr 节点,进行初始化:

# mongo --host 192.168.111.128 --port 27020
> rs.initiate()        <--配置文件是可选,没有则使用默认的
rsconf:PRIMARY> rs.status()        <--检查,如果没有从节点,则手动添加进来

mongs 实例:

# mongos --logpath /var/log/mongodb_mongos.log --port 30000 --bind_ip_all --configdb rsconf/192.168.111.128:27020,192.168.111.128:27021 --fork

现在,mongo 就是连接 mongos 节点,将两个片加入到配置服务器。

# mongo --host 192.168.111.128 --port 30000
> sh.addShard('192.168.111.128:27017');
> sh.addShard('192.168.111.128:27018');
> db.runCommand({addshard :"192.168.111.128:27017"});
> db.runCommand({addshard :"192.168.111.128:27018"});

> db.runCommand({addshard :"shardset1/192.168.111.128:27014,192.168.111.128:27015,192.168.111.128:27016"});
> db.runCommand({addshard :"shardset2/192.168.111.128:27017,192.168.111.128:27018,192.168.111.128:27019"});

mongos> sh.status()

mongos> sh.enableSharding('cydb');  允许 cydb 库分片
mongos> sh.shardCollection('cydb.emp',{name:1}) 表 emp 通过 name 列分片

注意:mongodb 不是从单篇文档的级别,绝对平均的散落在各个片上,而是N篇文档形成一个块"chunk",以 chunk 为单位优先放在某个片上,当这片上的 chunk 比另一个片的 chunk 区别比较大时,会把本片上的chunk移到其它片上,以 chunk 为单位维护片之间的数据均衡。

由此也带来一个问题:chunk 的来回折腾产生 IO 问题。
能否定义一个规则,某N条数据形成1个块,预先分配M个chunk,M个 chunk 预先分配在不同片上,以后的数据直接接入各自分配好的 chunk,不再来回移动。能,预先手动分片。

手动预先分片

如果要使用手动预先分片,要对业务规模有一个的规划,比如,一年的数据。

在前面对表按照某列分片的基础之上,执行如下命令:

for (var i=1; i<=40; i++) {
sh.splitAt('shop.user',{userid:i*1000})
}

这儿表示表示在 1K,2K,....40K 这样的界线切好 chunk(此时 chunk 是空的),这些 chunk 将会均匀地分配到各片上。
通过 mongos 添加 user 数据,数据会添加到预先分配好的 chunk 上,chunk 就不会来回移动了。

聚合与 mapReduce

分组统计:group()
简单聚合:aggregate()
强大统计:mapReduce()

db.collection.group(document) 本身没有求和/平均值等功能,需要在 group 中调用自定义函数来计算。

select count(*)from goods group by cat_id; 

db.emp.group({
key:{cat_id:1},    <-- 分组字段
cond:{},    <-- 查询条件
reduce: function(curr,result){
    result.total += 1;
},
initial:{total:0}
})

result 指代一个组, curr 指代当前行。

注意:
1: group需要我们手写聚合函数的业务逻辑
2: group 不支持shard cluster,无法分布式运算
3:分布式可以用aggregate()(version2.2),或者mapReduce()(version2.4)

简单地说, mapReduce 就是 RDBMS 中的 group,其强处在于支持分布式。

mapRecuce的工作过程:
map-->映射
reduce->归约

map:先是把属于回一个组的数据,映射到一个数组上。
reduce:把数组(同一组)的数据,进行运算。

-- By 许望(RHCA、OCM、VCP)
最后修改:2020 年 07 月 17 日 05 : 43 PM
如果觉得我的文章对你有用,请随意赞赏