如何基于另一集合的TTL配置MongoDB动态自动删除文档
解决方案:NestJS + MongoDB 实现跨集合关联的动态TTL自动删除
一、核心原理说明
MongoDB的TTL索引支持两种模式:固定秒数过期,或基于文档内具体日期字段自动过期。后者是实现单文档不同TTL的关键——只需将expireAfterSeconds设为0,同时给每个文档添加expireAt字段(存储该文档的具体过期时间),MongoDB后台进程会自动删除到达该时间的文档。
结合你的场景,我们需要先从关联集合获取用户的TTL配置,计算出日志的expireAt值,再利用TTL索引实现自动删除;同时还要处理TTL配置变更时的同步逻辑。
二、步骤1:调整API日志集合的Schema
修改api-hit logs的Mongoose Schema,新增expireAt字段并创建对应TTL索引:
import { Schema, Document } from 'mongoose'; export interface ApiHitLog extends Document { username: string; createdOn: Date; expireAt: Date; // 其他日志字段(如接口路径、请求参数等) } export const ApiHitLogSchema = new Schema<ApiHitLog>({ username: { type: String, required: true }, createdOn: { type: Date, required: true, default: Date.now }, expireAt: { type: Date, required: true }, // 其他字段定义 }, { timestamps: true }); // 创建TTL索引:基于expireAt字段,expireAfterSeconds设为0 ApiHitLogSchema.index({ expireAt: 1 }, { expireAfterSeconds: 0 });
三、步骤2:插入日志时计算过期时间
插入日志前,先从关联集合(假设叫UserSettings,含username和ttlSeconds字段)获取用户的TTL配置,计算出expireAt(日志创建时间 + TTL秒数)后存入数据库:
import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { ApiHitLog } from './schemas/api-hit-log.schema'; import { UserSettings } from '../user-settings/schemas/user-settings.schema'; @Injectable() export class ApiHitLogService { constructor( @InjectModel(ApiHitLog.name) private apiHitLogModel: Model<ApiHitLog>, @InjectModel(UserSettings.name) private userSettingsModel: Model<UserSettings>, ) {} async createLog(username: string, logData: Partial<ApiHitLog>) { // 获取用户TTL配置,无配置则用默认值(比如3600秒) const userSettings = await this.userSettingsModel.findOne({ username }); const ttlSeconds = userSettings?.ttlSeconds ?? 3600; const createdOn = new Date(); // 计算过期时间:创建时间转毫秒 + TTL秒数转毫秒 const expireAt = new Date(createdOn.getTime() + ttlSeconds * 1000); const log = new this.apiHitLogModel({ username, createdOn, expireAt, ...logData, }); return log.save(); } }
四、步骤3:同步关联集合的TTL变更
如果用户的TTL配置在UserSettings中被修改,需要同步更新该用户所有未过期的API日志expireAt字段,否则旧日志仍会按原时间过期。
方式1:手动触发更新(修改TTL时调用)
在用户TTL修改的服务中,添加同步日志的逻辑:
async updateUserTTL(username: string, newTTLSeconds: number) { // 更新用户TTL配置 await this.userSettingsModel.updateOne({ username }, { ttlSeconds: newTTLSeconds }); // 批量更新未过期日志的expireAt await this.apiHitLogModel.updateMany( { username, expireAt: { $gt: new Date() } }, // 仅更新未过期的日志 [ { $set: { expireAt: { $add: ['$createdOn', newTTLSeconds * 1000] } // MongoDB日期运算,单位毫秒 } } ] ); }
方式2:MongoDB变更流自动监听
若需完全自动化,可利用MongoDB的Change Streams监听UserSettings集合的更新事件,自动同步日志:
import { Injectable, OnModuleInit } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { UserSettings } from '../user-settings/schemas/user-settings.schema'; import { ApiHitLog } from './schemas/api-hit-log.schema'; @Injectable() export class UserSettingsTTLListener implements OnModuleInit { constructor( @InjectModel(UserSettings.name) private userSettingsModel: Model<UserSettings>, @InjectModel(ApiHitLog.name) private apiHitLogModel: Model<ApiHitLog>, ) {} async onModuleInit() { const changeStream = this.userSettingsModel.watch(); changeStream.on('change', async (change) => { // 仅处理TTL字段更新的事件 if (change.operationType === 'update' && change.updateDescription?.updatedFields?.ttlSeconds) { const username = change.documentKey.username; const newTTLSeconds = change.updateDescription.updatedFields.ttlSeconds; // 同步更新未过期日志 await this.apiHitLogModel.updateMany( { username, expireAt: { $gt: new Date() } }, [ { $set: { expireAt: { $add: ['$createdOn', newTTLSeconds * 1000] } } } ] ); } }); } }
五、注意事项
- MongoDB的TTL后台进程每60秒运行一次,文档可能会在过期后最多延迟60秒被删除,属于正常现象。
- 确保
expireAt字段为有效的Date类型,否则TTL索引不会生效。 - 务必处理关联集合无TTL配置的场景,设置合理默认值避免日志永不过期。
内容的提问来源于stack exchange,提问作者Udayaprakash
相关产品推荐
相关产品推荐

