MongoDB 索引、事务与性能调优

生产落地的三件套:索引、事务与调优 📖 前置阅读:本文是 MongoDB 系列的生产调优篇,假设读者已经掌握了 MongoDB 文档模型、SpringBoot 操作和聚合管道。如果还没有,建议先阅读前三篇: MongoDB 核心概念:文档模型、BSON 与查询操作符全解析 —— 介绍篇 SpringBoot MongoDB 全操作指南 —— 实战篇 MongoDB 聚合管道深入 —— 进阶篇 一、⚡ 问题切入:查询怎么越来越慢了? 用户表单数据从 10 万增长到 500 万,“按手机号查询表单记录"从 5ms 变成了 800ms。你用 explain() 一看——stage: "COLLSCAN",全表扫描。 这场景和 MySQL 一样——数据量大了没建索引,写什么数据库都快不了。但 MongoDB 的索引有一些 MySQL 没有的类型,还有 Schema 设计的决策(嵌入还是引用?要不要用事务?)直接影响性能。 本篇要解决的问题:怎么设计索引、怎么看懂 explain、怎么选嵌入还是引用、什么时候用事务——以及遇到性能问题时从哪下手排查。 二、📊 索引类型与策略 2.1 单字段索引与复合索引 底子和 MySQL 一样——B-Tree。创建一个索引语法几乎一样: // 单字段索引 db.users.createIndex({ email: 1 }) // 1 = 升序,-1 = 降序(单字段索引中不重要) // 复合索引(多个字段组合) db.orders.createIndex({ userId: 1, createTime: -1 }) // 查看所有索引 db.orders.getIndexes() 复合索引的 ESR 规则——这是 MongoDB 复合索引最重要的设计原则: ...

十月 29, 2022 · 6 分钟 · 1237 字 · yaomingye

MongoDB 聚合管道深入

聚合管道实战:从入门到精通 📖 前置阅读:本文是 MongoDB 系列的进阶篇,假设读者已经掌握了 MongoDB 文档模型和 SpringBoot 基本操作。如果还没有,建议先阅读前两篇: MongoDB 核心概念:文档模型、BSON 与查询操作符全解析 —— 介绍篇 SpringBoot MongoDB 全操作指南 —— 实战篇 一、⚡ 问题切入:查询能查出结果,但分析不出来 先看一个业务需求。你有一个电商订单 Collection: // 一条订单文档 { _id: ObjectId("..."), userId: 1001, total: NumberDecimal("6999.00"), status: "paid", items: [ { productName: "华为Mate60 Pro", category: "手机", price: NumberDecimal("6999.00"), quantity: 1 } ], createTime: ISODate("2024-01-15T10:30:00Z") } 产品经理要你给出以下数据: 每个用户的总消费金额 每月订单量趋势 销量最高的 10 个商品分类 每个用户的平均客单价 哪些商品经常一起购买(关联分析) 用 find 可以查出原始数据,但统计计算全得拉到 Java 内存里自己算——5 万条订单光加载到内存就需要 2 秒,再算就 5 秒起步。 这就是聚合管道的用武之地——把计算下推到 MongoDB 服务器端完成,只返回结果,不传原始数据。 二、🧱 聚合管道是什么 2.1 核心概念 聚合管道(Aggregation Pipeline) 是一组按顺序执行的数据处理阶段(Stage)。每个 Stage 接收上一阶段的输出,做一次数据变换,把结果传给下一阶段。 flowchart LR classDef stage fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold; classDef result fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold; COLLECTION[(orders\n50000 文档)] COLLECTION --> S1[$match\n过滤: status=paid] S1 --> S2[$group\n分组: 按 userId] S2 --> S3[$sort\n排序: 总金额降序] S3 --> S4[$limit\n取前 10] S4 --> RESULT([10 条聚合结果]) class S1,S2,S3,S4 stage; class COLLECTION data; class RESULT result; 关键点: ...

十月 28, 2022 · 8 分钟 · 1662 字 · yaomingye

MongoDB 核心概念

MongoDB 核心概念:文档模型、BSON 与查询操作符全解析 一、⚡ 问题切入:MySQL 为什么不适合这个场景? 先看一个典型的系统设计需求。你正在开发一个 SaaS 平台的"用户自定义表单"功能——每个客户可以自己创建表单,定义不同的字段: 客户A:报名表 → 姓名、手机号、紧急联系人姓名、紧急联系人电话、是否过敏(是/否) 客户B:问卷表 → 昵称、年龄、兴趣爱好(多选)、详细简历(文本)、作品链接 客户C:订单表 → 商品名、单价、数量、收货地址(省/市/区/详细)、发票抬头、纳税人识别号 用 MySQL 做这件事,摆在面前的有三条路: 方案一:一张宽表 CREATE TABLE form_data ( id BIGINT PRIMARY KEY, field_1 VARCHAR(200), -- 姓名/昵称/商品名 field_2 VARCHAR(200), -- 手机号/年龄/单价 field_3 VARCHAR(200), -- 紧急联系人/兴趣爱好/数量 -- ... 预留 50 个字段 field_50 VARCHAR(200) ); 客户A的"紧急联系人电话"存在 field_4,客户B的"作品链接"存在 field_5,客户C的"纳税人识别号"存在 field_7。字段名没有任何业务含义,查询时只能对着文档翻"field_4 到底存了什么"。SQL 写成: SELECT * FROM form_data WHERE field_1 = '张三' AND field_2 = '13800000000'; 这已经不是在写代码了,是在玩解谜游戏。 ...

十月 26, 2022 · 8 分钟 · 1575 字 · yaomingye

Elasticsearch 生产调优与索引设计

Elasticsearch 生产调优 📖 前置阅读:本文是 ES 系列的生产调优篇,假设读者已经掌握了 ES 核心概念、SpringBoot 操作和高级搜索聚合。如果还没有,建议先阅读前三篇: Elasticsearch 核心概念:倒排索引、分词器与 REST API 全解析 —— 介绍篇 SpringBoot Elasticsearch 全操作指南 —— 实战篇 ES 高级搜索与聚合分析 —— 进阶篇 一、⚡ 问题切入:搜索怎么越来越慢了? 商品从 10 万增加到 500 万,搜索响应时间从原来的 50ms 涨到了 2 秒。用户开始投诉"搜索好慢",产品经理开始质疑"ES 不是很快吗"。 你打开监控面板,发现: 搜索 P99 延迟 2.3 秒 ES 堆内存使用率 85% 磁盘 IO 等待时间飙升 GC 频率从每小时 2 次变成了每 5 分钟 1 次 问题出在哪?不一定是数据量大就一定慢。500 万数据对 ES 来说远没到上限——一个设计良好的 3 节点集群处理几亿数据都没问题。慢的往往是索引设计不合理、查询写得不高效、分页方式用错了。 本篇要解决的问题:怎么设计索引让 500 万数据搜索保持在 100ms 以内,以及遇到性能问题时从哪下手排查。 二、🗂️ 索引设计最佳实践 2.1 分片数设计 —— 不是越多越好 ES 把一个 Index 的数据切分成多个分片(Shard)分布在不同节点上。每个分片本质上是一个独立的 Lucene 实例——有自己的倒排索引、段文件、内存开销。 ...

十月 25, 2022 · 12 分钟 · 2352 字 · yaomingye

ES 高级搜索与聚合分析

ES 高级搜索 📖 前置阅读:本文是 ES 系列的进阶篇,假设读者已经掌握了 ES 核心概念(倒排索引、分词器、Mapping)和 SpringBoot 的基本操作。如果还没有,建议先阅读前两篇: Elasticsearch 核心概念:倒排索引、分词器与 REST API 全解析 —— 介绍篇 SpringBoot Elasticsearch 全操作指南 —— 实战篇 一、⚡ 问题切入:搜索结果不够准怎么办? 前面两篇学完,你已经可以搭建一个"能搜"的商品搜索功能了。但用户搜"苹果手机"时,排名第一的可能是"苹果水果礼盒"——因为倒排索引中"苹果"这个词也出现了。 问题出在几个地方: 用户搜"苹果手机"时,商品名包含"苹果手机"这四个字的应该排在最前面,但基础 match 查询没有考虑字段匹配的完整度 商品标题命中的权重应该比描述命中的权重更高,但基础查询一视同仁 用户期望按销量和评分来影响排序,而不仅仅是文本相关性 输入"苹果手鸡"应该能自动纠错成"苹果手机" 本篇要解决的问题就是:怎么让搜索结果更准、排序更合理、用户体验更接近 Google 搜索。 二、🔍 全文搜索再深入 2.1 multi_match —— 多字段搜索 第一篇的 match 查询只搜一个字段。实际产品中,搜索词可能同时匹配商品名、品牌名、描述等多个字段——用户输入"华为手机",应该既搜 name 也搜 description,甚至搜 brand。 multi_match 就是为此而生的。它有三种模式,差异在于多字段之间如何计算和合并相关性分数: flowchart LR classDef mode fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; classDef field fill:#0f172a,stroke:#3b82f6,stroke-width:1.5px,color:#bfdbfe,font-weight:bold; MM[multi_match\n搜索苹果手机] MM --> BF[best_fields\n取最优字段的分数] MM --> MF[most_fields\n各字段分数累加] MM --> CF[cross_fields\n跨字段组合匹配] BF --> BF_EX["name=苹果手机 → 10分\ndescription=苹果... → 2分\n最终得分:10分"] MF --> MF_EX["name=苹果... → 5分\ndescription=手机... → 3分\n最终得分:8分"] CF --> CF_EX["name=苹果 + detail=手机\n作为一个整体匹配\n最终得分:匹配两个字段"] class BF,MF,CF mode; class BF_EX,MF_EX,CF_EX field; best_fields(默认模式):搜索词在所有字段中分别执行匹配,取分数最高的那个字段作为最终得分。适合"搜索词大概率完整出现在某一个字段中"的场景——比如用户搜完整商品名。 ...

十月 24, 2022 · 10 分钟 · 2102 字 · yaomingye

Elasticsearch 核心概念

Elasticsearch 核心概念:倒排索引、分词器与 REST API 全解析 一、⚡ 问题切入:MySQL 模糊搜索为什么不行? 先看一个日常开发中最常见的搜索场景。用户在电商平台的搜索框里输入"华为手机",后端需要从商品表中查出匹配的商品。你第一反应肯定是写这样一条 SQL: SELECT * FROM product WHERE name LIKE '%华为手机%'; 这看起来没问题。但产品经理走过来跟你说:“搜索结果要把完全匹配的放在最前面,然后按销量排序,还要展示分类筛选和品牌聚合。“你看着手里的 SQL,表情逐渐僵硬。 MySQL LIKE '%keyword%' 有一个致命伤:前置通配符导致索引失效。B+Tree 索引遵循最左前缀匹配原则,% 一上来就破坏了索引的有序性,数据库只能全表扫描。500 万商品数据,一条 LIKE 查询耗时 3 秒以上——用户体验直接爆炸。 这不是加个索引能解决的问题。MySQL 是为精确匹配和范围查询设计的,不是为人类自然语言的模糊搜索设计的。用户不会输入精确的字段值,他们会打错字(“苹果手鸡”),用近义词(“笔记本” vs “笔记本电脑”),甚至用拼音(“huawei shouji”)。 全文搜索引擎就是为这个问题而生的。看一组实际数据: # MySQL LIKE:2.8 秒(500 万数据) mysql> SELECT * FROM product WHERE name LIKE '%华为手机%'; 500 rows in set (2.812 sec) # Elasticsearch match:0.015 秒(同量级数据,3 节点集群) GET /product/_search { "query": { "match": { "name": "华为手机" } } } # 返回:500 条结果,耗时 15ms,按相关性排序 接近 200 倍 的延迟差距。而且 ES 返回的结果自带相关性评分——包含"华为手机"这四个字且连在一起出现的商品分数最高,只包含"手机"的排在后面,只包含"华为"的更靠后。这就是 ES 作为搜索引擎存在的核心价值。 ...

十月 22, 2022 · 10 分钟 · 2065 字 · yaomingye

Redis 核心架构

Redis 核心架构:五大数据结构与常用命令全解析 一、⚡ 问题切入:MySQL 为什么不够? 先看一个典型的电商场景。商品详情页的 QPS(每秒请求数)在促销期间达到 5000,每个请求需要执行以下 SQL: -- 商品基本信息 SELECT * FROM product WHERE id = 10001; -- 商品 SKU 列表 SELECT * FROM product_sku WHERE product_id = 10001; -- 商品评价统计 SELECT COUNT(*), AVG(rating) FROM review WHERE product_id = 10001; MySQL 单机在简单查询下约能支撑 2000 ~ 3000 QPS,5000 QPS 直接打到数据库会导致连接池耗尽、响应超时,最终服务雪崩。 有人会说"加读写分离、分库分表",但这些方案在数据到达 MySQL 之前就有一个更直接的思路: 把热点数据放在内存里 。 这就是 Redis 存在的根本原因——将频繁访问的数据从磁盘(MySQL)迁移到内存,用空间换时间。一条 Redis GET 命令的延迟通常在 0.1ms 以内,而 MySQL 单条查询即使在索引命中、Buffer Pool 热数据全缓存的情况下,延迟也在 1ms ~ 5ms 之间。差距来自 存储介质 (内存 vs 磁盘)和 数据访问路径 (直接内存寻址 vs B+Tree 遍历)。 ...

十月 19, 2022 · 13 分钟 · 2659 字 · yaomingye
Cat Radio