在Node.js和MongoDB的开发场景中,很多业务需要文档过期后触发自定义通知逻辑,比如用户优惠券过期提醒、临时会话失效通知等。MongoDB本身提供了TTL索引可以自动删除过期文档,但无法直接执行自定义通知逻辑,因此需要结合Change Stream监听文档变化来实现过期通知。

一、核心实现原理
整体实现分为两个核心步骤:
- 第一步:为MongoDB集合创建TTL索引,指定文档的过期时间字段,让MongoDB在文档过期时自动删除对应文档。
- 第二步:使用Node.js连接MongoDB,通过Change Stream监听目标集合的删除操作,当捕获到由TTL过期触发的删除事件时,执行自定义的通知逻辑。
二、环境准备
需要提前准备以下内容:
- 安装Node.js运行环境,版本建议14及以上。
- 安装MongoDB数据库,版本建议4.0及以上,因为Change Stream在4.0版本后支持更多场景。
- 初始化Node.js项目,安装官方MongoDB驱动:
npm install mongodb
三、创建TTL索引设置文档过期
首先需要连接到MongoDB,为目标集合创建TTL索引。假设我们有一个<coupon>集合,存储用户优惠券信息,其中<expireAt>字段表示优惠券的过期时间,我们需要在这个字段上创建TTL索引。
以下是创建TTL索引的Node.js代码:
const { MongoClient } = require('mongodb');
// 连接MongoDB的字符串,本地默认地址为127.0.0.1:27017
const url = 'mongodb://127.0.0.1:27017';
const client = new MongoClient(url);
async function createTTLIndex() {
try {
await client.connect();
const db = client.db('test_db');
const couponCollection = db.collection('coupon');
// 在expireAt字段上创建TTL索引,expireAfterSeconds设为0表示文档的expireAt时间到达后立即过期
const result = await couponCollection.createIndex(
{ expireAt: 1 },
{ expireAfterSeconds: 0 }
);
console.log('TTL索引创建成功,索引名称:', result);
} finally {
await client.close();
}
}
createTTLIndex().catch(console.error);
插入测试过期文档的代码示例:
async function insertExpireCoupon() {
try {
await client.connect();
const db = client.db('test_db');
const couponCollection = db.collection('coupon');
// 插入一个1分钟后过期的优惠券文档
const expireTime = new Date(Date.now() + 60 * 1000);
const result = await couponCollection.insertOne({
userId: 'user_123',
couponId: 'coupon_456',
amount: 50,
expireAt: expireTime
});
console.log('插入过期测试文档成功,文档ID:', result.insertedId);
} finally {
await client.close();
}
}
四、监听文档过期删除事件实现通知
MongoDB的TTL索引过期删除文档时,会触发delete事件,我们可以通过Change Stream监听这个事件,然后执行通知逻辑。需要注意的是,Change Stream需要MongoDB运行在副本集模式下,如果是本地测试,可以启动单节点副本集。
以下是监听删除事件并执行通知的完整代码:
const { MongoClient } = require('mongodb');
const url = 'mongodb://127.0.0.1:27017';
const client = new MongoClient(url);
async function watchExpiredDocuments() {
try {
await client.connect();
const db = client.db('test_db');
const couponCollection = db.collection('coupon');
// 开启Change Stream,只监听delete操作
const changeStream = couponCollection.watch([
{ $match: { operationType: 'delete' } }
]);
console.log('开始监听优惠券文档过期删除事件...');
// 遍历Change Stream的事件
for await (const change of changeStream) {
// 获取被删除文档的ID
const deletedDocumentId = change.documentKey._id;
console.log('捕获到文档删除事件,文档ID:', deletedDocumentId);
// 这里可以执行自定义的通知逻辑,比如发送短信、推送站内信等
// 示例:模拟发送过期通知
sendExpirationNotification(deletedDocumentId);
}
} catch (error) {
console.error('监听Change Stream出错:', error);
} finally {
await client.close();
}
}
// 模拟发送过期通知的函数
function sendExpirationNotification(documentId) {
console.log(`向用户发送优惠券过期通知,关联文档ID:${documentId}`);
// 实际场景中可以在这里调用短信接口、推送接口等
}
watchExpiredDocuments().catch(console.error);
五、注意事项
- MongoDB的TTL索引删除文档是后台线程定期执行的,默认每60秒检查一次过期文档,因此文档不会在过期时间点立刻被删除,通知会有一定的延迟,延迟范围在0到60秒之间。
- Change Stream需要MongoDB副本集支持,单机模式默认不支持,本地测试可以执行<mongod --replSet rs0>启动单节点副本集,然后进入Mongo Shell执行<rs.initiate()>初始化副本集。
- 如果业务需要精确的过期时间点通知,不建议使用TTL索引,可以改用定时任务扫描过期文档的方案,但定时任务会增加数据库查询压力。
- Change Stream监听过程中如果连接断开,需要实现重连逻辑,避免遗漏删除事件。
六、方案对比
以下是TTL索引加Change Stream方案和定时任务扫描方案的对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| TTL索引+Change Stream | 无需主动扫描数据库,性能开销小,实现简单 | 通知有60秒内的延迟,依赖副本集 | 对过期时间精度要求不高的场景 |
| 定时任务扫描 | 过期时间精确,不依赖副本集 | 频繁扫描会增加数据库压力,实现复杂度更高 | 对过期时间精度要求高的场景 |
Node.jsMongoDBTTL_indexchange_streamdocument_expiration_notification修改时间:2026-06-25 15:36:39