哈希索引与TTL索引
哈希索引
哈希索引是MongoDB中一种特殊的单字段索引类型,它使用哈希函数对索引字段的值进行哈希计算,并将哈希结果存储在索引中。与常规的B树索引不同,哈希索引不支持范围查询,但可以提供更快的等值查询性能。
哈希索引的主要特点包括:
- 仅支持精确匹配查询(
$eq
),不支持范围查询($gt
,$lt
等) - 不支持多键索引(数组字段)
- 哈希值分布均匀,可以减少热点问题
- 索引大小固定,因为所有哈希值的长度相同
创建哈希索引的语法:
db.collection.createIndex({ field: "hashed" })
示例:为用户表的username
字段创建哈希索引
db.users.createIndex({ username: "hashed" })
哈希索引特别适合以下场景:
- 等值查询频繁且数据分布不均匀的字段
- 需要分散写入负载的情况
- 不需要范围查询的字段
需要注意的是,哈希索引不支持复合索引,也不能与其他索引类型组合使用。当使用哈希索引进行查询时,查询计划器会自动选择哈希索引进行精确匹配查询。
TTL索引
TTL(Time To Live)索引是MongoDB提供的一种特殊索引,允许自动删除集合中的过期文档。这种索引对于管理临时数据(如会话信息、日志、缓存等)非常有用,可以自动清理过期的数据,减少手动维护的工作量。
TTL索引的主要特点:
- 基于日期类型的字段
- 自动删除过期文档的后台线程默认每分钟运行一次
- 可以指定文档的存活时间(秒数)
- 只能针对单个字段创建
创建TTL索引的基本语法:
db.collection.createIndex({ field: 1 }, { expireAfterSeconds: 3600 })
示例1:创建24小时后过期的日志索引
db.logs.createIndex({ createdAt: 1 }, { expireAfterSeconds: 86400 })
示例2:创建1小时后过期的会话索引
db.sessions.createIndex({ lastAccessed: 1 }, { expireAfterSeconds: 3600 })
TTL索引的工作原理:
- MongoDB会定期(默认60秒)检查TTL索引
- 对于索引字段的值是日期类型的文档,MongoDB会将其与当前时间比较
- 如果当前时间超过字段值加上
expireAfterSeconds
,则删除该文档
重要注意事项:
- 如果字段不是日期类型或包含非日期值,文档不会被自动删除
- 如果文档的索引字段不存在,文档不会被自动删除
- 删除操作是异步的,可能会有延迟
- 副本集中的删除操作只会在主节点执行,然后通过oplog同步到从节点
高级用法:动态TTL 可以通过在文档中存储不同的过期时间来实现动态TTL控制:
// 文档中包含expireAt字段,指定具体的过期时间
db.events.insertOne({
message: "System maintenance",
expireAt: new Date("2023-12-31T23:59:59Z")
})
// 创建TTL索引,expireAfterSeconds设为0表示使用字段中的具体日期
db.events.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 })
哈希索引与TTL索引的比较
虽然哈希索引和TTL索引都是MongoDB的特殊索引类型,但它们解决的问题完全不同:
特性 | 哈希索引 | TTL索引 |
---|---|---|
主要用途 | 优化等值查询性能 | 自动删除过期文档 |
索引字段类型 | 任意类型 | 必须是日期类型 |
查询支持 | 仅支持精确匹配 | 不影响查询能力 |
自动维护 | 无 | 自动删除过期文档 |
复合索引 | 不支持 | 不支持 |
性能影响 | 写入时计算哈希 | 后台定期扫描 |
实际应用中可以结合使用这两种索引。例如,在一个消息队列系统中:
// 对消息ID创建哈希索引加速查找
db.messages.createIndex({ messageId: "hashed" })
// 对过期时间创建TTL索引自动清理
db.messages.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 })
性能考虑与最佳实践
哈希索引的性能考虑:
- 哈希冲突:虽然概率很低,但理论上可能存在不同值哈希相同的情况
- 内存使用:哈希索引通常比B树索引占用更多内存
- 写入开销:需要计算哈希值,写入性能略低于普通索引
TTL索引的性能考虑:
- 删除操作会增加系统负载,特别是在大量文档同时过期时
- 频繁的小批量删除比单次大批量删除对系统影响更小
- 过期时间设置应考虑业务高峰期,避免在高峰期大量删除
最佳实践:
- 哈希索引适合高基数且查询模式为精确匹配的字段
- TTL索引的过期时间应根据业务需求仔细设置
- 对于TTL索引,建议使用具体的过期时间字段(expireAt模式)而不是固定存活时间
- 监控TTL索引的删除性能,特别是在数据量大的情况下
示例:监控TTL索引性能
// 查看TTL索引的删除统计
db.serverStatus().metrics.ttl
// 示例输出可能包含:
{
"deletedDocuments" : NumberLong(1250),
"passes" : NumberLong(42)
}
实际应用案例
案例1:用户会话管理系统
// 创建会话集合
db.sessions.createIndex({ sessionToken: "hashed" }) // 快速查找会话
db.sessions.createIndex({ lastActivity: 1 }, { expireAfterSeconds: 1800 }) // 30分钟不活动则过期
// 更新会话活动时间
db.sessions.updateOne(
{ sessionToken: "abc123" },
{ $set: { lastActivity: new Date() } }
)
案例2:临时验证码存储
// 创建验证码集合
db.verificationCodes.createIndex({ phone: "hashed" }) // 快速查找手机号验证码
db.verificationCodes.createIndex({ createdAt: 1 }, { expireAfterSeconds: 300 }) // 5分钟后过期
// 插入验证码
db.verificationCodes.insertOne({
phone: "+1234567890",
code: "123456",
createdAt: new Date()
})
案例3:缓存系统实现
// 创建缓存集合
db.cache.createIndex({ key: "hashed" }) // 快速缓存查找
db.cache.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }) // 动态过期时间
// 设置缓存值
function setCache(key, value, ttlSeconds) {
db.cache.updateOne(
{ key: key },
{
$set: {
value: value,
expiresAt: new Date(Date.now() + ttlSeconds * 1000)
}
},
{ upsert: true }
)
}
// 获取缓存值
function getCache(key) {
const doc = db.cache.findOne({ key: key })
return doc ? doc.value : null
}
高级主题:TTL索引的内部机制
MongoDB的TTL索引实际上是一种特殊的单字段索引,其内部实现依赖于以下几个关键组件:
- 后台线程:MongoDB有一个专门的TTL线程,默认每分钟唤醒一次
- 索引扫描:线程会扫描TTL索引,找出符合删除条件的文档
- 批量删除:每次执行会删除一批文档,避免长时间占用资源
- 负载控制:如果过期文档太多,会分多次删除,防止系统过载
可以通过以下命令调整TTL线程的运行间隔(需要重启mongod):
// 在配置文件中设置TTL监视器运行间隔(秒)
storage:
ttlMonitorSleepSecs: 30
对于特别大的集合,可能需要调整TTL删除的批量大小:
// 在配置文件中设置每次TTL删除的文档数量
storage:
ttlMonitorBatchSize: 500
限制与注意事项
哈希索引的限制:
- 不能用于分片键(MongoDB 4.4及更早版本)
- 不支持多键索引(数组字段)
- 不能用于复合索引
- 不支持排序操作
TTL索引的限制:
- 不能用于固定集合(capped collection)
- 不能用于使用
_id
字段 - 在分片集群上,TTL索引需要片键包含索引字段
- 删除操作不是实时的,可能有延迟
特殊场景处理:
- 对于分片集合,TTL索引的字段必须包含在片键中或片键是索引的前缀
- 在故障转移时,TTL删除可能会延迟
- 当系统负载高时,TTL删除可能会被推迟
示例:分片集合上的TTL索引
// 先启用分片
sh.enableSharding("test")
// 创建分片键(必须包含TTL字段或作为前缀)
sh.shardCollection("test.events", { timestamp: 1, _id: 1 })
// 然后才能创建TTL索引
db.events.createIndex({ timestamp: 1 }, { expireAfterSeconds: 3600 })
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn