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:把数组(同一组)的数据,进行运算。