這個(gè)使用場景,Etcd 比 Redis 強(qiáng)
我們說,要評(píng)判一個(gè)東西的好壞,一定要說明具體在什么業(yè)務(wù)場景。脫離業(yè)務(wù)談好壞是沒有意義的。
Redis 非常強(qiáng)大,我出版過一本書專門介紹 Redis 的各種用法。但這并不是說 Redis 在各種方面都沒有對(duì)手。至少在分布式系統(tǒng)的配置更新這個(gè)場景上面,我認(rèn)為 etcd 做得更好。
要解釋這個(gè)問題,我們來看一個(gè)具體的業(yè)務(wù)場景:
在 Redis 中有一個(gè)列表 sentence,里面會(huì)源源不斷地寫入字符串?,F(xiàn)在我有一個(gè)過濾程序:trash_filter.py,它一條一條從 Redis 讀取數(shù)據(jù),判斷字符串中是否有特定的關(guān)鍵詞,如果有,那么直接丟棄。如果沒有,那么把數(shù)據(jù)存入 MongoDB。
這個(gè)場景非常簡單,于是你很快就寫出了一個(gè) Python 程序:
- import redis
- class TrashFilter:
- def __init__(self):
- self.client = redis.Redis()
- self.trash_words = ['垃圾']
- def read_data(self):
- while True:
- data = self.client.lpop('sentence')
- if not data:
- return
- yield data.decode()
- def do_filter(self):
- for sentence in self.read_data():
- for word in self.trash_words:
- if word in sentence:
- break
- else:
- self.save_sentence(sentence)
- def save_sentence(self, sentence):
- print('進(jìn)行后續(xù)保存 sentence 的操作:', sentence)
- if __name__ == '__main__':
- trash_filter = TrashFilter()
- trash_filter.do_filter()
在上面的代碼中,需要過濾的詞是以列表的形式直接寫到代碼里面的。那么問題來了,如果這些過濾詞是動(dòng)態(tài)改變的怎么辦?每次為了修改這些詞,你都需要重啟一下這個(gè)程序嗎?
可能有同學(xué)提到,可以把這些詞存放到數(shù)據(jù)庫里面,每次從數(shù)據(jù)庫里面讀取就可以了。Redis 本身就是一個(gè) Key-Value 數(shù)據(jù)庫,可以直接使用 Redis 的字符串來存放:
- def do_filter(self):
- for sentence in self.read_data():
- for word in self.client.get('trash_words').decode().split(','):
- if word in sentence:
- break
- else:
- self.save_sentence(sentence)
把所有的過濾詞以英文逗號(hào)分割組成長字符串,儲(chǔ)存到Redis 中名為trash_words的字符串里。每讀取到一個(gè)句子,都從 Redis 里面再次讀取這個(gè)過濾詞列表,然后進(jìn)行檢查。
這樣做,實(shí)時(shí)性確實(shí)得到了保障,每次只要trash_word字符串一發(fā)生修改,程序立刻就能獲取到最新的過濾詞。
但這樣做有一個(gè)問題——每次讀取trash_words是需要請(qǐng)求網(wǎng)絡(luò)的,而網(wǎng)絡(luò) IO 是非常費(fèi)時(shí)間的。
那么我們是不是可以每5分鐘獲取一次最新的trash_words呢?當(dāng)然也可以,我在文章:一日一技:實(shí)現(xiàn)有過期時(shí)間的LRU緩存中介紹過如何實(shí)現(xiàn)一個(gè)帶有過期時(shí)間的 LRU 緩存。
這樣做,速度確實(shí)提高了,但是實(shí)時(shí)性又降低了。
如果讀者對(duì) Redis 比較熟悉,當(dāng)然也可以使用 Lua 腳本或者 Redis 的Pipeline 實(shí)現(xiàn)在一次請(qǐng)求里面同時(shí)獲取一條句子并拿到過濾詞列表,或者使用 Monitor 命令監(jiān)控 Key 的變化。但代碼寫起來會(huì)比較復(fù)雜。
有沒有又快又簡單還穩(wěn)定的解決方案呢?答案是有,那就是使用 etcd.
etcd 的官網(wǎng)寫著這樣一句話:
A distributed, reliable key-value store for the most critical data of a distributed system.
用于分布式系統(tǒng)最關(guān)鍵數(shù)據(jù)的分布式、可靠的鍵值儲(chǔ)存。
etcd 本來就是為了分布式系統(tǒng)而生的,它專注于鍵值儲(chǔ)存。初看起來,相當(dāng)于只是 Redis 的字符串功能,但卻比 Redis 的字符串更為強(qiáng)大。
我們可以監(jiān)控 etcd 中的一個(gè)鍵,當(dāng)它發(fā)生變化的時(shí)候,就調(diào)用我們提前定義好的函數(shù)。
在 Ubuntu 中,可以使用 apt-get 安裝 etcd,在 macOS 中,可以使用 homebrew 安裝 etcd。當(dāng)然 etcd 也有已經(jīng)編譯好的可執(zhí)行文件,可以從Releases · etcd-io/etcd · GitHub[1]下載下來直接運(yùn)行就能啟動(dòng)一個(gè)單節(jié)點(diǎn)的 etcd 服務(wù)。
啟動(dòng)服務(wù)以后,我們?cè)賮戆惭b一個(gè)Python 庫,用來操作 etcd:
- pip install etcd3
Python 讀寫 etcd 非常簡單:
- import etcd3
- client = etcd3.client()
- client.put('key', value) # 添加數(shù)據(jù)
- value, kv_meta = client.get('key') # 讀取數(shù)據(jù),返回的數(shù)據(jù)value 是 bytes 型數(shù)據(jù)
而我們要用的,是 etcd 的watch功能。我們先寫一段簡單的代碼來說明 watch的功能:
- import etcd3
- import time
- def callback(response):
- for event in response.events:
- print(f'Key: {event.key}發(fā)生改變!新的值是:{event.value}')
- client = etcd3.client()
- client.add_watch_callback('test', callback)
- for i in range(100):
- print(i)
- time.sleep(1)
正常情況下,這個(gè)程序會(huì)打印從0到99,每秒打印一個(gè)數(shù)字。但是當(dāng)我們中途修改了 etcd 中,名為test這個(gè) key 的值以后,我們發(fā)現(xiàn)回調(diào)函數(shù)被運(yùn)行了,如下圖所示:
可以看到,etcd 監(jiān)控一個(gè) key 是否變化,它不像 Redis 的blpop這樣阻塞式地監(jiān)控,而是在后臺(tái)監(jiān)控,當(dāng)key 的值發(fā)生了改變時(shí),觸發(fā)一個(gè)事件,調(diào)用回調(diào)函數(shù)。所以整個(gè)監(jiān)控的過程不會(huì)干預(yù)我們自己程序的正常運(yùn)行。
在一般情況下,傳入回調(diào)函數(shù)的response 對(duì)象,它的.events屬性是只有一個(gè)元素的列表。但如果這個(gè) key 在極短時(shí)間內(nèi)變化了很多次,那么這個(gè)列表里面可能有多個(gè)值。
回到最開始需要解決的問題,我們引入 etcd 以后,困難輕輕松松就被解決了:
通過增加方框框住的update_trash_words方法,并把它作為監(jiān)控trash_words這個(gè)Key 變化事件的回調(diào)函數(shù),一旦這個(gè) Key 發(fā)生了變化,就會(huì)調(diào)用回調(diào)函數(shù),從而更新self.trash_words這個(gè)屬性。
運(yùn)行效果如下圖所示:
可以看到,在紅線上面,我是有臟數(shù)據(jù)的句子是不被過濾的,此時(shí)臟字也不是過濾詞。但是當(dāng)我們?cè)诿钚欣锩娓铝? etcd,把新的過濾詞改成垃圾,臟以后,就到了紅線下面,我是有臟數(shù)據(jù)的句子就會(huì)被過濾了。
這樣就做到了同時(shí)兼顧時(shí)效性和速度,避免了無效的網(wǎng)絡(luò)請(qǐng)求。
參考文獻(xiàn)
[1] Releases · etcd-io/etcd · GitHub: https://github.com/etcd-io/etcd/releases
本文轉(zhuǎn)載自微信公眾號(hào)「未聞Code」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系未聞Code公眾號(hào)。