1. 程式人生 > 資料庫 >MongoDB遊標超時問題的4種解決方法

MongoDB遊標超時問題的4種解決方法

當我們使用Python從MongoDB裡面讀取資料時,可能會這樣寫程式碼:

import pymongo

handler = pymongo.MongoClient().db.col

for row in handler.find():
 parse_data(row)

短短4行程式碼,讀取MongoDB裡面的每一行資料,然後傳入parse_data做處理。處理完成以後再讀取下一行。邏輯清晰而簡單,能有什麼問題?只要parse_data(row)不報錯,這一段程式碼就完美無缺。

但事實並非這樣。

你的程式碼可能會在for row in handler.find()這一行報錯。它的原因,說來話長。

要解釋這個問題,我們首先就需要知道,handler.find()返回的並不是資料庫裡面的資料,而是一個遊標(cursor)物件。如下圖所示:

只有當你使用for迴圈開始迭代它的時候,遊標才會真正去資料庫裡面讀取資料。

但是,如果每一次迴圈都連線資料庫,那麼網路連線會浪費大量時間。

所以pymongo會一次性獲取100行,for row in handler.find()迴圈第一次的時候,它會連上MongoDB,讀取一百條資料,快取到記憶體中。於是第2-100次迴圈,資料都是直接從記憶體裡面獲取,不會再連線資料庫。

當迴圈進行到底101次的時候,再一次連線資料庫,再讀取第101-200行內容……

這個邏輯非常有效地降低了網路I/O耗時。

但是,MongoDB預設遊標的超時時間是10分鐘。10分鐘之內,必需再次連線MongoDB讀取內容重新整理遊標時間,否則,就會導

致遊標超時報錯:

pymongo.errors.CursorNotFound: cursor id 211526444773 not found

如下圖所示:

所以,回到最開始的程式碼中來,如果parse_data每次執行的時間超過6秒鐘,那麼它執行100次的時間就會超過10分鐘。此時,當程式想讀取第101行資料的時候,程式就會報錯。

為了解決這個問題,我們有4種辦法:

  1. 修改MongoDB的配置,延長遊標超時時間,並重啟MongoDB。由於生產環境的MongoDB不能隨便重啟,所以這個方案雖然有用,但是排除。
  2. 一次性把資料全部讀取下來,再做處理:
all_data = [row for row in handler.find()]

for row in all_data:
 parse(row)

這種方案的弊端也很明顯,如果資料量非常大,你不一定能全部放到記憶體裡面。即使能夠全部放到記憶體中,但是列表推導式遍歷了所有資料,緊接著for迴圈又遍歷一次,浪費時間。

3.讓遊標每次返回的資料小於100條,這樣消費完這一批資料的時間就會小於10分鐘:

# 每次連線資料庫,只返回50行資料
for row in handler.find().batch_size(50): 
 parse_data(row)

但這種方案會增加資料庫的連線次數,從而增加I/O耗時。

4.讓遊標永不超時。通過設定引數no_cursor_timeout=True,讓遊標永不超時:

cursor = handler.find(no_cursor_timeout=True)
for row in cursor:
 parse_data(row)
cursor.close() # 一定要手動關閉遊標

然而這個操作非常危險,因為如果你的Python程式因為某種原因意外停止了,這個遊標就再也無法關閉了!除非重啟MongoDB,否則這些遊標會一直留在MongoDB上,佔用資源。

當然可能有人會說,使用try...except把讀取資料的地方包住,只要丟擲了異常,在處理異常的時候關閉遊標即可:

cursor = handler.find(no_cursor_timeout=True)
try:
 for row in cursor:
 parse_data(row)
except Exception:
 parse_exception()
finally:
 cursor.close() # 一定要手動關閉遊標

其中finally裡面的程式碼,無論有沒有異常,都會執行。

但這樣寫會讓程式碼非常難看。為了解決這個問題,我們可以使用遊標的上下文管理器:

with handler.find(no_cursor_timeout=True) as cursor:
 for row in cursor:
  parse_data(row)

只要程式退出了with的縮排,遊標自動就會關閉。如果程式中途報錯,遊標也會關閉。

它的原理可以用下面兩段程式碼來解釋:

class Test:
 def __init__(self):
  self.x = 1

 def echo(self):
  print(self.x)

 def __enter__(self):
  print('進入上下文')
  return self

 def __exit__(self,*args):
  print('退出上下文')
  
with Test() as t:
 t.echo()
print('退出縮排')

執行效果如下圖所示:

接下來在with的縮排裡面人為製造異常:

class Test:
 def __init__(self):
  self.x = 1

 def echo(self):
  print(self.x)

 def __enter__(self):
  print('進入上下文')
  return self

 def __exit__(self,*args):
  print('退出上下文')
  
with Test() as t:
 t.echo()
 1 + 'a' # 這裡一定會報錯
print('退出縮排')

執行效果如下圖所示:

無論在with的縮排裡面發生了什麼,Test這個類中的__exit__裡面的程式碼始終都會執行。

我們來看看pymongo的遊標物件裡面,__exit__是怎麼寫的,如下圖所示:

可以看到,這裡正是關閉遊標的操作。

因此,如果我們使用上下文管理器,就可以放心大膽地使用no_cursor_timeout=True引數了。

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對我們的支援。