一、为什么需要MongoDB和Redis协同工作

在现代Web应用中,数据存储和缓存的需求往往不是单一技术能够完美解决的。MongoDB作为文档型数据库,擅长处理结构化或半结构化的海量数据,而Redis作为内存数据库,则以超高的读写速度著称。把这两者结合起来,可以构建一个既具备持久化能力又拥有极高性能的缓存层。

举个例子,假设我们正在开发一个电商平台,商品详情页需要频繁访问,但每次从MongoDB直接读取显然效率不够。这时候,Redis就能派上用场——我们可以把热点数据缓存在Redis中,减轻MongoDB的压力。

// 技术栈:Node.js + MongoDB + Redis
const express = require('express');
const mongoose = require('mongoose');
const redis = require('redis');

// 连接MongoDB
mongoose.connect('mongodb://localhost:27017/ecommerce');
const Product = mongoose.model('Product', { name: String, price: Number });

// 连接Redis
const redisClient = redis.createClient({ host: 'localhost', port: 6379 });

const app = express();

// 获取商品详情,优先从Redis读取
app.get('/product/:id', async (req, res) => {
    const { id } = req.params;
    
    // 先查Redis
    redisClient.get(`product:${id}`, async (err, cachedData) => {
        if (cachedData) {
            return res.json(JSON.parse(cachedData)); // 命中缓存,直接返回
        }
        
        // 未命中缓存,从MongoDB读取
        const product = await Product.findById(id);
        if (!product) return res.status(404).send('Product not found');
        
        // 写入Redis,设置10分钟过期
        redisClient.setex(`product:${id}`, 600, JSON.stringify(product));
        res.json(product);
    });
});

app.listen(3000, () => console.log('Server running on port 3000'));

这个示例展示了如何优先从Redis读取数据,未命中时再查询MongoDB,并将结果缓存到Redis。

二、MongoDB和Redis的适用场景分析

1. MongoDB的强项

  • 灵活的数据模型:适合存储商品信息、用户资料等结构多变的数据。
  • 复杂查询:支持索引、聚合管道等高级查询功能。
  • 水平扩展:通过分片机制可以轻松应对数据增长。

2. Redis的强项

  • 超高性能:内存操作,读写速度极快。
  • 数据结构丰富:支持字符串、哈希、列表、集合等,适合缓存、计数器、排行榜等场景。
  • 原子性操作:比如INCR、HINCRBY等命令,适合高并发场景。

3. 协同使用的典型场景

  • 热点数据缓存:如商品详情、用户会话信息。
  • 计数器与排行榜:用Redis的INCR和ZSET实现,最终结果持久化到MongoDB。
  • 消息队列:Redis的List或Stream可以作为轻量级队列,MongoDB存储最终数据。

三、实战:构建一个完整的缓存策略

让我们通过一个更完整的例子,展示如何用MongoDB和Redis实现一个带缓存失效机制的订单系统。

// 技术栈:Node.js + MongoDB + Redis
const { promisify } = require('util');
const redisClient = redis.createClient();
const getAsync = promisify(redisClient.get).bind(redisClient);
const setexAsync = promisify(redisClient.setex).bind(redisClient);

// 订单服务
class OrderService {
    // 获取订单详情
    async getOrder(orderId) {
        // 先查Redis
        const cachedOrder = await getAsync(`order:${orderId}`);
        if (cachedOrder) return JSON.parse(cachedOrder);
        
        // 查MongoDB
        const order = await Order.findById(orderId);
        if (!order) throw new Error('Order not found');
        
        // 写入Redis,缓存1小时
        await setexAsync(`order:${orderId}`, 3600, JSON.stringify(order));
        return order;
    }
    
    // 更新订单状态(同时失效缓存)
    async updateOrderStatus(orderId, newStatus) {
        // 更新MongoDB
        const order = await Order.findByIdAndUpdate(
            orderId, 
            { status: newStatus },
            { new: true }
        );
        
        // 删除Redis缓存
        redisClient.del(`order:${orderId}`);
        return order;
    }
}

这个例子展示了缓存读取和写入的基本流程,特别注意在数据更新时主动失效缓存,避免脏数据问题。

四、注意事项与最佳实践

  1. 缓存雪崩:大量缓存同时失效导致请求直接打到数据库。解决方案:设置不同的过期时间,比如基础时间加上随机偏移量。
  2. 缓存穿透:查询不存在的数据,导致每次都不命中缓存。解决方案:对空结果也进行短时间缓存。
  3. 数据一致性:确保缓存与数据库的最终一致性,可以通过消息队列或定时任务同步。
  4. 内存管理:Redis是内存数据库,需要监控内存使用情况,避免OOM。
// 防止缓存穿透的示例
async function getProductSafe(id) {
    const cacheKey = `product:${id}`;
    const cached = await getAsync(cacheKey);
    
    // 特别注意:缓存空值(特殊标记)
    if (cached === 'NULL') return null; 
    if (cached) return JSON.parse(cached);
    
    const product = await Product.findById(id);
    if (!product) {
        // 缓存空值5分钟
        await setexAsync(cacheKey, 300, 'NULL');
        return null;
    }
    
    await setexAsync(cacheKey, 600, JSON.stringify(product));
    return product;
}

五、总结

MongoDB和Redis的协同使用,就像让马拉松选手和短跑运动员组队接力——MongoDB负责长期稳定的数据存储,Redis则处理高并发的瞬时请求。合理设计缓存策略,可以大幅提升系统性能,但也要注意缓存一致性、雪崩等问题。

在实际项目中,建议根据业务特点灵活调整缓存时间、数据结构,并配合监控工具及时发现问题。记住,没有放之四海而皆准的方案,只有最适合当前场景的设计。