不推荐手动定时执行 deleteMany() 清理过期数据,因其易漏/重复执行、阻塞写入且无法精确到秒;应使用 TTL 索引,但需确保字段为 Date 类型、单值、非空,并单独建索引,同时监控其实际生效情况。

为什么直接用 deleteMany() 清理过期数据不推荐
手动定时跑 deleteMany({ createdAt: { $lt: new Date(Date.now() - 24 * 60 * 60 * 1000) } }) 看似简单,但实际会带来三个问题:一是容易漏执行或重复执行;二是大集合上删除会阻塞写入、触发大量磁盘 I/O;三是无法保证“精确到秒”的过期行为——比如你希望文档在创建后 1 小时整自动消失,手动脚本做不到这点。
如何正确创建 TTL 索引:字段类型和值必须严格匹配
TTL 索引只对 Date 类型字段生效,且该字段必须是单个日期值(不能是数组、字符串或嵌套对象)。常见错误是存了字符串格式时间(如 "2024-05-20T10:00:00Z"),MongoDB 不会自动转换,索引就完全不触发清理。
- ✅ 正确做法:插入时用
new Date()或 ISODate 字面量:{ expiresAt: new Date(Date.now() + 3600000) } - ❌ 错误写法:
{ expiresAt: "2024-05-20T11:00:00Z" }(字符串)、{ expiresAt: { $date: "2024-05-20T11:00:00Z" } }(BSON 对象) - ⚠️ 注意:TTL 索引必须建在单字段上,不能是复合索引的一部分(
db.collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 3600 }))
TTL 清理不是实时的,延迟通常在 60 秒左右
MongoDB 的后台 TTL 线程默认每 60 秒扫描一次过期文档,所以即使 expiresAt 已过期,文档也可能多存最多 1 分钟。如果你的应用逻辑依赖“过期即不可见”,得在查询时额外加时间判断:
db.logs.find({
expiresAt: { $gt: new Date() }
})
另外,TTL 删除是异步的,不返回删除数量,也不触发 change stream 的 delete 事件——这点常被忽略,导致监听逻辑漏掉清理动作。
生产环境要监控 TTL 是否真正生效
光建了索引不代表它在干活。最容易被忽视的是:如果集合长期没有写入,TTL 线程可能跳过该集合;或者 expiresAt 字段存在大量 null / 缺失值,MongoDB 会跳过这些文档(不报错,也不清理)。
- 检查索引是否创建成功:
db.collection.getIndexes(),确认输出里有"expireAfterSeconds": 3600 - 验证字段值分布:
db.collection.countDocuments({ expiresAt: { $type: "date" } })应等于总文档数 - 观察日志:开启
verbosity: { storage: { ttl: 2 } }可看到 TTL 扫描详情(仅限 WiredTiger 引擎)
真正麻烦的不是建索引,而是字段值不干净、类型不一致、或误以为“建了就万事大吉”。TTL 是个安静的后台工人,不报错、不提醒,只默默跳过所有它看不懂的数据。











