
每次技术评审会只要聊到数据存储总会有人抛出一个经典问题MySQL、Redis、MongoDB到底怎么选问的人通常还会补半句——“我们新项目要上线数据量不小并发也不低怕以后撑不住。”我理解这种焦虑但说实话这个问法本身就有问题。选型不是在一堆数据库里挑“最强”的那个而是在业务特性、数据模型、一致性要求、团队熟悉度之间做匹配。MySQL、Redis、MongoDB分别解决的是完全不同类型的问题把它们放在一个擂台上比高低就像拿账本、便签板和陈列柜比谁更“好用”一样没有意义。这篇内容我不打算给你一份教科书式的功能对比表而是从几个真实业务场景出发讲讲我过去在这些数据库上踩过的坑、做过的决定以及一套可以直接套用的选型判断框架。1. 先搞清楚这三个数据库到底在解决什么问题很多选型错误根源不是技术能力不够而是没想明白“数据到底长什么样、怎么被读写”。所以在聊场景之前必须先把三者的本质差异拉清楚。1.1 MySQL关系模型与强事务的“业务账本”MySQL以及同类的传统关系型数据库核心优势是两件事关系模型和事务。关系模型意味着你能用外键、JOIN、约束把这些数据之间的关联关系表达得非常严密。比如订单表关联用户表、订单明细表关联商品表这种一对多、多对多的结构用关系模型天然契合。数据从写入的第一秒开始就处于一种“被约束”的状态主键唯一、外键一致、字段长度有限、状态值有限。这种约束看起来很死板但它换来的是数据的高可信度。事务则解决了“多步操作要么全成功、要么全失败”的问题。扣减库存和创建订单必须同时生效不能出现订单建好了库存没扣的中间状态。ACID这四个特性尤其是原子性和持久性让MySQL成为承载核心资金、交易、用户资产这类数据的不二选择。但关系型模型的代价也在这里扩展性。一旦数据量到了单机瓶颈做分库分表的复杂度会直线上升JOIN在数据量上去之后性能会明显下降表结构如果需要频繁变更每一次ALTER TABLE都可能变成一次上线事故。1.2 Redis内存速度与数据结构的“加速层”Redis的核心是两点数据全在内存里以及丰富的数据结构。内存决定了它的读写性能可以做到微秒到毫秒级单实例QPS轻松上万甚至更高。但这个“快”是有代价的——内存比磁盘贵得多数据量受物理内存限制。所以Redis最合理的定位是“加速层”而非“存储层”你把访问频率最高的那部分数据放进Redis把全量数据放在更便宜的持久化存储里。Redis的数据结构是它的另一大杀手锏String、Hash、List、Set、ZSet每种结构都对应一批原子操作。排行榜用ZSet计数用INCR分布式锁用SET NX EX消息队列可以用List或Stream这些操作配合原子性可以省掉大量业务代码。但那些把Redis当作唯一存储、所有数据都往里塞的团队最后基本都会在内存成本、数据丢失风险这两堵墙上撞得头破血流。1.3 MongoDB文档模型与水平扩展的“灵活容器”MongoDB的核心优势则是文档模型和水平扩展能力。文档模型把一条业务数据完整地存在一起比如一个用户的完整画像、一篇文章的全部内容、一台IoT设备上报的一条记录。这种“聚合”式的数据模型特别适合那些字段不固定、结构经常变化的业务。你不需要像关系型数据库那样先设计几十个字段、做复杂的迁移而是想到什么字段就加什么字段。JSON式的读写体验对开发效率的提升非常明显。同时MongoDB天生支持分片集群可以把数据自动分散到多台机器上对水平扩容友好。这让它在大数据量、高写入吞吐的场景下比MySQL的“单机性能再好也有上限”轻松得多。代价则是事务能力较弱。MongoDB虽然在4.0版本之后引入了多文档事务但性能和适用场景仍然不如MySQL成熟对复杂关联查询的支持也弱。它不适合作为强一致性核心账务系统的首选。1.4 一个能记住的类比我自己经常用这个类比向团队解释MySQL是财务部那本总账每一笔都得有凭有据、流程严密出错了要能审计Redis是前台摆的速查卡热门信息写到卡片上一翻就到丢失了可以重新抄一份MongoDB是仓库的灵活货架货品什么形状都能放等你想好标准化流程了再挪进总账。业务里到底哪本账更重要、哪些信息被高频翻看、哪些数据形态还不成熟——把这三类分清楚选型方向基本不会跑偏。2. 从真实业务场景看选型三个案例拆解理论说再多不如直接看场景。下面三个案例都是我在实际项目里接触过的类型业务特征非常典型我们一个一个拆开看。2.1 电商交易系统MySQL为什么是绝对核心先看一个最经典的电商订单场景。这里有用户、商品、购物车、订单、订单明细、支付流水、优惠券等实体彼此之间的关系非常清晰一个订单属于一个用户一个订单包含多个订单明细一个订单关联一条或多条支付流水一张优惠券可以被某个用户在某笔订单上使用。这种强关系模型加之一旦涉及支付、库存、对账就必须保证强一致MySQL是当之无愧的核心。具体落地时我一般会这样设计CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR(32) NOT NULL COMMENT 订单号, user_id BIGINT NOT NULL COMMENT 用户ID, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态0待支付1已支付2已发货3已完成4已取消, total_amount DECIMAL(10, 2) NOT NULL COMMENT 订单总金额, address_snapshot JSON NOT NULL COMMENT 收货地址快照, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;为什么用订单号唯一索引、用户ID索引、状态和时间组合索引因为订单系统的查询模式非常固定用户查自己的订单列表、后台按状态和时间筛选订单。索引设计必须跟着查询走面面俱到的索引不如精准命中。下单动作则需要开启事务锁定库存、扣减库存、创建订单、生成支付单任何一步失败都要回滚。MySQL的InnoDB在这里如鱼得水。如果非要用MongoDB做订单主库且数据量一旦上来就做分片跨分片的事务和关联查询会给你带来灾难级复杂度。MySQL在这个场景的唯一问题是订单表会越来越大。我的经验是不要过早做分库分表而是先做冷热分离半年前的订单归档到历史库线上只保留活跃订单。等单表数据真正突破千万级、查询明显变慢时再按用户ID分库也不迟。过早引入分库分表中间件会让查询、事务、统计SQL全部变形成本极高。2.2 秒杀与排行榜Redis是内存里的“快刀手”电商系统里最典型的高并发场景是秒杀和排行榜。这两类场景的共同特征是读多写少、瞬时流量大、单条数据极小、允许一定程度的最终一致。用MySQL硬扛的团队往往在活动开始几秒内就把数据库连接打满。秒杀场景最简单的方案是把商品库存预加载到Redis里用原子扣减命令完成库存检查与扣减。SET seckill:stock:1001 100 DECR seckill:stock:1001DECR命令是原子的多个线程同时执行也不会超卖。扣减成功后再异步创建订单。至于数据库只负责记录最终的订单结果。这个设计不是让Redis替代MySQL而是让Redis挡住99%的瞬时流量MySQL只需要承受“真正下单成功”的那部分请求。排行榜场景则直接使用ZSet。有序集合天然支持按分数排序用来做实时榜单再合适不过。ZADD leaderboard:daily 100 user_9527 ZINCRBY leaderboard:daily 5 user_9527 ZREVRANGE leaderboard:daily 0 9 WITHSCORES一行命令就实现了分数更新、排名查询。换成MySQL实现同样的榜单逻辑要反复做GROUP BY和ORDER BY并发一高性能立刻崩。这个场景选Redis不是因为它“比MySQL高级”而是它的数据结构天生就对这个问题建模了。但这里有一条铁律Redis里的数据必须是“可重建的”。缓存丢了可以从DB加载计数器可以从操作日志重放排行榜可以从历史流水重算。凡是丢失后无法恢复的数据都不应该只放在Redis里。我见过有团队把用户钱包余额直接放Redis一拍脑袋觉得这玩意都快结果一次宕机丢了一部分数据最后耗费几周时间人工对账。这种教训足够深刻——Redis是加速器不是保险箱。2.3 物联网设备数据与用户画像MongoDB的天然主场再举一个和上面两种场景气质完全不同的案例物联网设备上报数据。设备每次上报包含设备ID、时间戳、GPS坐标、温度、湿度、电池电量等几十个字段而且不同型号的设备上报字段不一样。同一批设备里A型号有“倾斜角度”B型号没有新固件版本新增了“震动频率”老设备永远不会有。如果用MySQL建一张大宽表每新增一个字段都要做ALTER TABLE几十张设备型号的表会让开发崩溃。这种场景用MongoDB非常顺手。一个文档天然对应一条上报记录字段随心所欲db.device_reports.insertOne({ device_id: gw-2048, ts: ISODate(2025-01-15T08:30:00Z), gps: { lat: 31.2304, lng: 121.4737 }, temperature: 36.5, humidity: 42.3, battery: 87, firmware: v3.2.1, extras: { tilt_angle: 12.5 } })查询也很直接按设备ID和时间范围建好索引db.device_reports.createIndex({ device_id: 1, ts: -1 })数据量大了以后按时间维度做分片MongoDB会自动把数据分散到多个分片节点。这种水平扩展能力让它在“海量写入、分析型查询、字段灵活”的赛道上优势显著。用户画像系统同理。一个用户的标签可能有几十个还不确定明天产品经理会不会新增标签用MySQL就要不断加列而文档模型加一个字段只是JSON里的一个key。把画像数据放进MongoDB把画像的查询结果缓存到Redis是常见且稳健的组合。3. 拿来就用的选型方法论上面三个案例讲完你应该已经感觉到选型没有标准答案但有规律。规律就是把血淋淋的话总结成一套判断框架。3.1 五个核心判断维度我先说结论再解释每个维度。判断维度MySQL优先的场景Redis优先的场景MongoDB优先的场景数据关系强关系、多表关联多、外键约束多无关系或仅简单的key引用聚合性强、字段嵌套深、关系少事务要求强一致ACID资金、库存、订单仅需单个操作原子性最终一致可接受简单事务即可读写模式读少写多、复杂查询多读多写少、单key访问、热点高并发海量写入、条件查询、字段不固定数据规模千万级单表依赖分库分表受内存限制数据量小但极热TB级起步分片扩展开发与运维团队最熟、生态最全、工具链成熟运维轻量但内存监控、持久化策略要上心部署较复杂需维护分片、均衡器这个表不是让你逐项打分而是帮你定位业务的主要矛盾。比如电商订单事务要求、数据关系两项直接锁定MySQL秒杀读写模式一项直接锁定Redis物联网采集数据规模和字段灵活性直接锁定MongoDB。如果好几个维度都属于同一列答案就呼之欲出了。3.2 选型决策步骤我把自己的决策过程拆成五个步骤你可以直接照做。第一步列出业务最核心的三种操作。比如订单系统的核心操作是“创建订单”“查我的订单”“后台按状态筛订单”。不要列一百种操作只列最高频、最关键的那三种。第二步判断这三种操作对应的数据模型。是关系复杂、必须JOIN吗还是一个独立文档就能搞定如果是前者MySQL如果是后者再看其他条件。第三步判断一致性需求。涉及钱、货、账户余额必须强一致就锁死MySQL如果只是用户头像、文章热度、商品浏览数允许短暂延迟就可以用Redis或MySQL异步更新不必上分布式事务。第四步估算数据量级与增速。日增100万条、半年突破亿级的写入场景优先MongoDB或列式存储日增几万条MySQL单表撑两三年没压力就别提前自虐搞分库分表。第五步评估团队能力。一个从没运维过MongoDB分片集群的团队贸然上大集群光是学习成本和线上事故就能把效率优势吃光。这不是说不能学而是要把学习成本计入选型成本。很多时候团队最熟的技术就是当前最优解。3.3 一个容易被忽略的问题混合存储的成本这里要泼一盆冷水当你决定用“组合拳”的时候代码复杂度不是11而是11再乘以一个连接符。MySQL存主数据、Redis做缓存、MongoDB存灵活扩展信息听起来很完美但代价是一条业务数据要写三个地方任何一处失败都面临不一致风险查询时要分别访问三个系统再合并结果统计口径可能要跨三个库对齐。所以混合架构必须明确边界——哪些表/集合归MySQL管哪些key归Redis管哪些集合归MongoDB管。千万不要出现同一个字段一会儿在MySQL、一会儿在MongoDB的情况那是灾难的开端。4. 混合架构实践最常见的三种组合现实中的系统很少只用一种数据库。下面三种组合是我见到频次最高的也是相对稳健的搭配。4.1 MySQL Redis最稳的组合拳这是最经典、最不容易出错的组合MySQL存全量数据Redis缓存热点数据。具体做法通常是这样读请求先查Redis没命中就查MySQL把结果回填Redis设置过期时间写请求只写MySQL然后主动删除Redis里的对应缓存。等待下一次读请求再回填。这个策略业界叫Cache Aside投入产出比极高。# 伪代码读缓存 def get_order(order_no): cache_key forder:{order_no} order redis.get(cache_key) if order: return json.loads(order) order mysql.query_one(SELECT * FROM orders WHERE order_no%s, order_no) if order: redis.setex(cache_key, 3600, json.dumps(order)) return order def update_order(order_no, new_status): mysql.execute(UPDATE orders SET status%s WHERE order_no%s, new_status, order_no) redis.delete(forder:{order_no})这里最大的坑是缓存一致性。写过数据库之后如果直接更新缓存而不是删除缓存在高并发下会出现老数据把新数据覆盖的问题。我的建议很简单更新数据库后永远删除缓存而不是写缓存。删除失败的兜底方案是给缓存设置相对短的过期时间例如30到60分钟这是最后一道防线。第二个坑是缓存穿透。当一个不存在的ID被大量请求时每次都会穿过Redis打到数据库。解决办法是缓存空值或者用布隆过滤器前置拦截。4.2 MySQL MongoDB结构化核心与灵活扩展适合那种“核心交易数据必须强一致但又有一大堆附属信息和灵活扩展字段”的业务。举个例子电商订单主表在MySQL但订单中的商品快照、售后详情、买家备注、扩展属性等字段繁多且可能因业务调整不断变化这些可以放进MongoDB的order_ext集合中。MySQL订单表只保存稳定的核心字段MongoDB存与主订单关联的扩展信息。这种设计的好处是MySQL表结构不会频繁变动核心事务性能稳定MongoDB侧字段随便加不影响已有逻辑。代价是查询订单详情时需要先查MySQL拿订单基础信息再查MongoDB拿扩展字段代码里多了两次数据源操作。为了减少这种拼接成本可以在MySQL的orders表里冗余一个JSON字段存一份高频使用的扩展信息摘要MongoDB只保存低频或超大的附加数据。4.3 Redis MongoDB面向海量写入的提速组合物联网、埋点日志这类场景经常这么搭MongoDB持久化全量数据Redis做实时计数和最新值缓存。比如设备状态监控每一台设备的实时状态写入Redis用Hash或者String按设备ID存储历史数据落MongoDB供后续分析查询。因为实时状态只保留当前值数据量小、访问快历史数据是海量写入MongoDB分片集群慢慢扛。监控面板查Redis毫秒出数报表分析查MongoDB慢慢跑互不干扰。这个组合要注意的是数据重建机制。Redis重启后实时状态需要从MongoDB最近一条记录中恢复。因此数据写入顺序是先写MongoDB再更新Redis。如果反过来Redis写成功了MongoDB写失败重启后状态就丢了。5. 选型落地时的常见坑与排查技巧即便选型方向对了落地过程中还是有一堆细节问题。我把踩过的和见过的典型问题整理一下每条都是真实事故换来的经验。5.1 把Redis当成唯一数据源症状是缓存里的数据丢了就真丢了没有任何途径找回。排查思路很简单问自己一个“如果这个key消失业务能不能自动恢复”。不能自动恢复的就必须在Redis之外有持久化副本。这是个判断题不是技术题。Redis的持久化机制RDB和AOF只是降低丢失概率不是保险箱真正保险的是链路里有MySQL或MongoDB兜底。5.2 MongoDB当作MySQL用具体表现为设计了十几个互相引用的集合业务代码里到处做应用层JOIN对单个文档频繁更新小块字段依赖跨文档事务处理复杂业务。这时候性能一定很难看——因为MongoDB不适合这种用法。排查思路是回头审视你的数据模型如果实体之间的关系需要在事务内保持强一致那它更适合关系型数据库如果MongoDB已经在线上跑着短期内无法切换可以做两件事一是用冗余字段替代引用关联让单文档自包含二是缩小事务范围尽量在单个文档内完成更新。5.3 MySQL索引设计随意症状是数据量到了几百万查询开始变慢DBA一查发现全是全表扫描。排查方法不复杂用EXPLAIN查看执行计划看Extra列是不是出现了Using filesort或Using temporary看type列是不是ALL。解决手段集中在两点第一为高频查询的WHERE条件和ORDER BY字段建联合索引第二遵循最左前缀原则不要给每个字段单独建索引。注意如果线上表已经有大量数据加索引时避免直接ALTER TABLE锁全表可以考虑用在线DDL工具或分批处理。5.4 过度追求“可用性”而忽略一致性有段时间技术圈特别推崇最终一致性好像任何场景都可以用“消息异步”解决。但资金、订单、库存这类核心链路最终一致性意味着对账系统要处理无穷无尽的中间状态。我的经验是核心链路用强事务保证简单直接“非核心”的边缘场景才做异步化。不要为了追求技术潮流增加系统的复杂度。5.5 监控与备份缺失很多团队选型时很兴奋上线后却忘了监控和备份。MySQL至少要有慢查询日志、主从复制延迟监控和定期全量备份Redis至少要有内存使用率报警、命中率监控和持久化策略MongoDB至少要有分片均衡状态检查和备份恢复演练。没有监控的数据库就像没有仪表盘的汽车早晚出事。备份策略要定期演练恢复而不是只把备份文件存起来就完事。我见过备份文件损坏导致无法恢复的事故所以至少要保证每季度真正从备份里拉起一次完整的恢复流程。6. 关于选型的一点个人体会做了这么多年项目数据库选型这个问题我最大的体会就是不要迷信什么“流行度排行榜”也不要因为某篇文章推荐了某个数据库就全员切换。选型是一个典型的“没有最优、只有最匹配”的决策。MySQL、Redis、MongoDB每个都非常优秀但它们擅长的事情不一样。把处理账务的交给MySQL把顶住高并发的交给Redis把灵活可变的海量数据交给MongoDB已经可以覆盖绝大多数业务场景。最后再分享一个我自己经常用的判断规则新项目的第一个版本优先选择你团队犯错成本最低的方案。最熟练的技术选型就是最好的技术选型。至于那些看起来很酷的新数据库完全可以在非核心模块里试点等团队真正积累出足够的运维经验和踩坑教训后再逐步扩大到核心链路。数据库迁移永远比一开始选对贵得多这是一句用无数次加班换来的实话。