最近使用Python写一段缓存用户登陆信息的一段代码,使用MongoDB保存token及用户信息,同时设置过期时间为当前时间1小时后,代码如下:
db['user'].insert_one(
{'token': token, 'info': user, 'expire_at': datetime.fromtimestamp(time.time() + 60 * 60 * 1)}
)
结果发现过了预期的时间很久后,数据依然存在。
rs0:PRIMARY> db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "sign.user"
},
{
"v" : 2,
"key" : {
"expire_at" : 1
},
"name" : "expire_at_1",
"ns" : "sign.user",
"expireAfterSeconds" : 0
}
]
rs0:PRIMARY> db.user.find({"_id" : ObjectId("5e0cfc630f37131c94412622")}, {expire_at: 1}).pretty()
{
"_id" : ObjectId("5e0cfc630f37131c94412622"),
"expire_at" : ISODate("2020-01-02T10:10:22.160Z")
}
一开始有点纳闷,仔细一看ISODate中有个"Z",大概知道怎么回事了:时区在作怪!
咱们是东8时,比标准时间快了8小时,也就是说我这里设置的是1小时后过期,实际上存储的时间是标准时间的9小时后...
于是想到下面两种方案来处理这个问题。
方案1:使用pymongo保存时间时,指定时区
首先尝试用几种不同的方法插入几条测试数据
db['user'].insert_one({'test': datetime.now()})
db['user'].insert_one({'test': datetime.utcnow()})
db['user'].insert_one({'test': datetime.fromtimestamp(time.time())})
db['user'].insert_one({'test': datetime.utcfromtimestamp(time.time())})
python环境下查询结果如下
{'_id': ObjectId('5e0d7369db0af443d23152e5'), 'test': datetime.datetime(2020, 1, 2, 12, 36, 57, 550000)}
{'_id': ObjectId('5e0d737bdb0af443d23152e6'), 'test': datetime.datetime(2020, 1, 2, 4, 37, 15, 745000)}
{'_id': ObjectId('5e0d73e1db0af443d23152e7'), 'test': datetime.datetime(2020, 1, 2, 12, 38, 57, 925000)}
{'_id': ObjectId('5e0d73eadb0af443d23152e8'), 'test': datetime.datetime(2020, 1, 2, 4, 39, 6, 430000)}
mongo客户端环境下查询结果如下
{ "_id" : ObjectId("5e0d7369db0af443d23152e5"), "test" : ISODate("2020-01-02T12:36:57.550Z") }
{ "_id" : ObjectId("5e0d737bdb0af443d23152e6"), "test" : ISODate("2020-01-02T04:37:15.745Z") }
{ "_id" : ObjectId("5e0d73e1db0af443d23152e7"), "test" : ISODate("2020-01-02T12:38:57.925Z") }
{ "_id" : ObjectId("5e0d73eadb0af443d23152e8"), "test" : ISODate("2020-01-02T04:39:06.430Z") }
由此可见,如果直接使用pymongo及datetime保存时间字段是,如果不设置时区,就会与标准时间产生偏差。
通过查阅pymongo的帮助文档发现,MongoClient这个类的构造函数中有一个参数tz_aware
,也正如其字面含义(知道、察觉时区)。
使用MongoClient连接时将这个参数设为True,查询结果如下
{'_id': ObjectId('5e0d7369db0af443d23152e5'), 'test': datetime.datetime(2020, 1, 2, 12, 36, 57, 550000, tzinfo=<bson.tz_util.FixedOffset object at 0x7fa4b0a8fac0>)}
{'_id': ObjectId('5e0d737bdb0af443d23152e6'), 'test': datetime.datetime(2020, 1, 2, 4, 37, 15, 745000, tzinfo=<bson.tz_util.FixedOffset object at 0x7fa4b0a8fac0>)}
{'_id': ObjectId('5e0d73e1db0af443d23152e7'), 'test': datetime.datetime(2020, 1, 2, 12, 38, 57, 925000, tzinfo=<bson.tz_util.FixedOffset object at 0x7fa4b0a8fac0>)}
{'_id': ObjectId('5e0d73eadb0af443d23152e8'), 'test': datetime.datetime(2020, 1, 2, 4, 39, 6, 430000, tzinfo=<bson.tz_util.FixedOffset object at 0x7fa4b0a8fac0>)}
这样能够知道查询到的时间使用的是什么时区,使用utcnow
、utcfromtimestamp
这两个函数保存的时间查询后可以根据时区得到真实的时间,不过如果保存时就已经发生了错误,就没有办法了。
除此以外,有没有方法来控制保存时间时采用的时区呢?除了上文提到的utcnow
、utcfromtimestamp
还有其它办法吗?继续查阅pymongo文档,其中提到了pytz
这个库。
from datetime import datetime
import pytz
db['user'].insert_one(
{'test': pytz.timezone('Asia/Shanghai').localize(datatime.now())}
)
实际上,datetime也支持一个默认参数tz,可以传入tzinfo
类型的值来指定时区。
db['user'].insert_one(
{
'token': ticket,
'info': user,
'expire_at': datetime.fromtimestamp(time.time() + 60 * 60 * 1, tz=pytz.timezone('Asia/Shanghai'))
}
)
这几种办法保存时间,采用的都是将时间先转化成标准时间,再进行保存。所以读取时也需要将标准时间转换成目标时区的时间。
from bson.codec_options import CodecOptions
collection = db.user.with_options(
codec_options=CodecOptions(tz_aware=True, tzinfo=pytz.timezone('Asia/Shanghai'))
)
result = collection.find()
获得带时区的datetime后,astimezone
函数可以进行时区转换
import datetime
import pytz
if __name__ == '__main__':
y = datetime.datetime(2020, 1, 2, 4, 37, 15, 745000, tzinfo=pytz.timezone('UTC'))
# print(y.astimezone(pytz.timezone('Asia/Shanghai')))
print(y.astimezone(datetime.timezone(datetime.timedelta(hours=8))))
print(y.astimezone(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S'))
结果如下
2020-01-02 12:37:15.745000+08:00
2020-01-02 12:37:15
方案2:设置MongoDB的时区
本想应该可以设置时间保存的时区,结果却没有找到相应的方法设置MongoDB中ISODate的默认时区,暂且搁置,如果有新的发现再来补充。
总结
最后思考了下,出现这个问题的原因,一方面是自己思考不足,另一方面跟MongoDB的设计思路有关,它的TTL索引字段只支持ISODate类型,如果没有这个限制,在所有时间字段一律使用时间戳,就避免了这个问题。
网友评论