尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

关系型数据库与 NoSQL 在刷题系统中的选择:MySQL、MongoDB 与 Redis 的分工

关系型数据库与 NoSQL 在刷题系统中的选择:MySQL、MongoDB 与 Redis 的分工
📅 发布时间:2026/7/29 20:20:57

关系型数据库与 NoSQL 在刷题系统中的选择:MySQL、MongoDB 与 Redis 的分工

一、深度引言与场景痛点:用 MySQL 存刷题历史,用 MongoDB 存题目标签,逻辑乱了

7 月设计刷题系统时,我在数据库选型上犹豫了很久。这个系统的数据可以分为三类:用户数据(账号、统计)是典型的关系型数据;题目数据(题目描述、标签、题解)是半结构化的文档型数据;用户每日活跃状态(签到、在线时长)是热数据,需要高频读写。

直觉告诉我应该"不同类型的数据用不同的数据库",但直觉没有告诉我边界在哪里——用户的刷题记录,它既是关系型数据(与用户 ID 关联),又是大量产生的时间序列数据(每天几十上百条)。这种情况下,存 MySQL 还是 MongoDB?

本文通过刷题系统的实际数据类型,拆解 MySQL、MongoDB、Redis 三种数据库的分工边界。核心结论是:不是每一类数据都需要专门的数据库,选型的关键在于识别数据的访问模式和一致性需求。

二、底层机制与原理深度剖析:三种数据库的设计哲学

MySQL 的选择逻辑:数据之间有严格的关联关系,需要 ACID 事务保证一致性。用户的刷题记录需要和用户表、题目表做 JOIN 查询("查询我本周做过的所有动态规划题"),这是关系型数据库的强项。

MongoDB 的选择逻辑:数据结构不确定或频繁变化,文档之间相对独立。题目的标签可能是动态的——今天加了"字节跳动面试题",明天又加了"高频题"。如果用 MySQL,你需要维护 N 对 M 的关系表,Schema 变更成本高。MongoDB 的文档模型天然支持这种动态字段。

Redis 的选择逻辑:数据需要极低延迟(< 1ms)访问,且允许一定的数据丢失(缓存数据可从持久化存储恢复)。排行榜(ZSET)、签到(BITMAP)、会话(String/Hash)——这些数据的热度和延迟要求,是 Redis 的天然场景。

三种数据库不是"谁更好"的比较关系,而是"谁更匹配当前数据的访问特征"的匹配关系。

三、生产级代码实现与最佳实践:分层数据架构实现

""" 刷题系统的分层数据架构 每种数据类型精确匹配一种数据库,不搞"万能解决方案" """ from typing import Dict, List, Optional from dataclasses import dataclass from datetime import datetime, date import json # ==================== MySQL 层:关系型数据 ==================== """ MySQL 负责:用户信息、刷题记录、用户统计 —— 需要 JOIN 查询和事务的数据 """ # SQL Schema 设计 MYSQL_SCHEMA = """ -- 用户表:标准的用户账户信息 CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_username (username) ); -- 刷题提交记录表:核心业务数据,需要按用户和日期联合查询 CREATE TABLE submissions ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, problem_id VARCHAR(20) NOT NULL, language VARCHAR(10), passed BOOLEAN DEFAULT FALSE, time_spent_seconds INT, -- 做题耗时 submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), -- 联合索引:按用户+日期查询是最频繁的访问模式 INDEX idx_user_date (user_id, submitted_at), INDEX idx_problem (problem_id) ); -- 用户统计表:高频更新的汇总数据 CREATE TABLE user_stats ( user_id BIGINT PRIMARY KEY, total_solved INT DEFAULT 0, current_streak INT DEFAULT 0, -- 连续打卡天数 longest_streak INT DEFAULT 0, total_submissions INT DEFAULT 0, -- 为保证数据一致性,和 submissions 表在同一事务中更新 FOREIGN KEY (user_id) REFERENCES users(id) ); """ # MySQL 数据访问实现 class MySQLDataAccess: """MySQL 层的 DAO —— 所有需要 JOIN 和事务的查询都走这里""" def get_weekly_problems_by_topic( self, user_id: int, topic: str ) -> List[Dict]: """ 查询本周做过的某个类型的所有题目 这里需要 JOIN submissions 和 topics 表 是关系型数据库最自然的查询方式 """ # 生产环境中这里使用 SQLAlchemy 或原生 SQL query = """ SELECT DISTINCT s.problem_id, s.passed, s.submitted_at FROM submissions s JOIN problem_topics t ON s.problem_id = t.problem_id WHERE s.user_id = %s AND t.topic = %s AND s.submitted_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) ORDER BY s.submitted_at DESC """ # cursor.execute(query, (user_id, topic)) return [] # 示例 # ==================== MongoDB 层:文档型数据 ==================== """ MongoDB 负责:题目详情、题解内容 —— 半结构化、读多写少的数据 """ @dataclass class ProblemDocument: """题目文档 —— MongoDB 中一道题的完整表示""" problem_id: str title: str difficulty: str # easy/medium/hard description_html: str # 完整题目描述(HTML 格式) tags: List[str] # 动态标签,可能随时增减 similar_problems: List[str] # 相关题目 ID 列表 solutions: List[Dict] # 题解列表(嵌套文档) # 嵌套文档的优势:不需要 JOIN,一次查询拿回所有关联数据 def to_mongo_doc(self) -> Dict: """序列化为 MongoDB 文档 —— 利用文档模型的灵活性""" return { "_id": self.problem_id, "title": self.title, "difficulty": self.difficulty, "description_html": self.description_html, "tags": self.tags, "similar_problems": self.similar_problems, "solutions": self.solutions, "updated_at": datetime.utcnow(), } class MongoDBDataAccess: """MongoDB 层 —— 处理无 Schema 约束的文档数据""" def search_by_tags(self, tags: List[str]) -> List[Dict]: """ 按标签搜索题目 —— MongoDB 的数组查询是原生能力 MySQL 做同样操作需要 tag 关联表 + JOIN """ query = {"tags": {"$all": tags}} # 同时包含所有指定标签 # db.problems.find(query).limit(20) return [] def add_solution(self, problem_id: str, solution: Dict): """ 向题目文档中追加一条题解 MongoDB 文档模型的优势:不需要 ALTER TABLE """ # db.problems.update_one( # {"_id": problem_id}, # {"$push": {"solutions": solution}} # ) pass # ==================== Redis 层:缓存与热数据 ==================== """ Redis 负责:排行榜、会话、签到 —— 高频访问、允许最终一致性 """ class RedisDataAccess: """Redis 层 —— 处理所有低延迟、高并发的数据""" def update_leaderboard(self, user_id: int, score: int): """ 更新排行榜 —— 使用 Sorted Set 时间复杂度:O(log N),毫秒级响应 """ # redis.zadd("leaderboard:weekly", {str(user_id): score}) def get_leaderboard(self, top_n: int = 100) -> List[tuple]: """ 获取排行榜前 N 名 —— ZREVRANGE 直接返回 如果用 MySQL 做同样的事,需要 ORDER BY + LIMIT 在千万行数据上扫描 """ # return redis.zrevrange("leaderboard:weekly", 0, top_n - 1, withscores=True) return [] def mark_daily_checkin(self, user_id: int): """ 每日签到 —— 使用位图 一个用户 365 天的签到数据仅需 46 字节 """ # day_of_year = datetime.now().timetuple().tm_yday # redis.setbit(f"checkin:{2026}", user_id, 1) pass def cache_session(self, session_id: str, user_data: Dict, ttl: int = 3600): """ 缓存用户会话 —— 设置过期时间 """ # redis.setex(f"session:{session_id}", ttl, json.dumps(user_data)) pass

三层数据架构的精髓不在于用了多少种数据库,而在于每种数据找到了最匹配的存储方式。排行榜用 Redis Sorted Set(1 行命令),在 MySQL 中需要复杂的 SQL。题目标签搜索用 MongoDB 的数组查询(原生支持),在 MySQL 中需要额外的关联表和 JOIN。

四、边界分析与架构权衡:全用 MySQL 行不行

对于一个用户量 < 100 的刷题系统,全用 MySQL 是完全可以的。引入多种数据库的代价(运维复杂度、数据一致性协调、学习成本)远超它们的收益。这个判断是本文最重要的 trade-off 结论。

只有当你遇到以下真实瓶颈时,才引入新数据库:

  1. 排行榜查询超过 500ms,且请求量大(引入了 Redis 做排行榜缓存)
  2. 题目的标签和分类频繁变化导致 MySQL Schema 变更困难(引入 MongoDB)
  3. 用户活跃数据的高频写入影响主库性能(引入 Redis 做写入缓冲)

在没有遇到这些瓶颈之前,多数据库 = 过度设计。一个实习生开发的小型系统,MySQL 能搞定所有需求。多数据库架构是给"已经遇到了瓶颈的系统"的升级方案,而不是给"还没开始开发的系统"的初始设计。

结论

数据库选型的核心原则是"用最少的数据库种类满足真实的数据需求"——而不是"为每一类数据分配一个专用数据库"。MySQL 作为主力存储足以覆盖小型刷题系统 90% 的需求。MongoDB 解决的是结构不确定的文档数据问题,Redis 解决的是低延迟热数据问题。这两个需求不是必然存在的。

对实习生来说,掌握"MySQL 作为主力数据库,Redis 作为缓存层"这个组合,已经覆盖了个人项目和中小型后端系统的绝大多数场景。MongoDB 的引入是在"数据确实不适合关系模型"时才会考虑的选项。

选型时不妨问自己一个问题:如果只用 MySQL 来实现,会遇到什么不可接受的困难?如果答案是没有——那就只用 MySQL。

相关新闻

  • 三、constexpr
  • 2026 天津和平区别墅卫生间防水机构推荐 10 家 合规资质甄选型录、实力维度测评及合作避坑全攻略 - 鼎万建筑修缮
  • 解决传统竞价广告成本高问题,高宇GEO适合谁

最新新闻

  • STM32 采集效率翻倍!ADC+DMA 乒乓缓冲 + FreeRTOS 并行架构全拆解
  • 从纸质家政合同到电子签,家政公司远程签完服务协议
  • 从入门到精通:React Native Modern Datepicker开发实战指南
  • 快速搭建 xiao/xiaozhi 环境:5分钟启动你的AI语音助手
  • 提示词失效?你可能正踩中这5个隐性陷阱:20年CV/NLP双领域专家拆解AI绘画语义解析机制
  • Java 编程语言入门指南

日新闻

  • 金融舆情监测系统:多语言情感分析与实时可视化技术解析
  • QT C++调用Python异常处理:PyBind11实战与跨语言编程指南
  • A-47双麦回音消除模块:主次麦空间分布与差分连接对ENC性能的影响

周新闻

  • 大连理工大学与东京大学联手打造的“主动型AI助手“
  • 170.2026年国家级科研瓶颈:超精密单点金刚石切削(SPDT)光学表面生成
  • SongBloom:革命性歌曲生成框架深度解析——如何通过交织自回归与扩散模型创作完整音乐

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号