在数据库运维中,我们经常会遇到这样的场景:用户会话信息只需要保留7天、日志数据保存30天后即可删除、临时缓存数据有效期为2小时...如果全靠人工手动清理,不仅效率低下,还容易遗漏。MongoDB的TTL(Time To Live)索引就是为解决这类问题而生的智能机制。
我管理过一个电商平台的用户行为日志系统,每天新增2000万条日志,最初采用定时任务每天凌晨删除过期数据,结果经常出现任务执行失败导致磁盘爆满的情况。后来改用TTL索引后,系统自动维护数据生命周期,再也不用担心这类问题了。
TTL索引本质上是一种特殊的单字段索引,MongoDB后台会运行一个专门的线程(TTL Monitor),默认每60秒扫描一次集合中设置了TTL索引的字段。当发现文档的时间戳早于当前时间减去设定的过期时间时,就会自动删除该文档。
这个过程的伪代码逻辑大致是这样的:
javascript复制while(running) {
sleep(60);
for(collection in collectionsWithTTLIndex) {
expiredTime = new Date() - index.expireAfterSeconds;
deleteMany({indexField: {$lt: expiredTime}});
}
}
TTL索引支持两种类型的时间字段:
重要提示:如果字段存储的是时间戳(如Unix timestamp),需要先转换为Date对象才能生效。我曾经踩过这个坑,字段用了Number类型存储时间戳,TTL索引完全不起作用。
创建一个7天后自动删除的索引示例:
javascript复制// 对createTime字段创建TTL索引,604800秒=7天
db.collection.createIndex(
{ "createTime": 1 },
{ expireAfterSeconds: 604800 }
)
TTL索引创建后可以随时修改过期时间:
javascript复制// 修改为24小时过期
db.runCommand({
"collMod": "collectionName",
"index": {
"keyPattern": { "createTime": 1 },
"expireAfterSeconds": 86400
}
})
如果是对数组中的日期元素创建索引:
javascript复制db.logs.createIndex(
{ "timestamps": 1 },
{ expireAfterSeconds: 3600 }
)
这时会以数组中最小的日期元素作为判断依据。
删除负载均衡:大量文档同时过期会导致删除操作集中爆发。可以通过在初始插入时给时间字段加上随机偏移量:
javascript复制// 使过期时间分散在前后6小时内
const expireAt = new Date(Date.now() + 86400000 + Math.random()*21600000 - 10800000);
db.collection.insert({data: "...", expireAt: expireAt});
索引策略:TTL索引应该建在查询也常用的字段上,实现索引复用。我曾优化过一个系统,把单独的TTL索引和查询索引合并,性能提升了40%。
通过以下命令查看TTL索引状态:
javascript复制db.collection.getIndexes()
输出示例:
json复制{
"v": 2,
"key": { "createTime": 1 },
"name": "createTime_1",
"expireAfterSeconds": 3600,
"background": true
}
主从延迟:在副本集中,如果从节点延迟较大,可能导致主节点已删除的文档在从节点仍然可见。可以通过以下命令检查复制状态:
javascript复制rs.printSecondaryReplicationInfo()
时钟不同步:集群节点间时间不同步会导致意外行为。确保所有节点都使用NTP时间同步服务。
TTL删除操作会:
如果发现删除性能下降,可以检查当前操作:
javascript复制db.currentOp({ "op": "remove" })
通过在文档中存储不同的过期时间,可以实现文档级别的动态TTL:
javascript复制// 文档A设置1小时过期
db.collection.insert({
data: "A",
expireAt: new Date(Date.now() + 3600000)
});
// 文档B设置1天过期
db.collection.insert({
data: "B",
expireAt: new Date(Date.now() + 86400000)
});
// 创建索引时expireAfterSeconds设为0
db.collection.createIndex(
{ "expireAt": 1 },
{ expireAfterSeconds: 0 }
);
这样每个文档会根据自己的expireAt时间独立过期。
在分片集群中使用TTL索引时需要注意:
我曾经处理过一个案例,某个分片上的TTL删除操作特别慢,最后发现是该分片的chunk分布不均匀导致的,通过手动拆分chunk解决了问题。
当TTL索引不适用时,可以考虑:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| TTL索引 | 自动维护,开销小 | 精度较低(60秒) | 常规过期需求 |
| 定时任务 | 精确控制执行时间 | 需要额外维护 | 需要精确时间控制 |
| 固定集合 | 自动淘汰旧数据 | 不能按条件删除 | 日志类只保留最新数据 |
| 客户端逻辑 | 完全可控 | 实现复杂 | 需要复杂过期逻辑 |
在实际项目中,我通常会根据数据规模选择方案:
最后分享一个真实案例:在某物联网平台中,我们使用TTL索引处理设备状态心跳数据(保留7天),同时用定时任务按月归档重要指标。这种组合方案稳定运行了3年,日均处理20亿条数据。关键是要充分理解业务的数据访问模式,才能设计出最合适的过期策略。