天天看點

mongodb 重新開機_4種方法解決MongoDB遊标逾時的問題

當我們使用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)對象

。如下圖所示:

mongodb 重新開機_4種方法解決MongoDB遊标逾時的問題

隻有當你使用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
           

如下圖所示:

mongodb 重新開機_4種方法解決MongoDB遊标逾時的問題

是以,回到最開始的代碼中來,如果

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循環又周遊一次,浪費時間。

  1. 讓遊标每次傳回的資料小于100條,這樣消費完這一批資料的時間就會小于10分鐘:
# 每次連接配接資料庫,隻傳回50行資料
for row in handler.find().batch_size(50): 
    parse_data(row)
           

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

  1. 讓遊标永不逾時。通過設定參數

    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('退出縮進')
           

運作效果如下圖所示:

mongodb 重新開機_4種方法解決MongoDB遊标逾時的問題

接下來在

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('退出縮進')
           

運作效果如下圖所示:

mongodb 重新開機_4種方法解決MongoDB遊标逾時的問題

無論在

with

的縮進裡面發生了什麼,

Test

這個類中的

__exit__

裡面的代碼始終都會運作。

我們來看看pymongo的遊标對象裡面,

__exit__

是怎麼寫的,如下圖所示:

mongodb 重新開機_4種方法解決MongoDB遊标逾時的問題

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

是以,如果我們使用上下文管理器,就可以放心大膽地使用

no_cursor_timeout=True

參數了。