马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?用户注册
×
NoSQL 选型:MongoDB 与 Redis 场景对比——别再把缓存当数据库用
一、具体的问题
不少团队一听到"高并发""要快",就顺手把 Redis 或者 MongoDB 搬上来,但两者根本不是一类东西:一个是内存中的键值/数据结构存储,一个是落盘的文档数据库。把 Redis 当主库用,结果一重启数据全空;把 MongoDB 当缓存用,结果热点查询被磁盘 IO 拖慢。本文聚焦一个最实在的问题:面对一个具体业务,到底该选 MongoDB 还是 Redis,什么时候该两个一起上,以及怎么搭配才不踩坑。
二、核心原理
1. 先分清"持久化文档库"和"内存数据结构库"
MongoDB 是文档数据库,数据以 BSON 文档存在磁盘上,提供丰富的查询、聚合和索引能力,适合作为系统的"主存储"存放业务实体(订单、用户资料、设备采集点)。Redis 是内存数据库,所有数据驻留在 RAM,读写微秒级,但容量受内存限制,设计初衷是缓存、会话、计数器、消息队列这类"快进快出"的场景。一句话:MongoDB 管"存得下、查得清",Redis 管"快"。
2. 典型职责边界
- MongoDB 适合:数据结构半固定、需要按多字段检索和聚合的业务数据;单文档几 KB 到几 MB;容忍毫秒级延迟。例如商品目录、工单、埋点明细。
- Redis 适合:高频读、可丢失或能重建的临时数据;需要原子计数、排行榜、发布订阅、分布式锁。例如登录会话、首页热点缓存、秒杀库存、限流计数。
- 两者配合:MongoDB 做源库,Redis 做前置缓存。读请求先打 Redis,命中直接返回;未命中回源 MongoDB 并回填缓存。写请求落 MongoDB,再失效(或更新)对应 Redis 键。
3. 一个常见的翻车现场
某系统把用户余额直接写在 Redis 里,没配持久化策略,半夜机房一次内存过载触发驱逐(maxmemory-policy 设成了 allkeys-lru),余额键被清掉,早上用户发现钱"归零"。根因是把"不该丢的数据"放进了默认可丢的内存库,又没有 AOF/RDB 兜底,也没有把余额作为权威值落盘到 MongoDB。缓存只该放"丢了能重建"的数据。
4. Redis 持久化的取舍
Redis 不是不能持久化:RDB 定时快照、AOF 逐条记命令。但若真要"不能丢",就得 AOF everysec 甚至 always,这会让写入退化到磁盘速度,失去内存库的意义。所以工程上共识是:Redis 当缓存/辅助,权威数据放 MongoDB 等落盘库;Redis 持久化只用于"宕机后快速预热",而不是当作唯一真相源。
三、实例参考(动手步骤)
下面给出一套"MongoDB 作主库 + Redis 作缓存"的最小可照做方案,并用 redis-cli / mongosh 演示关键操作。
1) MongoDB 存权威文档(商品信息),建立常用查询索引:
- // mongosh
- use shop;
- db.products.insertOne({ sku:"A100", name:"机械键盘", price:399, stock:120, updatedAt:new Date() });
- db.products.createIndex({ sku:1 }); // 按 sku 检索
- db.products.createIndex({ price:1 }); // 按价格范围聚合
复制代码
2) Redis 做热点缓存,写入带过期时间,避免脏数据常驻:
- # redis-cli
- SET cache:product:A100 '{"name":"机械键盘","price":399,"stock":120}' EX 300
- # 读路径:先查缓存
- GET cache:product:A100
复制代码
3) 读路径伪代码(先缓存后源库, miss 回填):
- val = redis.get(key)
- if val == nil:
- doc = mongo.products.findOne({sku: sku}) # 回源
- redis.set(key, serialize(doc), ex=300) # 回填,300s 过期
- return doc
- else:
- return deserialize(val)
复制代码
4) 写路径:先更 MongoDB,再删缓存键(Cache-Aside 失效而非更新,避免并发写乱序):
- mongo.products.updateOne({sku:sku}, {$set:{price:379, updatedAt:now}})
- redis.del("cache:product:" + sku) # 失效,下次读自动回填新值
复制代码
5) 验证对比:未加缓存前,压测 find({sku:"A100"}) 在机械盘上 P99 约 8ms;套上 Redis 后热点命中 P99 降到 0.3ms 以内,MongoDB 读压力下降约 70%(监控 db.serverStatus().opcounters 的 query 计数)。若把 Redis 关掉(模拟驱逐),业务仍能从 MongoDB 正常读出,只是变慢——这正是"缓存可丢、源库不丢"该有的韧性。
前后对比:方案落地前,所有读直接打 MongoDB,大促时磁盘 IO 打满、接口超时;落地后热点走 Redis,MongoDB 只扛写和冷数据,超时消失,且 Redis 重启不影响数据正确性(能从 MongoDB 重建)。
四、实操检查清单
- 这份数据"丢了能不能重建"?能重建的才进 Redis,不能丢的必须落 MongoDB 等持久库。
- Redis 是否设置了合理的 maxmemory 与淘汰策略(如 allkeys-lru),并清楚知道它会主动清键?
- 缓存与源库的一致性策略是否明确:写时失效(del)还是更新?是否接受短暂脏读?
- 缓存键是否带 TTL,避免脏数据永久驻留、内存只涨不跌?
- MongoDB 是否按真实查询模式建了索引(explain() 看是否 IXSCAN 而非 COLLSCAN)?
- 是否用 Cache-Aside:读 miss 回填、写后失效,且失效操作放在源库提交之后?
- 是否做过"Redis 宕机"演练:业务能否仅靠 MongoDB 继续服务,只是变慢?
- 大文档是否拆分?MongoDB 单文档建议 < 16MB,超大的考虑 GridFS 或对象存储。
- 是否监控 Redis 内存水位与命中率(INFO memory、INFO stats 的 keyspace_hits/misses)?
- 需要原子计数/排行榜/分布式锁时,是否直接用 Redis 的原生结构(INCR / ZADD / SET NX)而非自己实现?
五、一点经验
选型不是"用不用 NoSQL",而是"谁在什么层干什么活"。把 MongoDB 当唯一的快存储、或把 Redis 当唯一真相源,都是把工具用错地方。稳妥的默认是:MongoDB 扛业务实体与复杂查询,Redis 守在前面扛热点与原子操作,二者通过"缓存可丢、源库不丢"的边界隔离风险。先把这条边界画清楚,再谈性能优化,系统才既快又稳。 |