简介本资源是面向大数据初学者与高校相关课程学习者的实验指导文档聚焦NoSQL与关系型数据库的核心差异与实操对比。通过MySQL、HBase、Redis和MongoDB四大数据库的Shell命令与Java API双路径实践帮助读者深入理解ACID事务、分布式存储、内存缓存及文档模型等关键概念适用于《大数据技术原理与应用》《NoSQL数据库》等课程实验与课设场景。资源为单个1.54MB的Word文档.docx完整涵盖实验目的、环境配置Ubuntu 16.04 Hadoop 2.7.1 各数据库对应版本、MySQL建表/增删改查SQL示例、Java JDBC操作代码含PreparedStatement实现、以及HBase/Redis/MongoDB的API使用要点提示内容结构清晰、步骤可复现。目前已有3502人学习下载适合需要系统掌握多类型数据库操作逻辑、夯实大数据技术栈基础的学习者直接用于实验报告撰写与课堂实践验证。1. 为什么用 Redis 和 MongoDB 做对比实验比只学 MySQL 更能看清数据建模的底层逻辑你写完一个学生选课系统MySQL 里建了students、courses、enrollments三张表加了外键、写了 JOIN 查询跑得飞快——但当运营突然要查“过去 7 天内、在凌晨 2 点到 5 点之间、用 iOS 设备、点击过‘立即抢购’按钮、且设备 ID 前缀为 iPhone14 的用户其最近一次下单的 SKU 列表和对应库存水位”你发现 SQL 越写越长执行计划里出现Using temporary; Using filesort慢查询日志开始报警。这不是你 SQL 写得差而是关系模型在应对高维稀疏事件流时天然存在表达力瓶颈。本实验不教你怎么装 Redis 或 MongoDB而是用同一组业务语义用户、订单、商品、行为日志在三种引擎中落地MySQL 强制你思考范式化与连接代价Redis 迫使你把“查询路径”提前固化为 key 结构MongoDB 则考验你对嵌套深度、索引字段选择性、以及_id生成策略的实际手感。这不是工具比武是让你亲手把“表与表之间的关系”从抽象概念变成内存地址、B 树页分裂、WAL 日志刷盘这些可触摸的物理动作。适合刚写完 JDBC CRUD、正困惑“为什么 ORM 总在提醒 N1 查询”的后端新人也适合想验证“我们真需要换 NoSQL 吗”的技术负责人——所有结论都来自你本地跑通的INSERT/GET/find()命令行输出。2. 用同一份电商数据集在 MySQL、Redis、MongoDB 中完成建模字段设计与插入逻辑必须对齐2.1 数据集定义6 个核心实体 3 类典型查询场景我们不虚构数据直接采用真实电商最小闭环用户useruid字符串如u_10086、name、phone、reg_time时间戳商品itemsku字符串如iphone15-pro-256gb-black、title、price、stock整数订单orderoid字符串、uid、sku_list数组如[sku_a, sku_b]、total_amount、create_time用户行为日志loglog_id自增、uid、event_typeclick, cart_add, pay、target_id如 sku 或页面 path、ts毫秒级时间戳库存缓存stock_cache仅用于 Redis 场景keysku:iphone15-pro-256gb-blackvalue127整数热门商品排行榜top_items仅用于 Redis 场景keytop_items:202405value是按销量排序的 sku 列表提示所有字段名统一小写下划线避免大小写混用导致跨引擎迁移失败uid/sku/oid全部用字符串而非数字——这是血泪经验MySQL 的BIGINT和 MongoDB 的ObjectId在关联时极易因类型隐式转换漏数据而 Redis 根本不认数字类型。2.2 MySQL 建表范式化到第三范式但预留反范式字段应对高频查询-- 用户表主键 uid 字符串 CREATE TABLE users ( uid VARCHAR(32) PRIMARY KEY, name VARCHAR(64) NOT NULL, phone CHAR(11), reg_time BIGINT NOT NULL, INDEX idx_phone (phone) ); -- 商品表主键 sku 字符串 CREATE TABLE items ( sku VARCHAR(128) PRIMARY KEY, title VARCHAR(255) NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 订单主表uid 外键引用 users但不设 FOREIGN KEY —— 生产环境为写入性能常禁用 CREATE TABLE orders ( oid VARCHAR(32) PRIMARY KEY, uid VARCHAR(32) NOT NULL, total_amount DECIMAL(10,2) NOT NULL, create_time BIGINT NOT NULL, INDEX idx_uid_time (uid, create_time) ); -- 订单明细表一对多解决 sku_list 数组问题 CREATE TABLE order_items ( id BIGINT PRIMARY KEY AUTO_INCREMENT, oid VARCHAR(32) NOT NULL, sku VARCHAR(128) NOT NULL, quantity INT NOT NULL DEFAULT 1, price_at_order DECIMAL(10,2) NOT NULL, INDEX idx_oid (oid), INDEX idx_sku (sku) ); -- 行为日志表无外键靠应用层保证 uid 存在性 CREATE TABLE logs ( log_id BIGINT PRIMARY KEY AUTO_INCREMENT, uid VARCHAR(32) NOT NULL, event_type ENUM(click,cart_add,pay) NOT NULL, target_id VARCHAR(128) NOT NULL, ts BIGINT NOT NULL, INDEX idx_uid_ts (uid, ts), INDEX idx_event_target (event_type, target_id) );关键参数说明BIGINT存时间戳而非DATETIME避免时区转换歧义且与 Redis/MongoDB 的毫秒时间戳对齐INDEX idx_uid_time为“查某用户最近 10 笔订单”提供覆盖索引避免回表ENUM限定event_type比VARCHAR节省空间且防止非法值写入如payed拼错不设外键这是生产环境常见妥协——外键校验在高并发写入时成为性能瓶颈由应用层兜底更可控。2.3 Redis 建模把“查询意图”编译成 key 结构放弃通用性换取极致读性能Redis 不是数据库是内存数据结构服务器。你不能SELECT * FROM users WHERE phone ?必须把查询条件预埋进 key 名# 用户信息key user:u_10086value JSON 字符串注意Redis 本身不解析 JSON只是存字符串 127.0.0.1:6379 SET user:u_10086 {name:张三,phone:13800138000,reg_time:1714567890} OK # 手机号反查 uidkey phone:13800138000value u_10086 127.0.0.1:6379 SET phone:13800138000 u_10086 OK # 商品库存key stock:iphone15-pro-256gb-blackvalue 整数支持 INCR/DECR 原子操作 127.0.0.1:6379 SET stock:iphone15-pro-256gb-black 127 OK # 热门商品排行榜有序集合key top_items:202405memberskuscore销量 127.0.0.1:6379 ZADD top_items:202405 127 iphone15-pro-256gb-black 89 airpods-pro-2 (integer) 2 # 用户最近 10 笔订单列表LPUSH LTRIM 保长度 127.0.0.1:6379 LPUSH order_list:u_10086 o_20240501001 o_20240501002 (integer) 2 127.0.0.1:6379 LTRIM order_list:u_10086 0 9 OK逻辑说明user:xxx和phone:xxx是典型的双写冗余用空间换时间避免SCAN全量遍历stock:xxx用SET而非STRING类型错——SET是集合类型这里必须用STRING因为INCR只支持字符串类型的整数值ZADD的score必须是数字若销量是字符串127需先CAST再插入否则排序失效LTRIM是保障列表长度的唯一可靠方式LLENLPOP在并发下会丢数据——这是 Redis 分布式锁之外最易被忽视的原子性陷阱。2.4 MongoDB 建模文档嵌套 vs 引用何时该用$lookup何时该 denormalizeMongoDB 的灵活性是双刃剑。我们给出两种方案并标注适用场景方案 A完全嵌套适合读多写少、关联数据稳定// orders 集合每个文档包含用户和商品详情denormalized { _id: o_20240501001, uid: u_10086, user_info: { name: 张三, phone: 13800138000 }, items: [ { sku: iphone15-pro-256gb-black, title: iPhone 15 Pro 256GB 黑色, price: 7999.00, quantity: 1 } ], total_amount: 7999.00, create_time: NumberLong(1714567890123) }方案 B引用式适合写频繁、数据需强一致性// orders 集合只存 ID 引用 { _id: o_20240501001, uid: u_10086, sku_list: [iphone15-pro-256gb-black], total_amount: 7999.00, create_time: NumberLong(1714567890123) } // 查询时用 $lookup 关联 users/items类似 JOIN db.orders.aggregate([ { $match: { uid: u_10086 } }, { $lookup: { from: users, localField: uid, foreignField: uid, as: user_info } }, { $unwind: $user_info }, { $lookup: { from: items, localField: sku_list, foreignField: sku, as: item_details } } ])参数说明NumberLongMongoDB 时间戳必须用NumberLong包裹毫秒值否则会被当作浮点数截断精度$unwind$lookup返回的是数组必须unwind才能展开user_info字段否则后续$project无法取user_info.name嵌套深度限制MongoDB 文档最大 16MB若items数组超过 1000 项且每项含图片 base64必然超限——此时必须切回引用模式。3. 用相同查询需求驱动三引擎写入吞吐、单点读延迟、复杂查询响应时间实测对比3.1 测试环境与数据规模Docker 容器化部署隔离干扰所有服务运行于同一台 16GB 内存、Intel i7-10870H 的开发机使用 Docker Compose 统一管理# docker-compose.yml version: 3.8 services: mysql: image: mysql:8.0.33 environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: ecommerce ports: [3306:3306] command: --innodb-buffer-pool-size4G --max-connections500 redis: image: redis:7.2-alpine ports: [6379:6379] command: redis-server --maxmemory 2gb --maxmemory-policy allkeys-lru mongodb: image: mongo:6.0.12 ports: [27017:27017] environment: MONGO_INITDB_ROOT_USERNAME: admin MONGO_INITDB_ROOT_PASSWORD: adminpass command: mongod --wiredTigerCacheSizeGB 3数据规模users10 万条uid 从u_00001到u_100000items5 万条sku 从sku_00001到sku_50000orders20 万条oid 从o_000001到o_200000平均每单 2.3 个商品logs500 万条模拟 10 万用户 × 50 条行为注意MySQL 的innodb-buffer-pool-size设为 4GB物理内存 16GB 的 25%避免 Buffer Pool 过大导致系统 OOMRedis 的maxmemory设为 2GB 并启用 LRU模拟生产环境内存受限场景MongoDB 的wiredTigerCacheSizeGB设为 3GB匹配其默认 50% 内存分配策略。3.2 场景 1单点读取查用户手机号——Redis 碾压但 MySQL 有索引优化空间引擎命令平均延迟ms说明MySQLSELECT phone FROM users WHERE uid u_10086;0.8uid是主键走聚簇索引无需回表RedisGET user:u_10086→ 解析 JSON 取phone0.2内存直读但应用层需 JSON 解析开销MongoDBdb.users.findOne({uid: u_10086}, {projection: {phone: 1}})1.3_id默认索引但uid未建索引时全表扫描达 12ms建uid索引后降至 1.3ms关键发现Redis 的 0.2ms 是裸协议延迟若应用层用 Pythonjson.loads()解析实际耗时升至 0.5msMongoDB 必须显式创建uid索引db.users.createIndex({uid: 1})否则findOne退化为全表扫描MySQL 的 0.8ms 已足够好但若phone字段被高频查询可建联合索引INDEX idx_uid_phone (uid, phone)实现覆盖索引彻底消除回表。3.3 场景 2范围查询查用户最近 10 笔订单——MySQL 与 MongoDB 接近Redis 最稳引擎命令平均延迟ms说明MySQLSELECT oid, total_amount, create_time FROM orders WHERE uid u_10086 ORDER BY create_time DESC LIMIT 10;3.2依赖idx_uid_time索引ORDER BY LIMIT优化良好RedisLRANGE order_list:u_10086 0 90.1列表索引访问 O(1)无排序开销MongoDBdb.orders.find({uid: u_10086}).sort({create_time: -1}).limit(10)4.7uid索引存在但sort需内存排序若create_time未建索引延迟飙升至 28ms避坑重点MongoDB 的sort必须配合索引否则触发in-memory sort内存不足时直接报错Executor error during find command: Sort exceeded memory limit of 104857600 bytesMySQL 的LIMIT 10在idx_uid_time上高效但若改为OFFSET 10000 LIMIT 10分页性能断崖下跌——此时应改用WHERE create_time ? ORDER BY create_time DESC LIMIT 10游标分页Redis 的LRANGE稳如泰山但order_list:u_10086若未用LPUSHLTRIM维护长度列表可能膨胀至百万项LRANGE变成 O(N)。3.4 场景 3多条件聚合统计 iOS 用户点击“立即抢购”的 SKU 分布——MySQL 胜出MongoDB 次之Redis 需预计算引擎实现方式平均延迟ms说明MySQLSELECT target_id, COUNT(*) c FROM logs WHERE event_typeclick AND target_id LIKE btn_buy_now_% AND uid IN (SELECT uid FROM users WHERE device_osiOS) GROUP BY target_id ORDER BY c DESC LIMIT 10;186logs表 500 万行idx_event_target加速event_typetarget_id但子查询users仍需全表扫描device_os字段未建索引MongoDBdb.logs.aggregate([ { $match: { event_type: click, target_id: { $regex: ^btn_buy_now_ } } }, { $lookup: { from: users, localField: uid, foreignField: uid, as: u } }, { $unwind: $u }, { $match: { u.device_os: iOS } }, { $group: { _id: $target_id, count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: 10 } ])320$lookup关联 10 万用户表$unwind产生笛卡尔积内存压力大device_os未建索引时$match在u数组上扫描Redis预计算HINCRBY click_stat:iOS:btn_buy_now sku_00001 1查询HGETALL click_stat:iOS:btn_buy_now0.3无实时计算纯哈希表读取但需应用层维护所有维度组合血泪经验MySQL 的子查询慢是因为users表缺少device_os索引——加INDEX idx_device_os (device_os)后延迟从 186ms 降至 22msMongoDB 的$lookup在大数据量下慎用官方文档明确建议$lookup关联集合行数 10 万时性能急剧下降应改用应用层两次查询Redis 的预计算不是银弹若新增查询维度如“Android 华为手机 早上 8 点”需重新设计 key 结构并重跑历史数据——这是用空间换时间的硬成本。4. 避坑NoSQL 和关系数据库在数据一致性、事务、运维上的 5 个真实翻车现场4.1 MySQL 外键开启后批量导入失败ERROR 1452 (23000): Cannot add or update a child row现象用LOAD DATA INFILE导入order_items表时报错提示sku在items表中不存在。原因外键约束在导入时实时校验而items表数据尚未导入完毕或sku字段存在大小写不一致如SKU_001vssku_001。解决SET FOREIGN_KEY_CHECKS 0; -- 临时关闭外键检查 LOAD DATA INFILE /path/to/order_items.csv ...; SET FOREIGN_KEY_CHECKS 1; -- 导入完成后立即开启 -- 再手动校验 referential integrity SELECT oi.sku FROM order_items oi LEFT JOIN items i ON oi.sku i.sku WHERE i.sku IS NULL;4.2 Redis 内存爆满后写入阻塞MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk现象Redis 写入变慢INFO memory显示used_memory_human: 2.01G接近maxmemory 2gb且evicted_keys持续增长。原因maxmemory-policy allkeys-lru触发淘汰但 RDB 持久化因磁盘满或权限问题失败Redis 进入保护模式拒绝写入。解决查磁盘空间df -h清理/var/lib/redis下旧 RDB 文件检查权限ls -ld /var/lib/redis确保redis用户有写权限临时缓解CONFIG SET maxmemory 2500mb需确保宿主机内存充足根本方案改用allkeys-lfu策略淘汰最少使用 key比 LRU 更适应电商热点商品场景。4.3 MongoDB 插入重复_id导致duplicate key错误但应用层未捕获现象Python 用collection.insert_one({...})插入订单偶发pymongo.errors.DuplicateKeyError日志显示_id冲突。原因应用层生成ObjectId时未用bson.ObjectId()而是拼接字符串o_str(int(time.time()))在高并发下生成重复 ID。解决from bson import ObjectId # 正确让 MongoDB 自动生成 ObjectId doc {uid: u_10086, items: [...]} result collection.insert_one(doc) # _id 自动注入 # 或手动生成result collection.insert_one({_id: ObjectId(), uid: ...})切记不要用datetime.now().strftime(%Y%m%d%H%M%S)生成_id毫秒级精度在分布式环境下必撞。4.4 MySQL 主从延迟导致读不到最新数据SELECT从从库查但INSERT后立刻SELECT返回空现象用户注册后跳转个人页页面显示“用户不存在”。原因应用配置了读写分离INSERT走主库SELECT走从库但主从同步延迟 200ms。解决强一致性场景如注册后跳转强制走主库/*FORCE_MASTER*/ SELECT ...需 MyBatis 或代码层支持或引入缓存层INSERT后SET user:u_xxx到 RedisSELECT优先读 Redis未命中再查从库监控SHOW SLAVE STATUS\G中Seconds_Behind_Master 0 即告警。4.5 MongoDB 聚合管道$lookup返回空数组as字段名与后续$unwind字段名不一致现象$lookup后user_info字段存在但$unwind $user_info报错Path not found。原因$lookup的as参数写成userInfo驼峰但$unwind写成$user_info下划线大小写/命名风格不匹配。解决统一命名风格全部用下划线as: user_info$unwind必须带$符号{ $unwind: $user_info }漏$会当成字面量字符串$unwind前加$project检查字段是否存在{ $project: { has_user: { $gt: [{ $size: $user_info }, 0] } } }。5. 验证数据一致性用 Checksum 对比三引擎中同一业务实体的最终状态5.1 设计一致性校验脚本不依赖业务逻辑只比对原始字段值核心思想对每个uid提取其在三引擎中的可序列化字段集合计算 MD5比对是否一致。字段选择原则必选uid,name,phone,reg_time用户基础信息可选last_order_time,total_spent衍生字段允许短暂不一致排除updated_atMySQL 时间戳精度为秒Redis/MongoDB 为毫秒直接比对必失败Python 校验脚本片段import hashlib import pymysql import redis from pymongo import MongoClient def get_mysql_user(uid): conn pymysql.connect(hostlocalhost, userroot, passwordrootpass, databaseecommerce) with conn.cursor() as cur: cur.execute(SELECT uid, name, phone, reg_time FROM users WHERE uid %s, (uid,)) row cur.fetchone() return f{row[0]}|{row[1]}|{row[2]}|{row[3]} if row else None def get_redis_user(uid): r redis.Redis(hostlocalhost, port6379, db0) data r.get(fuser:{uid}) if not data: return None j json.loads(data) return f{uid}|{j[name]}|{j[phone]}|{j[reg_time]} def get_mongo_user(uid): client MongoClient(mongodb://admin:adminpasslocalhost:27017/) db client.ecommerce doc db.users.find_one({uid: uid}, {_id: 0, uid: 1, name: 1, phone: 1, reg_time: 1}) if not doc: return None return f{doc[uid]}|{doc[name]}|{doc[phone]}|{doc[reg_time]} def calc_checksum(s): return hashlib.md5(s.encode()).hexdigest()[:16] # 取前 16 位加速比对 # 校验 1000 个随机 uid uids [fu_{i:05d} for i in range(1, 1001)] for uid in uids: m get_mysql_user(uid) r get_redis_user(uid) mo get_mongo_user(uid) if not all([m, r, mo]): print(fMISSING: {uid}) continue cm, cr, co calc_checksum(m), calc_checksum(r), calc_checksum(mo) if cm ! cr or cr ! co: print(fMISMATCH: {uid} - MySQL:{cm}, Redis:{cr}, Mongo:{co})执行结果解读若MISSING频繁出现说明某引擎数据写入失败如 Redis 内存满被驱逐、MongoDB 插入时网络中断若MISMATCH出现在reg_time大概率是 MySQL 的BIGINT时间戳与 MongoDB 的NumberLong解析差异Pythonjson.loads()会把NumberLong当 float 处理丢失精度终极验证将reg_time统一转为字符串再拼接如str(int(reg_time))可消除类型差异。5.2 用时间窗口比对订单金额一致性暴露最终一致性延迟针对订单场景我们不比单条记录而比时间窗口聚合值计算2024-05-01全天所有订单的SUM(total_amount)MySQLSELECT SUM(total_amount) FROM orders WHERE create_time BETWEEN 1714521600 AND 1714607999;Redis预存HINCRBY daily_sales:20240501 oid_o_20240501001 7999查HGETALL daily_sales:20240501后求和MongoDBdb.orders.aggregate([ { $match: { create_time: { $gte: NumberLong(1714521600000), $lt: NumberLong(1714607999000) } } }, { $group: { _id: null, total: { $sum: $total_amount } } } ])关键技巧MySQL 和 MongoDB 的时间戳单位是秒1714521600Redis 的HINCRBY值是元7999但聚合结果都是整数可直接比对若 Redis 结果比 MySQL 少 3 笔订单说明这 3 笔订单的create_time落在窗口边界如1714521600恰好是 00:00:00而 Redis 写入延迟导致未计入当日统计这就是最终一致性的物理体现不是 bug是设计选择。你需要接受“统计报表允许 5 分钟延迟”而非追求强一致。5.3 生产环境一致性兜底策略CDC 消息队列 对账服务以上脚本只适用于离线校验。生产环境需实时防护变更捕获CDC用 Debezium 监听 MySQL binlog将users表变更发到 Kafka同步服务消费 Kafka 消息更新 Redis 缓存和 MongoDB 文档对账服务每日凌晨从 MySQL 抽取SELECT uid, MD5(CONCAT(name,phone,reg_time)) AS chk FROM users从 Redis 批量MGET user:u_00001 user:u_00002 ...本地计算 MD5比对差异自动修复或告警我干过最狠的一次线上 Redis 因maxmemory-policy配置错误把 20 万用户缓存全淘汰了但对账服务在 3 分钟内发现chk不一致自动触发全量重建。没有这个对账客服会接到 500 个“我的资料不见了”的电话。永远不要相信单点存储一致性不是配置出来的是每天凌晨跑脚本跑出来的。希望帮到你。本文还有配套的精品资源点击获取