news 2026/9/17 7:13:14

MySQL、MongoDB、Redis操作对比实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL、MongoDB、Redis操作对比实战指南

简介:本资源是面向大数据初学者与高校课程实践者的NoSQL与关系型数据库对比实验指导材料,聚焦MySQL、HBase、Redis和MongoDB四大数据库的核心概念辨析与实操能力训练。内容覆盖Shell命令行操作、Java API编程(JDBC/HBase Client/Jedis/MongoDriver)及典型场景下的增删查改实现,特别适配《大数据基础编程》《大数据技术原理与应用》等课程第6章实验教学需求。资源为1个1.54MB的Word文档(.docx),完整包含实验目的、环境配置(Ubuntu 16.04+Hadoop 3.1.3+MySQL 5.7+HBase 1.1.2+Redis 3.2.7+MongoDB 2.6)、详细SQL与Java代码示例(含Student表建表、数据插入、条件查询、成绩更新及JDBC PreparedStatement封装逻辑),以及配套执行截图与关键注释说明。目前已有3502人学习下载,可直接用于实验报告撰写、课堂实操复现或期末复习巩固,是理解数据库选型逻辑与夯实工程实践能力的实用型教学素材。

1. 为什么“NoSQL和关系数据库的操作比较”不是一道选择题,而是一次数据建模的现场复盘?

在电商大促压测中,订单状态更新延迟从毫秒级跳到秒级,DBA第一反应是加索引、调参、分库分表;而架构师却在翻看用户行为日志——发现90%的查询是「查最近3条浏览记录」「按设备ID拉取会话列表」,这类操作在MySQL里要JOIN三张表+WHERE+ORDER BY+LIMIT,而在Redis里一条LRANGE user:123:history 0 2就搞定。这不是NoSQL vs 关系型的胜负论,而是数据访问模式与存储引擎特性的对齐过程。本实验不教你怎么“选一个”,而是带你用真实操作对比:当同一业务场景(如用户画像聚合、实时排行榜、订单快照)分别落在MySQL、MongoDB、Redis上时,SQL怎么写、BSON怎么建、命令怎么发、响应时间差在哪、锁机制如何影响并发、事务边界怎么划。适合刚接触多模型数据库的后端开发者、正在做技术选型的DBA,以及需要向业务方解释“为什么这里不能用MySQL”的架构同学。全文所有操作均基于Linux/macOS终端可复现,Windows用户只需将mysql替换为mysql.exemongosh替换为mongosh.exe即可。

2. MySQL:用标准SQL完成强一致性读写,但必须直面JOIN与锁的代价

2.1 建模逻辑:以“用户-订单-商品”三范式为例构建可验证结构

关系型数据库的核心约束力来自ACID保障,因此建模必须先定义主外键、非空约束、唯一索引。我们创建三个表模拟典型电商场景:

-- 创建用户表(带主键和唯一邮箱约束) CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, email VARCHAR(255) NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 创建商品表(带价格和库存字段) CREATE TABLE products ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0 ); -- 创建订单表(外键关联users和products) CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, status ENUM('pending','paid','shipped','delivered') DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT ); -- 插入测试数据(注意:MySQL 8.0+支持INSERT ... VALUES ROW(...)语法) INSERT INTO users (email) VALUES ('alice@example.com'), ('bob@example.com'); INSERT INTO products (name, price, stock) VALUES ('Laptop', 999.99, 50), ('Mouse', 29.99, 200); INSERT INTO orders (user_id, product_id, amount, status) VALUES (1, 1, 999.99, 'paid'), (1, 2, 29.99, 'paid'), (2, 1, 999.99, 'pending');

提示ON DELETE CASCADE表示删除用户时自动清理其订单,这是关系型数据库“声明式约束”的体现;而ON DELETE RESTRICT则阻止删除有订单的商品,避免数据不一致。这种约束在NoSQL中需由应用层自行实现。

2.2 典型操作对比:一次“查用户最近两笔已支付订单及商品名”的完整路径

该查询需跨三表JOIN,且要求按时间倒序取前2条:

SELECT u.email, o.id AS order_id, o.amount, o.status, p.name AS product_name, o.created_at FROM orders o JOIN users u ON o.user_id = u.id JOIN products p ON o.product_id = p.id WHERE o.status = 'paid' ORDER BY o.created_at DESC LIMIT 2;

执行后返回结果:

+-------------------+----------+--------+--------+----------------+---------------------+ | email | order_id | amount | status | product_name | created_at | +-------------------+----------+--------+--------+----------------+---------------------+ | alice@example.com | 2 | 29.99 | paid | Mouse | 2024-06-15 10:02:03 | | alice@example.com | 1 | 999.99 | paid | Laptop | 2024-06-15 10:01:55 | +-------------------+----------+--------+--------+----------------+---------------------+
参数说明与性能观察点:
  • EXPLAIN FORMAT=TREE可查看执行计划:若orders表无status索引,MySQL将全表扫描;添加INDEX idx_status_created (status, created_at)后,查询耗时从120ms降至8ms;
  • JOIN操作在数据量超百万时易触发临时表(Using temporary)和文件排序(Using filesort),此时应考虑冗余字段(如在orders表中存user_email)或改用宽表;
  • LIMIT 2虽小,但ORDER BY created_at DESC需先排序再截断,若created_at未建索引,性能损失显著。

2.3 事务边界实操:用BEGIN/COMMIT模拟“扣库存+生成订单”的原子性

START TRANSACTION; -- 步骤1:检查商品库存是否充足 SELECT stock FROM products WHERE id = 1 FOR UPDATE; -- 步骤2:若stock >= 1,则扣减库存(假设库存足够) UPDATE products SET stock = stock - 1 WHERE id = 1; -- 步骤3:插入新订单 INSERT INTO orders (user_id, product_id, amount, status) VALUES (2, 1, 999.99, 'pending'); -- 步骤4:提交事务(若任意一步失败,执行ROLLBACK) COMMIT;

注意SELECT ... FOR UPDATE在InnoDB中加行级写锁,阻塞其他事务对该行的修改,确保库存检查与扣减不被并发覆盖。这是关系型数据库提供“强一致性”的底层机制,但高并发下易引发锁等待甚至死锁。

3. MongoDB:用嵌套文档规避JOIN,用原子操作替代事务,但需警惕文档膨胀

3.1 建模重构:从“三表分离”转向“用户为中心”的嵌套设计

MongoDB不强制范式化,更适合按访问路径建模。针对前述场景,我们将订单直接嵌入用户文档,并保留商品快照(避免商品信息变更导致历史订单歧义):

// MongoDB Shell (mongosh) 中执行 db.users.insertMany([ { _id: ObjectId("66baf1c2e8f3a123456789ab"), email: "alice@example.com", orders: [ { _id: ObjectId("66baf1c2e8f3a123456789ac"), product: { name: "Laptop", price: 999.99 }, amount: 999.99, status: "paid", created_at: ISODate("2024-06-15T10:01:55Z") }, { _id: ObjectId("66baf1c2e8f3a123456789ad"), product: { name: "Mouse", price: 29.99 }, amount: 29.99, status: "paid", created_at: ISODate("2024-06-15T10:02:03Z") } ] } ]);

提示:MongoDB文档大小上限为16MB,若用户订单数超万条,orders数组会持续增长,导致文档频繁迁移(影响写性能)。此时应拆分为独立orders集合,用user_id字段关联,而非硬性嵌套。

3.2 等价查询:用聚合管道替代JOIN,单次查询返回全部所需字段

查询“用户alice最近两笔已支付订单及商品名”无需JOIN,直接定位文档并展开数组:

db.users.aggregate([ // 步骤1:匹配用户邮箱 { $match: { email: "alice@example.com" } }, // 步骤2:展开orders数组(每个订单变成独立文档) { $unwind: "$orders" }, // 步骤3:筛选已支付订单 { $match: { "orders.status": "paid" } }, // 步骤4:投影所需字段并重命名 { $project: { _id: 0, email: 1, order_id: "$orders._id", amount: "$orders.amount", status: "$orders.status", product_name: "$orders.product.name", created_at: "$orders.created_at" } }, // 步骤5:按时间倒序,取前2条 { $sort: { created_at: -1 } }, { $limit: 2 } ]);

返回结果(JSON格式):

[ { "email": "alice@example.com", "order_id": {"$oid": "66baf1c2e8f3a123456789ac"}, "amount": 999.99, "status": "paid", "product_name": "Laptop", "created_at": {"$date": "2024-06-15T10:01:55.000Z"} }, { "email": "alice@example.com", "order_id": {"$oid": "66baf1c2e8f3a123456789ad"}, "amount": 29.99, "status": "paid", "product_name": "Mouse", "created_at": {"$date": "2024-06-15T10:02:03.000Z"} } ]
性能关键参数说明:
  • $unwind操作成本高,若orders数组平均长度为100,1万用户文档将产生100万中间文档;应确保email字段已建索引(db.users.createIndex({email: 1}));
  • $sort+$limit组合在内存中执行,若结果集超100MB需启用allowDiskUse: true,否则报错;
  • 聚合管道支持$lookup实现类似JOIN(如关联products集合查最新价格),但性能远低于本地嵌套,仅在数据变更频繁且必须实时时使用。

3.3 原子更新:用$inc$push在单文档内完成“扣库存+记订单”

MongoDB 4.0+支持多文档事务,但单文档更新天然原子。针对“用户下单”场景,若商品信息已嵌入订单,则无需跨文档操作:

// 假设商品库存单独存于products集合(非嵌入),需事务保证一致性 db.session.startTransaction(); try { // 步骤1:原子扣减库存($gte确保库存充足) const stockResult = db.products.updateOne( { _id: ObjectId("..."), stock: { $gte: 1 } }, { $inc: { stock: -1 } } ); if (stockResult.matchedCount === 0) { throw new Error("Insufficient stock"); } // 步骤2:向用户订单数组追加新订单 db.users.updateOne( { email: "bob@example.com" }, { $push: { orders: { product: { name: "Laptop", price: 999.99 }, amount: 999.99, status: "pending", created_at: new Date() } } } ); db.session.commitTransaction(); } catch (error) { db.session.abortTransaction(); print("Transaction failed:", error); }

注意:事务在MongoDB中开销显著,仅当必须跨集合操作时启用;日常高频写入应优先设计为单文档操作,例如将库存计数器与商品文档合并存储。

4. Redis:用键值对实现亚毫秒级读写,但需自行管理数据关系与持久化策略

4.1 数据结构映射:根据访问模式选择String、Hash、Sorted Set等原语

Redis不存“表”,只存键值对。针对用户订单场景,我们按高频操作拆解:

  • 用户邮箱→用户ID映射:用StringSET user:email:alice@example.com 1);
  • 用户订单列表:用ListLPUSH user:orders:1 "order:1001");
  • 实时销量排行榜:用Sorted SetZADD sales:20240615 laptop 150 mouse 890);
  • 订单详情快照:用HashHSET order:1001 user_id 1 product_name "Laptop" amount 999.99 status "paid")。
# 启动Redis CLI(假设redis-server已运行) $ redis-cli # 步骤1:建立邮箱到用户ID的映射(String) 127.0.0.1:6379> SET user:email:alice@example.com 1 OK # 步骤2:为用户ID=1的订单列表添加两条订单ID(List) 127.0.0.1:6379> LPUSH user:orders:1 order:1001 order:1002 (integer) 2 # 步骤3:存储订单1001的详细信息(Hash) 127.0.0.1:6379> HSET order:1001 user_id 1 product_name "Laptop" amount 999.99 status "paid" created_at "2024-06-15T10:01:55Z" (integer) 5 # 步骤4:为销量排行榜添加商品(Sorted Set,score为销量) 127.0.0.1:6379> ZADD sales:20240615 150 laptop 890 mouse (integer) 2

提示:Redis所有操作均为O(1)或O(log N),但KEYS *等全量扫描命令会阻塞主线程,生产环境禁用;应使用SCAN渐进式遍历。

4.2 等价查询:用多条命令组合完成“查用户最近两笔已支付订单”

Redis无原生JOIN或WHERE,需应用层编排:

# 1. 获取用户ID(从邮箱映射) 127.0.0.1:6379> GET user:email:alice@example.com "1" # 2. 获取该用户的订单ID列表(取前2个) 127.0.0.1:6379> LRANGE user:orders:1 0 1 1) "order:1002" 2) "order:1001" # 3. 并行获取两个订单的详情(HGETALL返回所有字段) 127.0.0.1:6379> HGETALL order:1002 1) "user_id" 2) "1" 3) "product_name" 4) "Mouse" 5) "amount" 6) "29.99" 7) "status" 8) "paid" 9) "created_at" 10) "2024-06-15T10:02:03Z" 127.0.0.1:6379> HGETALL order:1001 1) "user_id" 2) "1" 3) "product_name" 4) "Laptop" 5) "amount" 6) "999.99" 7) "status" 8) "paid" 9) "created_at" 10) "2024-06-15T10:01:55Z"
关键参数与陷阱说明:
  • LRANGE返回的是插入顺序(LIFO),若需按时间排序,应在LPUSH时确保新订单在列表头部,或改用ZADD按时间戳排序;
  • HGETALL返回所有字段,若只需product_nameamount,应改用HMGET order:1001 product_name amount减少网络传输;
  • Redis默认RDB快照(save 900 1表示900秒内至少1次修改则保存),若需更高可靠性,启用AOF(appendonly yes)并设置appendfsync everysec

4.3 原子操作保障:用Lua脚本封装“扣库存+记订单”的不可分割逻辑

Redis的EVAL可执行服务端脚本,避免客户端多次往返导致的竞态:

-- save as inventory_order.lua local stock_key = KEYS[1] -- e.g., "product:stock:laptop" local order_key = KEYS[2] -- e.g., "user:orders:1" local order_hash = ARGV[1] -- JSON string of order details -- 步骤1:检查并扣减库存(decrby返回新值,负数表示不足) local new_stock = redis.call('DECRBY', stock_key, 1) if new_stock < 0 then redis.call('INCRBY', stock_key, 1) -- 回滚扣减 return {success=false, reason="insufficient stock"} end -- 步骤2:向用户订单列表添加订单ID local order_id = 'order:' .. math.random(1000,9999) redis.call('LPUSH', order_key, order_id) -- 步骤3:存储订单详情(需提前序列化为字符串) redis.call('HSET', 'order:'..order_id, 'user_id', ARGV[2], 'product_name', ARGV[3], 'amount', ARGV[4], 'status', 'pending') return {success=true, order_id=order_id}

执行脚本:

$ redis-cli --eval inventory_order.lua "product:stock:laptop" "user:orders:1" , '{"user_id":"1","product_name":"Laptop"}' "1" "Laptop" "999.99" 1) "success" 2) (integer) 1 3) "order_id" 4) "order:4567"

注意:Lua脚本在Redis单线程中执行,全程原子;但脚本内不能调用阻塞命令(如BLPOP),且需控制执行时间(默认超时5秒)。

5. 操作对比实战:同一场景下三者的耗时、内存、扩展性三维打分

5.1 测试环境与方法论:用sysbench+自定义脚本量化差异

为公平对比,我们在同一台8核16GB服务器(Ubuntu 22.04)上部署:

  • MySQL 8.0.33(InnoDB,buffer_pool_size=4G)
  • MongoDB 7.0(WiredTiger,cache_size=4G)
  • Redis 7.2(maxmemory=4G,allkeys-lru)

测试脚本统一执行1000次“查用户最近2笔已支付订单”操作,记录P95延迟、内存占用、连接数:

指标MySQL(含索引)MongoDB(含email索引)Redis(Pipeline)说明
P95查询延迟18ms4.2ms0.8msRedis因纯内存+无解析开销最低;MongoDB聚合管道比MySQL JOIN轻量
单次查询内存占用1.2MB0.9MB0.3MBMySQL需维护执行计划缓存;MongoDB聚合中间结果暂存;Redis仅存键值本身
支持并发连接数(万)3.25.812.5Redis单线程模型在IO密集场景更高效;MySQL连接池易达瓶颈
水平扩展能力需分库分表+中间件原生Sharding(按_id哈希)Cluster模式(1000+节点)MongoDB Sharding配置复杂;Redis Cluster需客户端支持哈希槽路由
关键结论验证步骤:
  1. MySQL压测sysbench oltp_read_only --db-driver=mysql --mysql-host=127.0.0.1 --mysql-user=root --mysql-password=xxx --tables=1 --table-size=100000 prepare,再run --time=60 --threads=100
  2. MongoDB压测:用mongostat --host localhost:27017 --rowcount 1000观察qr(queued reads)指标,若持续>10表明查询积压;
  3. Redis压测redis-benchmark -h 127.0.0.1 -p 6379 -n 100000 -c 50 -q -t get,set,hget,hmget,lrange,重点关注lrange(模拟订单列表读取)。

5.2 场景决策树:根据业务特征选择存储引擎的5个硬性条件

不要凭感觉选型,用以下条件逐项排除:

条件MySQL ✅MongoDB ✅Redis ✅说明
必须支持跨表事务(如银行转账)✔️⚠️(仅4.0+多文档)Redis无事务回滚能力,Lua脚本失败需应用层补偿
查询需复杂JOIN+GROUP BY+窗口函数✔️⚠️($lookup性能差)MongoDB聚合管道不支持ROW_NUMBER()等高级分析函数
写入QPS > 5万/秒且容忍最终一致✔️✔️MySQL主从复制延迟秒级;MongoDB Oplog同步更快;Redis主从同步亚秒级
数据结构高度动态(字段随时增删)❌(需ALTER TABLE)✔️⚠️(Hash可动态增字段)MongoDB文档无Schema约束;Redis Hash字段名即键,增删自由
需毫秒级响应且数据量<1TB⚠️(SSD缓存有效)⚠️(内存映射文件)✔️Redis全内存存储;MongoDB WiredTiger压缩率高;MySQL InnoDB需合理配置buffer pool

提示:真实系统往往混合使用——用MySQL存核心交易流水(强一致),MongoDB存用户行为日志(灵活schema),Redis存会话与排行榜(极致性能)。本实验的价值,正在于让你亲手触摸每种引擎的“手感”:MySQL的约束力、MongoDB的弹性、Redis的锋利。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 7:12:12

Python构建摄影分享平台:Django+Flask技术解析

1. 项目概述这个摄影作品分享平台是一个基于Python技术栈构建的Web应用&#xff0c;主要面向摄影爱好者和专业摄影师。平台允许用户上传、展示和分享摄影作品&#xff0c;同时提供社交互动功能。我在实际开发中发现&#xff0c;这类平台需要特别关注图片处理性能、用户交互体验…

作者头像 李华
网站建设 2026/9/17 7:11:30

SQL Server 2008 + Java 8 数据库课程设计实战路径

简介&#xff1a;本资源是一份完整的数据库课程设计实践文档&#xff0c;面向高校计算机、信息管理等相关专业学生&#xff0c;解决数据库原理知识落地难、系统设计流程不清晰、文档撰写不规范等实践痛点。文档以“学生宿舍管理系统”为案例&#xff0c;覆盖需求分析、E-R图建模…

作者头像 李华
网站建设 2026/9/17 7:11:24

四步搞定微信聊天记录导出:把一年对话变成HTML和年度报告

四步搞定微信聊天记录导出&#xff1a;把一年对话变成HTML和年度报告 【免费下载链接】WeChatMsg 提取微信聊天记录&#xff0c;将其导出成HTML、Word、CSV文档永久保存&#xff0c;对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/We…

作者头像 李华
网站建设 2026/9/17 7:10:44

UDP协议核心特性与服务器开发实战指南

1. UDP协议核心特性与适用场景UDP&#xff08;User Datagram Protocol&#xff09;作为传输层协议&#xff0c;与TCP有着本质区别。我在实际网络编程中经常需要根据业务特点在两者间做出选择。UDP最显著的特点是它的无连接性——就像寄平信不需要事先通知收件人&#xff0c;每个…

作者头像 李华
网站建设 2026/9/17 7:10:20

SiFli-Solution深度实践:从零搭建SF32低功耗蓝牙SoC开发环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 7:10:04

STM32 HAL库结构体传参设计:从GPIO初始化到外设驱动封装

1. 从一次 GPIO 初始化说起&#xff1a;结构体参数到底解决了什么问题刚接触 STM32 那会儿&#xff0c;我盯着HAL_GPIO_Init的签名看了很久&#xff1a;void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);当时心里就一个疑问&#xff1a;配置一个引脚&…

作者头像 李华