在开发中遇到一个场景:将消费订单校验失败的消息记录下来,因为校验失败的原因除了业务失败还可能是RPC中下游的异常导致,记录这些失败记录便于做流量回放和补偿,并且消费订单的消息是具备时效性的;准备使用MongoDB来存储数据,并且需要一个TTL的功能;

本篇介绍MongoDB的过期删除策略及使用;MongoDB的集合有TTL (time to live,即生存的时间) 特性,可以让MongoDB自动移除过期了的数据;

这种机制便比较适合一些记录【消息数据/事件数据】这种具备时效性数据的业务场景;

原理上,MongoDB通过一个TTL索引来实现这种机制:MongoDB通过一个后台线程去不断的读取集合中某个日期类型的索引,并且移除掉满足过期条件的文档documents;

下面介绍下如何使用;

使用步骤

通过 db.collection.createIndex() 命令创建索引Index,然后配合Index的expireAfterSeconds选项来对某个字段做TTL索引;

注意:这个字段必须是date类型或者是一个包含date类型值的数组字段,一般我们使用date类型;

创建TTL过期规则有2种模式:

(1)一种是固定时间间隔后失效的模式,类似Redis的ttl,即设置一个失效时间,当MongoDB发现当前时间与时间索引字段的时差超过了这个时间间隔,则认为数据过期;

(2)另一种是指定失效的时间点,即设置一个过期时间点,当MongoDB发现当前系统时间已经超过那个时间点,则认为数据过期;但是这种方式下,真正失效数据的时间与我们设定的时间点存在一点误差(0~60s),因为MongoDB后台线程的检测间隔是60秒

下面我们分别看下两种模式的设置方式;

(1)指定过期的时间间隔

假如我们准备存放订单消费消息,其中的记录格式为:

{
    "_id": "5f43d5c00b34962beb026aad",
    "messageBody": "...",
    "reqNo": "1598281152423-6245975993217447",
	"createdAt": Date()
}

接下来,对 createdAt 字段建立一个TTL索引:

# db为数据库名,consume_message为集合名;
db.consume_message.createIndex({ "createdAt": 1 }, {expireAfterSeconds: 3600 })

其含义是,记录在createdAt字段的值的时刻基础上,再加上3600秒之后的那个时间过期;其中 createdAt: 1 表示对 createdAt 字段建立正序的索引;索引的选项 expireAfterSeconds: 3600 表示 记录在3600秒(即1小时) 之后过期;使用时,将createdAt的值设置为当前时间即可;

实际测试时,过期时间符合预期;

(2)指定过期的时间点

有时候,我们不希望在记录创建时刻之后的多少秒再删除(因为这样相当于固定死了失效间隔),而是希望在程序运行时指定某个时刻 (例如避开流量的高峰期) 进行失效;

因此,我们希望给文档(记录)指定一个特定的过期时间点,这种规则,MongoDB也是支持的——在创建TTL索引时,只需把 expireAfterSeconds 配置的值设为0即可;

例如我们把这个TTL索引字段取名叫做expireAt,数据记录的结构如下所示:

{
    "_id": "5f43d5c00b34962beb026aad",
    "messageBody": "...",
    "reqNo": "1598281152423-6245975993217447",
	"expireAt": Date()
}

然后,创建expireAt字段的TTL索引:

# db为数据库名,consume_message为集合名;
db.consume_message.createIndex({ "expiredAt": 1 }, {expireAfterSeconds: 0})

至此,我们就把expireAt字段配置为TTL索引,其中expireAfterSeconds:0 表示MongoDB将用索引字段,也就是expireAt的时间值加0秒后的时间(即expireAt的值本身)作为判断数据失效的依据;

实际测试时,过期时间不符合预期,而是发现到了指定的过期时间,数据还未删除,等了几十秒后才删除;可以发现MongoDB并不是立刻删除该数据的,而是在大概60秒之后才删除,这是因为MongoDB后台线程的检测间隔是60秒

第二种过期模式实际生产中用到的场景较多,如日志数据/订单数据仅保留X天,可以在每条记录入库的时候,就给他添加一个expireAt字段,用来标记该记录的过期时间;

在Springboot下的使用示例

在SpringBoot中继承MongoDB,《SpringBoot整合MongoDB》这篇文章讲的很细,这里仅贴个使用示例的代码;

记录实体(集合)定义:

/**
 * 活动订单消息
 * 集合(表名):activity_consume_message_content
 */
@Data
@Document(collection = "activity_consume_message_content")
@CompoundIndexes({
        @CompoundIndex(name = "activity_user", def = "{'activityId':1,'userId':1}")
})
public class ActivityConsumeMessageContent {
    @Id
    private ObjectId id;

    private Long activityId;

    private String userId;

    private String orderNo;

    private ConsumeOrderMessageDto consumeMessage;

    /**
     * 过期时间 推荐使用expireAt模式 可以运行时配置过期时间 (ttl模式无法修改失效间隔配置)
     */
    @Indexed(name = "expire_time", background = true, expireAfterSeconds = 0)
    private Date expireAt;
}

指定失效时间:

// 订单消息默认保存30天 支持配置
Date expireAt = new Date(System.currentTimeMillis() + ConfigManager.getLong("consumeMessageContent.expireDays", 30) * 24 * 60 * 60 * 1000);
consumeMessageContent.setExpireAt(expireAt);
mongoTemplate.insert(consumeMessageContent);

效果:

注意,测试环境使用Robo 3T作为client链接MongoDB,但是发现到期时间显示的值与预期时间差了8小时,这是因为Robo 3T默认设置的时区是UTC,而我们位于东八区,只需要将Robo 3T设置为Local Timezone即可显示正常的时间;

参考:

SpringBoot整合MongoDB

通过mongodbTTL机制让集合中的数据自动过期删除

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐