快盘下载:好资源、好软件、快快下载吧!

快盘排行|快盘最新

当前位置:首页软件教程电脑软件教程 → Pymongo的一个小坑

Pymongo的一个小坑

时间:2022-09-20 09:46:04人气:作者:快盘下载我要评论

//

Pymongo的一个小坑

//

这两天遇到一个线上的问题,是关于Pymongo处理时间方面的,这里记录一下,希望对大家有帮助。

故事的场景是这样的:线上业务反馈MongoDB的集合中总是存在过期的数据,他已经在某个时间字段上设置了TTL索引,但是TTL的时间过期之后,文档没有删除,依旧包含旧文档。

看到这个问题,解决的思路肯定是沿着TTL索引这个方向去查找的,我们先来看看TTL索引是个什么东西?

TTL索引简介

Pymongo的一个小坑

TTL索引是与时间生命周期相关的索引,该索引指定文档中时间字段,并为它设计一个时间值,如果文档的时间超过了这个值,则文档会被自动删除。它通常用来设置哪些不重要的文档,例如,一个文件系统的log文档、巡检信息文档等。

它包含4个主要的特点:

1、TTL索引不可以是符合索引,否则不知道该以哪个时间为准。

2、如果TTL创建的字段不是时间字段,那么该文档不会被删除

3、TTL索引每60s运行一次,移除过期的文档

4、如果时间类型的字段已经被设置为其他索引,则无法通过设置TTL索引来删除此文档。

为了加深理解,我们来看下面的测试:

首先我们使用下面的语句来创建一个集合timetest,这个集合包含2个字段,一个是text字段,另外一个是createtime时间字段:

插入时间字段:
db.timetest.insert({"text":"timetest","createtime":new Fate()})

我们给集合中插入数据,插入数据的过程中,使用new Date函数插入默认当前时间的数据。插入完成之后的结果如下:

> 
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.find()
{ "_id" : ObjectId("5fcb2a6cc08d6a7b799f457a"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:28.894Z") }
{ "_id" : ObjectId("5fcb2a81c08d6a7b799f457b"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:49.845Z") }
{ "_id" : ObjectId("5fcb2a83c08d6a7b799f457c"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:51.460Z") }
{ "_id" : ObjectId("5fcb2a85c08d6a7b799f457d"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:53.165Z") }
{ "_id" : ObjectId("5fcb2a86c08d6a7b799f457e"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:54.647Z") }
{ "_id" : ObjectId("5fcb2a88c08d6a7b799f457f"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:56.117Z") }
{ "_id" : ObjectId("5fcb2a89c08d6a7b799f4580"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:57.605Z") }
{ "_id" : ObjectId("5fcb2a8bc08d6a7b799f4581"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:59.095Z") }
{ "_id" : ObjectId("5fcb2a8cc08d6a7b799f4582"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:00.615Z") }
{ "_id" : ObjectId("5fcb2a8ec08d6a7b799f4583"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:02.038Z") }
{ "_id" : ObjectId("5fcb2a8fc08d6a7b799f4584"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:03.471Z") }
{ "_id" : ObjectId("5fcb2a90c08d6a7b799f4585"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:04.909Z") }
{ "_id" : ObjectId("5fcb2a92c08d6a7b799f4586"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:06.383Z") }
{ "_id" : ObjectId("5fcb2a93c08d6a7b799f4587"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:07.817Z") }
> 

可以看到,数据已经插入到集合当中了。

紧接着,我们使用createIndex来设置createtime这个时间字段为TTL索引,然后过期时间是60s,如下:

> 
> db.timetest.createIndex({"createtime":-1},{expireAfterSeconds:60})
{
        "createdCollectionAutomatically" : false,
        "numIndexesBefore" : 1,
        "numIndexesAfter" : 2,
        "ok" : 1
}
> 
> 
> db.timetest.getIndexes()
[
        {
                "v" : 2,
                "key" : {
                        "_id" : 1
                },
                "name" : "_id_",
                "ns" : "yeyz.timetest"
        },
        {
                "v" : 2,
                "key" : {
                        "createtime" : -1
                },
                "name" : "createtime_-1",
                "ns" : "yeyz.timetest",
                "expireAfterSeconds" : 60
        }
]
> 

可以看到,插入索引成功后,timetest这个集合中有2个索引了。

此时我们进行find操作,看看这些记录会不会自动删除:

> db.timetest.find()
{ "_id" : ObjectId("5fcb2a6cc08d6a7b799f457a"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:28.894Z") }
{ "_id" : ObjectId("5fcb2a81c08d6a7b799f457b"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:49.845Z") }
{ "_id" : ObjectId("5fcb2a83c08d6a7b799f457c"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:51.460Z") }
{ "_id" : ObjectId("5fcb2a85c08d6a7b799f457d"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:53.165Z") }
{ "_id" : ObjectId("5fcb2a86c08d6a7b799f457e"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:54.647Z") }
{ "_id" : ObjectId("5fcb2a88c08d6a7b799f457f"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:56.117Z") }
{ "_id" : ObjectId("5fcb2a89c08d6a7b799f4580"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:57.605Z") }
{ "_id" : ObjectId("5fcb2a8bc08d6a7b799f4581"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:59.095Z") }
{ "_id" : ObjectId("5fcb2a8cc08d6a7b799f4582"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:00.615Z") }
{ "_id" : ObjectId("5fcb2a8ec08d6a7b799f4583"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:02.038Z") }
{ "_id" : ObjectId("5fcb2a8fc08d6a7b799f4584"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:03.471Z") }
{ "_id" : ObjectId("5fcb2a90c08d6a7b799f4585"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:04.909Z") }
{ "_id" : ObjectId("5fcb2a92c08d6a7b799f4586"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:06.383Z") }
{ "_id" : ObjectId("5fcb2a93c08d6a7b799f4587"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:07.817Z") }
> db.timetest.find()

我们发现,第一次查看的时候,所有的记录还是存在于集合timetest当中的,但是第二次查询的时候,集合中的记录就不复存在了。

上面的实验过程,说明了2个问题:

1、说明TTL索引生效了,已经自动帮助我们删除了过期的文档,

2、TTL索引创建完成之后,不是立即生效的,需要有一定的时间间隔,然后才会按照索引的这个时间字段进行删除操作。

有了这个实验,想必对于TTL索引,大家已经有一个直观的认知了。

案例分析

Pymongo的一个小坑

为了方便阅读,我把之前案例场景搬迁过来:

这个问题看起来似乎变的很玄乎,设置了生效时间,结果没有删除掉,根据刚才我们了解的TTL特点,它可能是以下几个问题:

1、这个字段上还有其他的索引

2、索引刚刚创建好,需要等待一段时间才可以看到被删除后的结果

3、数据记录的时间错误了。

我们一一来看,经过查看业务表当前的时间字段没有其他的索引,因此排除场景1;经过了解,这个索引已经创建了好几天,时间肯定大于60s了,一定是触发了删除操作的,排除场景2;所以只可能是场景3了。我们的重点问题是查看场景3到底是哪儿的时间出现问题了。

验证结果

Pymongo的一个小坑

首先我们看MongoDB里面的时间:

> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.find()
{ "_id" : ObjectId("5fcb31b9c08d6a7b799f4588"), "text" : "timetest", "createtime" : ISODate("2020-12-05T07:07:37.749Z") }

正常插入当前时间new Date,发现时间变成了:

2020-12-05T07:07:37.749Z

但是当前写公众号文章的真正时间是:

2020-12-05 15:07:37

关于这个时间转换,做过mysql运维的人肯定比较熟悉,看ERROR log的时候,经常会遇到"T"的时间。

其实带"T"的时间是格林尼治时间,它在数值上等于UTC时间,北京时间和UTC时间之间相差8小时,准确的来讲:

北京时间=UTC时间+8小时=格林尼治时间+8小时

所以,根据上面createtime的结果,不难看出来,MongoDB的默认时间是格林尼治时间。

到这里,可以判定可能是MongoDB的默认时间和我们普遍认知的时间有偏差导致的问题,于是再次追问业务看到的时间点是多少,结果得到了以下4个时间信息。

1、发现错误的真正时间:2020-12-04 11:30:00 (大概)

2、MongoDB数据库中存储的时间:

2020-12-04T11:30:00.000Z

3、TTL索引设置的过期时间:3600s

4、业务方认为没删除的文档在数据库中存储的时间:

2020-12-04T02:29:00.00Z

到这里,我明白了时间误差所在的地方:

1、业务方误认为上面第4个时间2020-12-04T02:29:00.00Z代表今天的凌晨两点,其实不然,它是格林尼治表示法,代表的是早晨10:29:00这个时间。

2、MongoDB数据库中存储的时间是错误的,因为2020-12-04T11:30:00.000Z代表的是晚上19:30:00,而不是发现错误的时间11:30,也就是说,业务方在中午11:30这个时间,写入到数据库中的时间是:2020-12-04T11:30:00.000Z,这明显是写入错误了。

看到这里,继续追问业务方写入MongoDB的方法是什么,得知是pymongo之后,我查询了pymongo的官方文档,看到了下面的内容:

官方文档

简单说,就是从pymongo驱动将当前时间写入到mongodb数据库的时候,时间字段需要使用python的utcnow()函数来写入,而不能使用now()函数。具体细节大家可以进入官方文档进行查看:

https://pymongo.readthedocs.io/en/stable/examples/datetimes.html?Highlight=date

到这里,我们需要进一步测试验证自己的猜想,于是我自己用pymongo写了一个小脚本来写入数据,给yeyz的timetest表里面插入一条数据,看看数据的结果是多少:

# encoding=utf-8
import pymongo
import time
from datetime import datetime, timedelta

conn = pymongo.MongoClient("mongodb://127.0.0.1:27017/")


# 指定已经授权的DB
db = conn['admin']

db.authenticate("root", "123456")
#
# # 获取数据库下的表
db1 = conn['yeyz']

my_table = db1["timetest"]
curtime =  datetime.now()
print curtime

mydict = {"name": "timetest", "createtime": curtime}
my_table.insert_one(mydict)

当前时间是15:39,来看写入的时间:

> db.timetest.find()
{ "_id" : ObjectId("5fcb3a244049990699a0974f"), "name" : "now", "createtime" : ISODate("2020-12-05T15:43:32.966Z") }
{ "_id" : ObjectId("5fcb3a244049990699a09750"), "name" : "utcnow", "createtime" : ISODate("2020-12-05T07:43:32.966Z") }

可以看到,如果我们使用now()写入的时间变成了:

ISODate("2020-12-05T15:43:32.966Z")

它代表今天晚上的23:43分,明显与实际不符。

而使用utcnow()写入的时间变成了:

ISODate("2020-12-05T07:43:32.966Z")

这个时间是正确的,跟我们实际期望相符。

此时如果createtime字段上设置了3600s的TTL索引,那么文档的时间最多可以保存到2020-12-05T06:43:32.966Z左右,但是不一定刚好1个小时。

说明只有使用utcnow()的时候,pymongo才能写入正确的时间字段,否则,时间字段的转换就会出错,写入时间将是单签时间未来8小时的时间。

到这里,这个问题就告一段落了,虽然只是一个很小的时间问题,但是稍有不慎,可能就会踩到坑,从而造成不可知的影响。

有帮助的话还希望点下再看哈

相关文章

  • 一步步带你设计MySQL索引数据结构

    一步步带你设计MySQL索引数据结构,想想我们生活中的例子,比如新华字典,我们有一个目录,目录根据拼音排序,内容包含了汉字位于字典中具体的的页码。聪明的你肯定也想到了,我们也可以借鉴这种思想,建立一个MySQL的目录,叫做“索引”。...
  • 影刀连接Mysql数据库

    影刀连接Mysql数据库,影刀配置连接mysql数据库基础版...

网友评论

快盘下载暂未开通留言功能。

关于我们| 广告联络| 联系我们| 网站帮助| 免责声明| 软件发布

Copyright 2019-2029 【快快下载吧】 版权所有 快快下载吧 | 豫ICP备10006759号公安备案:41010502004165

声明: 快快下载吧上的所有软件和资料来源于互联网,仅供学习和研究使用,请测试后自行销毁,如有侵犯你版权的,请来信指出,本站将立即改正。