多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MongoDB入门到实战:安装配置、增删改查与聚合查询安全指南

MongoDB入门到实战:安装配置、增删改查与聚合查询安全指南 1. 先搞清楚MongoDB是什么再动手不少人在命令行敲下mongod看到花花绿绿的日志时心里其实还在嘀咕MongoDB到底跟MySQL差在哪里如果带着MySQL的惯性思维去学MongoDB十个有八个会卡在表结构怎么设计这个问题上。我的建议是先把脑子里那套关系型数据库的框架放一放MongoDB的底层逻辑完全不同它不是表而是集合不是行而是文档。MongoDB属于NoSQL家族里的文档型数据库核心存储单位是BSON格式的文档说白了就是JSON的二进制变体。每个文档可以拥有不同的字段同一个集合里不会强制要求结构一致这正是它在面对快速变化的需求时比MySQL灵活的地方。你不需要预先定义表结构不需要写CREATE TABLE插进去的数据长什么样它就长什么样。这篇内容适合谁如果你是刚接触MongoDB的初学者或者在公司里被迫接手一个MongoDB项目但之前只用过MySQL这篇文章能帮你把安装、配置、增删改查、查询、聚合、安全这一整条链路走通。如果你连MongoDB都没装过那更没问题我们从环境搭建开始讲把我踩过的坑顺便也指给你看。另外提一句很多人接触MongoDB是从头歌平台的任务开始的包括初识MongoDBMongoDB数据库基本操作MongoDB数据库安全这类题目。这些练习任务本质上就是在倒逼你动手去敲命令和我下面要讲的实操路子完全一致。所以不管你是为了提高课程分数还是为了干活看这篇都够用。2. 安装部署真机实操与踩坑记录2.1 Linux环境安装MongoDB 4.4/6.x的完整套路安装MongoDB我首选Linux服务器原因很简单生产环境基本都是Linux而且Linux下的安装方式比Windows干净得多。这里以CentOS 7/Ubuntu 20.04为例讲一套通用的安装流程。在CentOS上先配置MongoDB官方yum源。注意MongoDB从4.4之后版本号跳动较大每个大版本有独立的源配置。比如安装4.4版本需要创建/etc/yum.repos.d/mongodb-org-4.4.repo文件写入[mongodb-org-4.4] nameMongoDB Repository baseurlhttps://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/4.4/x86_64/ gpgcheck1 enabled1 gpgkeyhttps://www.mongodb.org/static/pgp/server-4.4.asc然后执行yum install -y mongodb-orgUbuntu/Debian系统则需要先导入公钥再添加源操作稍有不同但思路一致。装完之后先别急着改配置直接用默认配置启动一次试试systemctl start mongod systemctl status mongod如果启动成功mongo命令就能连上默认的localhost:27017。但这里有个新手最容易踩的坑CentOS默认开启了SELinux可能会拦截MongoDB的日志文件读写导致启动时报权限错误。我当时排查了半天最后发现是SELinux的问题临时关闭或者设置正确上下文才能解决。还有一个版本选择上的建议如果只是学习装4.4或者6.0都行如果是装Ops Manager这类监控管理工具那必须卡死对应的版本要求。热搜词里出现安装mongodb ops manager4.4说明不少人正在用4.4版本搭建Ops Manager。Ops Manager本质上是MongoDB官方提供的可视化运维平台它本身也依赖一个MongoDB实例来存配置数据。装它之前先确认目标MongoDB版本是4.4.x否则后续会出现兼容性报错。2.2 Windows环境安装与绿色版小技巧Windows下安装MongoDB其实更简单官方提供了MSI安装包一路点Next就行。但需要注意两个选择一是安装时把Install MongoDB Compass勾上虽然Compass后面也可以单独装但一次性搞定省事二是安装路径尽量别带空格和中文之前有人在C:\Program Files下装完配置文件的路径解析各种反人类。Windows安装完默认是注册成Windows服务的服务名是MongoDB启动方式直接net start MongoDB。但是我自己实测下来更推荐用绿色版的方式直接下载zip解压包解压到一个目录比如D:\mongodb4.4然后在目录下建好data\db和data\log两个文件夹启动时用命令行mongod --dbpath D:\mongodb4.4\data\db --logpath D:\mongodb4.4\data\log\mongod.log这种方式的好处是随时可以启动多个不同版本的实例测试时切换版本只需要换一个解压目录就够了。很多mongodb安装失败的问题其实是服务方式安装时权限或路径出问题用绿色版基本能绕开这些坑。2.3 启动失败与fassert()报错的真相安装过程中最让人崩溃的错误之一是日志里出现fassert()相关的字样。搜mongodb fassert()的帖子特别多这里我说明一下fassert()是MongoDB源码里的一个断言函数当进程内部检测到致命状态时会调用它终止进程所以看到Fatal Assertion或者fassert说明MongoDB主动自杀了而不是系统崩溃。最常见的触发场景有两个。一个是存储引擎的wiredTiger元数据损坏多数是上次非正常关机导致的日志里会出现类似WiredTiger (13) Permission denied或者Operation not permitted的行。另一个是版本不兼容比如数据文件是由更高版本的MongoDB创建的低版本启动时也会触发断言。遇到这种问题别慌先确认三条信息当前用的MongoDB版本、数据目录里dbpath的完整路径是否可写、上次关闭是否正常。如果确认是异常关机导致的备份好整个data/db目录后可以尝试用mongod --repair来修复注意修复必须在数据文件版本对应的MongoDB版本下进行否则越修越坏。实在不行直接换一个干净的数据目录重新初始化这是最快但也是最后的手段。3. 数据库基本操作增删改查一次讲透3.1 连接、建库、建集合的本质MongoDB不需要手动创建数据库。输入use testdb表面上只是切换到了一个叫testdb的上下文实际上只有当你在里面写入第一条数据时这个数据库才会真正被创建出来。这个延迟创建的机制让许多新手误以为我没有建库。同理集合也不需要预先创建db.test.insertOne({...})执行完test集合就自动出现了。连接方式上最简单的就是启动mongo客户端然后直接回车默认连接localhost的27017端口。指定主机和端口可以这样mongo 192.168.1.10:27017/mydb -u username -p password --authenticationDatabase admin这里提一个细节-u和-p后面如果带数据库名是指定要操作的库如果没带MongoDB会先从admin库做认证。很多MongoDB连接不上认证失败的问题都是因为指定的认证库不对。查看当前数据库使用db命令列出所有数据库用show dbs查看当前库所有集合用show collections。其中show dbs有个特点刚插入数据但内存还没落盘的数据库可能不显示需要等一会儿或者触发一次db.fsyncLock()这类强制刷盘操作。但实际开发中很少用到后者知道有这回事就行。3.2 插入与查询入门插入操作优先推荐insertOne和insertMany两个方法。回到头歌MongoDB数据库基本操作这类题目其实考的就是这两个方法的正确调用以及后面要讲的find。db.users.insertOne({ name: zhang, age: 28, city: shanghai, tags: [java, python] }) db.users.insertMany([ { name: li, age: 22, city: beijing, tags: [go] }, { name: wang, age: 30, city: shanghai, tags: [java, mongo] } ])查询最基本的方法是find()它返回一个游标对象。很多人直接敲db.users.find()发现结果一屏装不下就开始慌其实加个.pretty()就能格式化显示。如果只想看一条用findOne()。条件查询直接用JSON对象表示。比如查城市是上海的用户db.users.find({ city: shanghai }).pretty()查询年龄大于25的db.users.find({ age: { $gt: 25 } })这里$gt是MongoDB查询操作符对应大于。类似的还有$lt小于、$gte大于等于、$lte小于等于、$ne不等于、$in在列表内、$nin不在列表内。这些操作符是后续一切复杂查询的积木必须记熟。3.3 更新与删除的细节更新操作是重灾区。新手最容易犯的错误是只用update不带任何更新操作符把整个文档替换掉比如把{name: zhang, age: 28, city: shanghai}直接更新成{name: zhang, age: 29}结果city和tags全没了。正确的做法是使用$set操作符只更新指定字段db.users.updateOne( { name: zhang }, { $set: { age: 29 } } )updateOne只更新匹配到的第一条updateMany更新所有匹配到的文档。如果需要有则更新无则插入用upsert: true选项db.users.updateOne( { name: zhao }, { $set: { age: 26, city: guangzhou } }, { upsert: true } )删除操作同样分deleteOne和deleteMany。注意db.users.remove({})这种旧写法能清空整个集合但现在已经不推荐了而且如果集合极大remove会非常慢。清空整个集合正确的方式是db.users.deleteMany({})想彻底释放存储空间则用db.users.drop()直接删掉集合。4. 查询语句进阶嵌套list到底怎么查4.1 比较、逻辑与正则操作符组合出招项目中真正用得多的是多个操作符组装在一起的复合查询。比如查年龄在20到30之间且城市是上海或北京的Java开发者db.users.find({ age: { $gte: 20, $lte: 30 }, city: { $in: [shanghai, beijing] }, tags: java }).pretty()这里tags: java这种写法如果tags字段是一个数组MongoDB会自动匹配数组里包含java的文档这就是数组字段的默认查询行为不需要额外使用$elemMatch。很多人一开始不知道这一点以为数组查询必须特殊处理其实普通相等匹配就能覆盖大部分场景。逻辑操作符$and、$or、$not、$nor则负责组合多个条件。比如查年龄小于20或者城市是上海且年龄小于35db.users.find({ $or: [ { age: { $lt: 20 } }, { city: shanghai, age: { $lt: 35 } } ] })正则查询用$regex比如查所有名字以zh开头的用户db.users.find({ name: { $regex: ^zh } })实际开发中正则查询通常和索引配合使用否则全表扫描很慢。前缀正则^zh恰好能用普通索引优化但如果正则写成zh$结尾匹配索引就失效了。知道这个区别能帮你写出性能更稳的查询。4.2 嵌套文档与嵌套数组的查询方法mongodb怎么查list嵌套list是热搜词里很有代表性的一类问题。先看一个典型的嵌套数组场景每个用户的文档里有一个orders字段里面放多笔订单每笔订单又是一个数组或者嵌套对象db.users.insertOne({ name: zhang, orders: [ { id: 1001, items: [{ sku: a, price: 10 }, { sku: b, price: 20 }] }, { id: 1002, items: [{ sku: c, price: 30 }] } ] })要查orders里包含id为1001订单的用户直接用点号路径就行db.users.find({ orders.id: 1001 })注意点号路径的字段名必须加引号不能写成orders.id不带引号字符串里带引号是正确姿势。这里orders是数组MongoDB会扫描数组里的每一个元素看元素的id字段是否等于1001这是数组内嵌文档的默认匹配行为。如果你要查至少有一笔订单里items存在price大于20的商品光靠点号路径不行因为会导致数组元素之间字段错位匹配的问题。必须用$elemMatchdb.users.find({ orders: { $elemMatch: { items.price: { $gt: 20 } } } })$elemMatch要求整个内部的匹配逻辑必须落在同一个数组元素上这是嵌套list查询里最重要的一个操作符。很多查嵌套list嵌套list的问题归根结底就是没用对$elemMatch结果查出来的数据要么漏了要么多了。4.3 用$unwind展开嵌套数组后再查还有一种思路遇到多层嵌套又怕匹配逻辑复杂可以先把嵌套数组用$unwind展开成一个个独立的临时文档再结合$match查询。例如db.users.aggregate([ { $unwind: $orders }, { $unwind: $orders.items }, { $match: { orders.items.price: { $gt: 20 } } }, { $project: { name: 1, orders: 1 } } ])这段管道的效果是先把每个用户的orders数组逐条展开再把每笔订单的items逐条展开之后$match只保留符合条件的行。这个写法在调试阶段尤其好用因为你能直观地看到展开之后每一行的结构不会像$elemMatch那样有到底匹配没匹配的困惑。要注意的是$unwind展开后一个用户会变成多行后续如果要做去重统计可以再用$group按_id收拢回来。这个是聚合操作的范围了正好引出下一节。5. 聚合函数查询统计group by和解5.1 聚合管道的核心概念MongoDB的聚合框架大致对应SQL里的GROUP BY、HAVING、ORDER BY、LIMIT但执行方式完全不同。它不是一条语句搞定而是把一堆操作串成管道前一个操作的结果继续喂给后一个操作去处理。mongodb之聚合函数查询统计这类学习任务本质就是让你把这张管道图理顺。管道里的每个阶段用一个JSON对象表示。最常用的四个节点是$match负责过滤$group负责分组统计$project负责字段重塑$sort负责排序。把这些节点串起来就完成了一段聚合查询。要理解聚合最直观的方式是从一个按城市统计用户数量和平均年龄的需求入手db.users.aggregate([ { $match: { age: { $gt: 20 } } }, { $group: { _id: $city, count: { $sum: 1 }, avgAge: { $avg: $age } } }, { $sort: { count: -1 } } ])这段的含义是先只保留年龄大于20的用户然后按city分组$sum: 1表示每出现一个文档计数加1$avg: $age表示求组内age字段的平均值。注意字段名前带$符号用来表示引用文档里的字段。如果漏了$MongoDB会把city当成字符串常量结果全部分成同一组这是最常见的聚合翻车点。5.2 $group、$sum、$avg、$match组合实战在$group节点里_id是必须指定的分组键其他字段都是统计结果。_id可以是单字段也可以是一个嵌套对象实现多字段分组。比如统计每个城市里每个年龄段的人数db.users.aggregate([ { $group: { _id: { city: $city, ageGroup: $age }, count: { $sum: 1 } } } ])这个查询会把城市和年龄完全相同的用户归为一组。分组键是复合结构时结果中的_id也会变成一个子文档。$sum: 1之外还可以用$sum: $price实现对字段值的累加。比如统计每个用户订单总金额db.users.aggregate([ { $unwind: $orders }, { $group: { _id: $name, total: { $sum: $orders.price } } } ])这里如果orders的每个元素有price字段先unwind把每个订单变成独立文档再按name分组求和。注意如果订单嵌套更深比如price在orders.items.price里就需要先两次unwind再在分组前用$sum配合$multiply等方法处理。$match放在聚合开头和放在中间效果不一样。放在开头可以提前过滤掉大量不相关的文档节省后面阶段的开销类似SQL中WHERE优先执行的优化思路放在中间则可以过滤掉某些分组计算后的中间结果比如只保留总数大于5的城市这种功能类似SQL里的HAVING但实现方式更灵活。5.3 用$project重塑输出结果聚合查出来的结果往往有不少中间字段直接输出比较乱。$project节点专门负责选择输出哪些字段以及怎么加工。比如上面的复合分组结果想输出成城市、年龄段、人数三个平铺字段db.users.aggregate([ { $group: { _id: { city: $city, ageGroup: $age }, count: { $sum: 1 } } }, { $project: { _id: 0, city: $_id.city, ageGroup: $_id.ageGroup, count: 1 } }, { $sort: { count: -1 } } ])_id: 0表示不输出_id字段city: $_id.city表示从嵌套的_id对象里取city字段重新赋值给结果里的city。count: 1表示原样保留。这套字段重塑的逻辑和JSON对象操作很像一旦理解了复杂的聚合结果想怎么展示就怎么展示。6. 数据库安全配置从裸奔到及格6.1 开启认证的完整过程很多教程在你第一次启动MongoDB时根本没提安全这回事导致默认安装完就是一个没有任何认证的实例谁连上27017端口谁就能读写全部数据。这在公司内网还好说一旦端口映射到公网基本等于裸奔。网上搜到mongodb数据库安全相关题目核心诉求就是让你学会开启访问控制和认证。先看现在常见做法。启动MongoDB后先连上admin库创建一个管理员用户use admin db.createUser({ user: root, pwd: your_strong_password, roles: [ { role: root, db: admin } ] })这里root角色是MongoDB里的超级管理员能管理所有库。创建完用户后修改MongoDB配置文件/etc/mongod.conf添加或修改security段security: authorization: enabled然后重启mongodsystemctl restart mongod重启后再执行mongo直接回车就会连不上并提示需要认证这是正常的。必须通过mongo -u root -p your_strong_password --authenticationDatabase admin才能进入。这个认证库的概念非常关键用户是在哪个库下面创建的连接时就必须指定那个库做认证。如果你已经创建了用户才发现没有开启认证也不需要删数据重建。只要按上面步骤创建用户、改配置、重启就能平滑开启访问控制。实测下来这个流程是最稳妥的不会出现开启认证后原有数据不可访问的情况。6.2 角色权限设计要点实际项目中不太建议所有应用都用一个root账号。MongoDB的角色体系把权限分得很细比如read只允许读readWrite允许读写dbAdmin负责管理索引和集合clusterAdmin管集群层面。最小权限原则在这里同样适用一个业务服务只需要读写某个库就单独创建一个绑定该库的用户use myapp db.createUser({ user: app_user, pwd: app_password, roles: [ { role: readWrite, db: myapp } ] })这里特别注意创建这个用户时所在的use库和roles里面db字段指定的库两者可能不一样。认证时MongoDB去myapp库里找用户信息这也是新手经常搞混的地方。如果创建时没有指定db默认使用当前use的库之后连接时认证库就要填一样的库名。安全配置做到及格线至少应该包括开启authorization: enabled禁用或者关闭bindIp对外网开放给所有库的业务账号分配最小角色定期备份。如果公司要求更高还可以上企业版的字段级加密或者搭配Ops Manager做自动备份和监控但那些都是后话。6.3 头歌安全类练习题的答题思路平台上那些MongoDB数据库安全的练习出题套路比较固定先创建一个有权限的用户然后让你在未认证和已认证两种状态下分别验证能否读写最后判断授权是否正确。这类题本质上是检验你对db.createUser、roles、--authenticationDatabase这几个知识点的掌握程度。我建议做这类练习时别急着背命令把流程自己完整走一遍先创建用户再重启服务再用新用户连接最后尝试跨库访问一个没有权限的库看看是否被拒。亲手验证过一遍权限模型比刷十道题都管用。做题时如果某一步连接失败优先检查认证库是否写对八成的问题都出在这里。7. 常见问题排查实录7.1 安装失败的高频原因与对策mongodb安装失败这个热搜词下面我能想到的典型原因大概五类。第一yum源里没有对应版本多半是baseurl里$releasever解析出的系统版本和官方源不匹配解决办法是手动改成7.0之类的固定值。第二依赖包冲突安装时报libcrypto.so.10这类找不到库的错误多数是老系统需要更新openssl或libssl版本。第三磁盘空间不足但日志提示不明显检查一下/var/lib/mongodb所在分区的剩余空间。第四SELinux拦截前面已经提过。第五端口被占用27017端口已经被别的进程占着启动就会报address already in use。排查顺序我建议是先看日志再看端口再看磁盘最后考虑源的问题。MongoDB的日志一般写在/var/log/mongodb/mongod.log用tail -100拉出最后一百行大多数错误原因都能直接看到。7.2 连接失败、认证失败与超时的区分连接失败和认证失败是两个完全不同的问题但新手经常混在一起。连接失败是根本到不了MongoDB表现为connect timed out或者connection refused认证失败是连上了但要密码时密码不对表现为Authentication failed。定位思路也不同连接失败先查网络用telnet 192.168.1.10 27017测试端口通不通认证失败先查用户名、密码和认证库。还有一类情况是连接超时原因可能是机器负载太高、网络丢包、或者MongoDB开启了/etc/hosts解析异常。我的经验是连接超时优先看服务器CPU和负载尤其是有慢查询在跑的时候MongoDB可能连握手都顾不上。如果CPU不高但依然超时再检查防火墙和云安全组规则。7.3 聚合查询慢或内存爆掉的处理心得聚合管道跑起来慢通常有两种情况。一种是没有充分利用索引最常见是$match过滤条件没有落到索引上导致全表扫描。做法是先用db.collection.createIndex()给过滤字段建好索引再在管道里把$match尽量放前面。另一种是$unwind展开后数据膨胀太厉害展开了几百万行后面的$group内存根本顶不住。MongoDB聚合默认有100MB的内存限制超过了会报错。解决办法有两种一是加allowDiskUse: true允许使用磁盘临时文件比如db.users.aggregate([...], { allowDiskUse: true })二是从业务上拆分先缩小数据范围再做聚合。我的建议是优先做业务拆分磁盘排序带来的性能损耗在数据量大时非常明显能避免就避免。还有个小技巧聚合结果如果只需要一部分字段不要把整条文档都推给后面的阶段处理在$project或者$match阶段就把不需要的字段扔掉能显著减轻后续阶段的压力。这也是mongodb之聚合函数查询统计里经常考到的优化点。8. 最后分享几个实战中的小习惯个人经验是MongoDB学习曲线最陡的地方不在语法而在思维转换。SQL里你习惯了一堆表join一起查MongoDB里你更倾向于把相关联的数据直接嵌套在同一个文档里。嵌套越深查询越灵活但更新代价也越大。实际项目里文档设计得不好后面查询和聚合全都跟着遭殃。我给自己定的几条规矩集合里的文档结构尽量保持稳定即使字段允许不一致也别真的让它五花八门所有查询条件涉及的字段尤其在集群环境里一定要建索引能用updateOne就别用update能写$set就绝不整文档替换。每次踩完坑我都把原因记下来后来发现80%的问题都是同一个套路要么没用索引要么嵌套太深还硬用点号路径查。如果你现在正卡在某个mongodb安装失败或者查询嵌套list的问题上先按照上面第2节和第4节的方法走一遍大概率能解决。实在不行把报错信息里最关键的一行贴到搜索引擎里比瞎改配置有效率得多。MongoDB的生态已经很成熟官方文档也写得很详细多动手验证几次比背多少篇教程都强。
返回列表