阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 哈希索引与TTL索引

哈希索引与TTL索引

作者:陈川 阅读数:43894人阅读 分类: MongoDB

哈希索引

哈希索引是MongoDB中一种特殊的单字段索引类型,它使用哈希函数对索引字段的值进行哈希计算,并将哈希结果存储在索引中。与常规的B树索引不同,哈希索引不支持范围查询,但可以提供更快的等值查询性能。

哈希索引的主要特点包括:

  1. 仅支持精确匹配查询($eq),不支持范围查询($gt, $lt等)
  2. 不支持多键索引(数组字段)
  3. 哈希值分布均匀,可以减少热点问题
  4. 索引大小固定,因为所有哈希值的长度相同

创建哈希索引的语法:

db.collection.createIndex({ field: "hashed" })

示例:为用户表的username字段创建哈希索引

db.users.createIndex({ username: "hashed" })

哈希索引特别适合以下场景:

  • 等值查询频繁且数据分布不均匀的字段
  • 需要分散写入负载的情况
  • 不需要范围查询的字段

需要注意的是,哈希索引不支持复合索引,也不能与其他索引类型组合使用。当使用哈希索引进行查询时,查询计划器会自动选择哈希索引进行精确匹配查询。

TTL索引

TTL(Time To Live)索引是MongoDB提供的一种特殊索引,允许自动删除集合中的过期文档。这种索引对于管理临时数据(如会话信息、日志、缓存等)非常有用,可以自动清理过期的数据,减少手动维护的工作量。

TTL索引的主要特点:

  1. 基于日期类型的字段
  2. 自动删除过期文档的后台线程默认每分钟运行一次
  3. 可以指定文档的存活时间(秒数)
  4. 只能针对单个字段创建

创建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索引的工作原理:

  1. MongoDB会定期(默认60秒)检查TTL索引
  2. 对于索引字段的值是日期类型的文档,MongoDB会将其与当前时间比较
  3. 如果当前时间超过字段值加上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 })

性能考虑与最佳实践

哈希索引的性能考虑:

  1. 哈希冲突:虽然概率很低,但理论上可能存在不同值哈希相同的情况
  2. 内存使用:哈希索引通常比B树索引占用更多内存
  3. 写入开销:需要计算哈希值,写入性能略低于普通索引

TTL索引的性能考虑:

  1. 删除操作会增加系统负载,特别是在大量文档同时过期时
  2. 频繁的小批量删除比单次大批量删除对系统影响更小
  3. 过期时间设置应考虑业务高峰期,避免在高峰期大量删除

最佳实践:

  1. 哈希索引适合高基数且查询模式为精确匹配的字段
  2. TTL索引的过期时间应根据业务需求仔细设置
  3. 对于TTL索引,建议使用具体的过期时间字段(expireAt模式)而不是固定存活时间
  4. 监控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索引实际上是一种特殊的单字段索引,其内部实现依赖于以下几个关键组件:

  1. 后台线程:MongoDB有一个专门的TTL线程,默认每分钟唤醒一次
  2. 索引扫描:线程会扫描TTL索引,找出符合删除条件的文档
  3. 批量删除:每次执行会删除一批文档,避免长时间占用资源
  4. 负载控制:如果过期文档太多,会分多次删除,防止系统过载

可以通过以下命令调整TTL线程的运行间隔(需要重启mongod):

// 在配置文件中设置TTL监视器运行间隔(秒)
storage:
  ttlMonitorSleepSecs: 30

对于特别大的集合,可能需要调整TTL删除的批量大小:

// 在配置文件中设置每次TTL删除的文档数量
storage:
  ttlMonitorBatchSize: 500

限制与注意事项

哈希索引的限制:

  1. 不能用于分片键(MongoDB 4.4及更早版本)
  2. 不支持多键索引(数组字段)
  3. 不能用于复合索引
  4. 不支持排序操作

TTL索引的限制:

  1. 不能用于固定集合(capped collection)
  2. 不能用于使用_id字段
  3. 在分片集群上,TTL索引需要片键包含索引字段
  4. 删除操作不是实时的,可能有延迟

特殊场景处理:

  1. 对于分片集合,TTL索引的字段必须包含在片键中或片键是索引的前缀
  2. 在故障转移时,TTL删除可能会延迟
  3. 当系统负载高时,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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌