MongoDB游標(biāo)超時(shí)問(wèn)題的4種解決方法
當(dāng)我們使用Python從MongoDB里面讀取數(shù)據(jù)時(shí),可能會(huì)這樣寫代碼:
import pymongo handler = pymongo.MongoClient().db.col for row in handler.find(): parse_data(row)
短短4行代碼,讀取MongoDB里面的每一行數(shù)據(jù),然后傳入parse_data做處理。處理完成以后再讀取下一行。邏輯清晰而簡(jiǎn)單,能有什么問(wèn)題?只要parse_data(row)
不報(bào)錯(cuò),這一段代碼就完美無(wú)缺。
但事實(shí)并非這樣。
你的代碼可能會(huì)在for row in handler.find()
這一行報(bào)錯(cuò)。它的原因,說(shuō)來(lái)話長(zhǎng)。
要解釋這個(gè)問(wèn)題,我們首先就需要知道,handler.find()返回的并不是數(shù)據(jù)庫(kù)里面的數(shù)據(jù),而是一個(gè)游標(biāo)(cursor)對(duì)象。如下圖所示:
只有當(dāng)你使用for循環(huán)開(kāi)始迭代它的時(shí)候,游標(biāo)才會(huì)真正去數(shù)據(jù)庫(kù)里面讀取數(shù)據(jù)。
但是,如果每一次循環(huán)都連接數(shù)據(jù)庫(kù),那么網(wǎng)絡(luò)連接會(huì)浪費(fèi)大量時(shí)間。
所以pymongo會(huì)一次性獲取100行,for row in handler.find()循環(huán)第一次的時(shí)候,它會(huì)連上MongoDB,讀取一百條數(shù)據(jù),緩存到內(nèi)存中。于是第2-100次循環(huán),數(shù)據(jù)都是直接從內(nèi)存里面獲取,不會(huì)再連接數(shù)據(jù)庫(kù)。
當(dāng)循環(huán)進(jìn)行到底101次的時(shí)候,再一次連接數(shù)據(jù)庫(kù),再讀取第101-200行內(nèi)容……
這個(gè)邏輯非常有效地降低了網(wǎng)絡(luò)I/O耗時(shí)。
但是,MongoDB默認(rèn)游標(biāo)的超時(shí)時(shí)間是10分鐘。10分鐘之內(nèi),必需再次連接MongoDB讀取內(nèi)容刷新游標(biāo)時(shí)間,否則,就會(huì)導(dǎo)
致游標(biāo)超時(shí)報(bào)錯(cuò):
pymongo.errors.CursorNotFound: cursor id 211526444773 not found
如下圖所示:
所以,回到最開(kāi)始的代碼中來(lái),如果parse_data每次執(zhí)行的時(shí)間超過(guò)6秒鐘,那么它執(zhí)行100次的時(shí)間就會(huì)超過(guò)10分鐘。此時(shí),當(dāng)程序想讀取第101行數(shù)據(jù)的時(shí)候,程序就會(huì)報(bào)錯(cuò)。
為了解決這個(gè)問(wèn)題,我們有4種辦法:
- 修改MongoDB的配置,延長(zhǎng)游標(biāo)超時(shí)時(shí)間,并重啟MongoDB。由于生產(chǎn)環(huán)境的MongoDB不能隨便重啟,所以這個(gè)方案雖然有用,但是排除。
- 一次性把數(shù)據(jù)全部讀取下來(lái),再做處理:
all_data = [row for row in handler.find()] for row in all_data: parse(row)
這種方案的弊端也很明顯,如果數(shù)據(jù)量非常大,你不一定能全部放到內(nèi)存里面。即使能夠全部放到內(nèi)存中,但是列表推導(dǎo)式遍歷了所有數(shù)據(jù),緊接著for循環(huán)又遍歷一次,浪費(fèi)時(shí)間。
3.讓游標(biāo)每次返回的數(shù)據(jù)小于100條,這樣消費(fèi)完這一批數(shù)據(jù)的時(shí)間就會(huì)小于10分鐘:
# 每次連接數(shù)據(jù)庫(kù),只返回50行數(shù)據(jù) for row in handler.find().batch_size(50): parse_data(row)
但這種方案會(huì)增加數(shù)據(jù)庫(kù)的連接次數(shù),從而增加I/O耗時(shí)。
4.讓游標(biāo)永不超時(shí)。通過(guò)設(shè)定參數(shù)no_cursor_timeout=True,讓游標(biāo)永不超時(shí):
cursor = handler.find(no_cursor_timeout=True) for row in cursor: parse_data(row) cursor.close() # 一定要手動(dòng)關(guān)閉游標(biāo)
然而這個(gè)操作非常危險(xiǎn),因?yàn)槿绻愕腜ython程序因?yàn)槟撤N原因意外停止了,這個(gè)游標(biāo)就再也無(wú)法關(guān)閉了!除非重啟MongoDB,否則這些游標(biāo)會(huì)一直留在MongoDB上,占用資源。
當(dāng)然可能有人會(huì)說(shuō),使用try...except把讀取數(shù)據(jù)的地方包住,只要拋出了異常,在處理異常的時(shí)候關(guān)閉游標(biāo)即可:
cursor = handler.find(no_cursor_timeout=True) try: for row in cursor: parse_data(row) except Exception: parse_exception() finally: cursor.close() # 一定要手動(dòng)關(guān)閉游標(biāo)
其中finally里面的代碼,無(wú)論有沒(méi)有異常,都會(huì)執(zhí)行。
但這樣寫會(huì)讓代碼非常難看。為了解決這個(gè)問(wèn)題,我們可以使用游標(biāo)的上下文管理器:
with handler.find(no_cursor_timeout=True) as cursor: for row in cursor: parse_data(row)
只要程序退出了with的縮進(jìn),游標(biāo)自動(dòng)就會(huì)關(guān)閉。如果程序中途報(bào)錯(cuò),游標(biāo)也會(huì)關(guān)閉。
它的原理可以用下面兩段代碼來(lái)解釋:
class Test: def __init__(self): self.x = 1 def echo(self): print(self.x) def __enter__(self): print('進(jìn)入上下文') return self def __exit__(self, *args): print('退出上下文') with Test() as t: t.echo() print('退出縮進(jìn)')
運(yùn)行效果如下圖所示:
接下來(lái)在with的縮進(jìn)里面人為制造異常:
class Test: def __init__(self): self.x = 1 def echo(self): print(self.x) def __enter__(self): print('進(jìn)入上下文') return self def __exit__(self, *args): print('退出上下文') with Test() as t: t.echo() 1 + 'a' # 這里一定會(huì)報(bào)錯(cuò) print('退出縮進(jìn)')
運(yùn)行效果如下圖所示:
無(wú)論在with的縮進(jìn)里面發(fā)生了什么,Test這個(gè)類中的__exit__里面的代碼始終都會(huì)運(yùn)行。
我們來(lái)看看pymongo的游標(biāo)對(duì)象里面,__exit__是怎么寫的,如下圖所示:
可以看到,這里正是關(guān)閉游標(biāo)的操作。
因此,如果我們使用上下文管理器,就可以放心大膽地使用no_cursor_timeout=True參數(shù)了。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
MongoDB數(shù)據(jù)庫(kù)用戶角色和權(quán)限管理詳解
這篇文章主要給大家介紹了關(guān)于MongoDB數(shù)據(jù)庫(kù)用戶角色和權(quán)限管理的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11Mongodb在UPDATE操作中使用$push向數(shù)組中插入數(shù)據(jù)的方法
在update操作中,使用$push操作符向數(shù)組中插入新的元素,按照相應(yīng)的語(yǔ)法,使用$push操作符,下面通過(guò)本文給大家分享Mongodb在UPDATE操作中使用$push向數(shù)組中插入數(shù)據(jù)的方法,感興趣的朋友一起看看吧2024-06-06關(guān)于mongoDB數(shù)據(jù)庫(kù)添加賬號(hào)的問(wèn)題
這篇文章主要介紹了mongoDB數(shù)據(jù)庫(kù)添加賬號(hào)的問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02MongoDB性能篇之創(chuàng)建索引,組合索引,唯一索引,刪除索引和explain執(zhí)行計(jì)劃
這篇文章主要介紹了MongoDB性能篇之創(chuàng)建索引,組合索引,唯一索引,刪除索引和explain執(zhí)行計(jì)劃的相關(guān)資料,需要的朋友可以參考下2016-02-02Mongoose find 查詢返回json數(shù)據(jù)處理方式
這篇文章主要介紹了Mongoose find 查詢返回json數(shù)據(jù)處理方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04