一日一技:實(shí)現(xiàn)有過(guò)期時(shí)間的LRU緩存
在一日一技:實(shí)現(xiàn)函數(shù)調(diào)用結(jié)果的 LRU 緩存一文中,我們提到Python自帶的LRU緩存lru_cache。通過(guò)這個(gè)裝飾器可以非常輕松地實(shí)現(xiàn)緩存。
現(xiàn)在我們考慮下面這個(gè)應(yīng)用場(chǎng)景:MongoDB中有100對(duì)id-用戶(hù)名的對(duì)應(yīng)關(guān)系,我從Redis中持續(xù)不斷讀取id,如果id能在MongoDB中找到對(duì)應(yīng)關(guān)系,那么就把對(duì)應(yīng)的用戶(hù)名打印出來(lái)。如果找不到對(duì)應(yīng)關(guān)系,那么就把這個(gè)id丟棄。
為了防止頻繁讀取MongoDB,我在程序開(kāi)始的時(shí)候直接讀取這一百對(duì)對(duì)應(yīng)關(guān)系,并存為字典:
- import pymongo
- import redis
- client = redis.Redis()
- handler = pymongo.MongoClient().weibo.id_name_map
- def read_id_name_map():
- id_name = {}
- for row in handler.find():
- id_name[row['id']] = row['name']
- return id_name
- id_name_map = read_id_name_map()
- while True:
- data = client.blpop('weibo_id')
- user_id = data[1].decode()
- if user_id in id_name_map:
- print(id_name_map[user_id])
大家可以思考一下,上面這段代碼有沒(méi)有什么問(wèn)題。然后繼續(xù)看后面。
如果我現(xiàn)在需要再增加100個(gè)id-用戶(hù)名的對(duì)應(yīng)關(guān)系怎么辦?
由于這個(gè)程序運(yùn)行以后就一直阻塞式地讀取Redis,不會(huì)停止,所以整個(gè)過(guò)程只會(huì)讀取一次MongoDB。后面即使我向MongoDB中添加了新的對(duì)應(yīng)關(guān)系,只要程序不重啟,就無(wú)法讀取到新的對(duì)應(yīng)關(guān)系。
肯定有同學(xué)想到,在while循環(huán)里面增加一個(gè)計(jì)時(shí)器,每x分鐘就重新調(diào)用一下read_id_name_map()函數(shù),更新對(duì)應(yīng)關(guān)系。
不過(guò)今天我們要講的是另一個(gè)更有創(chuàng)意的辦法,使用lru_cache來(lái)實(shí)現(xiàn)。
對(duì)于這個(gè)例子來(lái)說(shuō),lru_cache的maxsize參數(shù)只需要設(shè)置為1,因?yàn)橹恍枰娣?份對(duì)應(yīng)關(guān)系即可。那么我們?nèi)绾巫龅剑热缑?0分鐘更新一次呢?我們知道,在使用lru_cache時(shí),如果調(diào)用同一個(gè)函數(shù),并且傳入的參數(shù)相同,那么從第二次開(kāi)始就會(huì)使用緩存。現(xiàn)在我們?nèi)绾巫寱r(shí)間在每10分鐘內(nèi)相同呢?
我們來(lái)看現(xiàn)在的時(shí)間戳:1578399211.30042
它除以600,值是1578399211.30042 // 600 = 2630665.0。然后我讓這個(gè)時(shí)間戳加5分鐘,也就是增加300秒,變成1578399511.30042。這個(gè)新的時(shí)間戳再除以600,發(fā)現(xiàn)結(jié)果還是2630665.0。但如果原來(lái)的時(shí)間戳增加超過(guò)10分鐘,例如增加了601秒,我們?cè)賮?lái)看看效果(1578399211.30042 + 601) // 600 = 2630666.0,此時(shí)的結(jié)果也發(fā)生了變化。
利用這個(gè)特點(diǎn),修改一下我們的代碼:
- import pymongo
- import redis
- import time
- from functools import lru_cache
- client = redis.Redis()
- handler = pymongo.MongoClient().weibo.id_name_map
- @lru_cache(maxsize=1)
- def read_id_name_map(_):
- id_name = {}
- for row in handler.find():
- id_name[row['id']] = row['name']
- return id_name
- while True:
- data = client.blpop('weibo_id')
- id_name_map = read_id_name_map(time.time() // 600)
- user_id = data[1].decode()
- if user_id in id_name_map:
- print(id_name_map[user_id])
現(xiàn)在,我們直接在while循環(huán)內(nèi)部調(diào)用read_id_name_map,如果兩次調(diào)用的時(shí)間間隔小于600秒,那么time.time() // 600的值是相同的,第二次直接使用緩存,也就不會(huì)查詢(xún)MongoDB了。當(dāng)時(shí)間超過(guò)10分鐘后,時(shí)間戳除以600的值增加了,于是緩存沒(méi)有命中,進(jìn)入查詢(xún)MongoDB的過(guò)程,更新id_name_map。實(shí)現(xiàn)了有過(guò)期時(shí)間的LRU緩存。
補(bǔ)充:可能有同學(xué)注意到定義read_id_name_map函數(shù)的時(shí)候,參數(shù)我寫(xiě)的是下劃線(xiàn)。這是Python 編碼規(guī)范中建議的一種寫(xiě)法。當(dāng)一個(gè)變量不會(huì)被使用,但又需要保留時(shí),就可以用下劃線(xiàn)表示。
本文轉(zhuǎn)載自微信公眾號(hào)「未聞Code」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系未聞Code公眾號(hào)。