Counter
在 datastore 中的 data entity 只有一個,每次新增資料時,程式都會到同一個地方作資料寫入(update 也是一種 write)的動作,如果新增資料的頻率很高,那就很容易發生寫入衝突(write contention)而使得程式的效能低落。試想你在作一個類似 Twitter 的網站,使用者增加訊息的速度是非常驚人的,如果只使用單一個 data entity 來儲存、更新計數器,那每則訊息的更新時間就會變長,因為都在等待更新計數器的動作。如果你曾經學習過用在磁碟機上的 RAID 技術,那便可以借鏡 RAID 5 規格中分散資料擺放的方式,使得各個讀寫資料的動作可以同時進行。
以計數器為例,可以試著把計數器拆開成好幾個碎片(shard),在更新計數器時,可以只要隨便選一個碎片來更新就可以了,如果要取得完整的數值,只要把各碎片的值加總就得到答案了。
這樣的概念,程式要怎麼寫呢?首先把
Counter
的定義修改成這樣:from google.appengine.ext import db
class Counter(db.Model):
name = db.StringProperty(required=True)
count = db.IntegerProperty(required=True, default=0)
增加了一個
name
欄位,這是為了標記碎片是屬於哪一個計數器,也就是 name
欄位相同的碎片就是屬於同一個計數器。有了這樣的 model 之後,更新計數器的程式碼可以改寫成這樣:...
import random
shard_name = 'product_counter_%d' % random.randint(0, 3) # 假設我分成4個碎片
counter = Counter.get_by_key_name(shard_name)
if counter is None:
counter = Counter(key_name=shard_name, name='product_counter')
counter.count += 1
counter.put()
上面的例子是我預計將計數器切成 4 個碎片,然後每個「碎片 entity」的
key_name
分別為 product_counter_0
, ..., product_counter_3
,於是便能夠直接根據 key_name
將碎片的 entity 取出,若是碎片不存在就立刻產生一個新的 entity,而新的 entity 除了要設定 key_name
之外,最重要的就是要記得標上 name
欄位的值,因為當我們需要加總計數器時,就是靠著這個欄位來取出各個碎片。最後則是將 count
欄位的值加 1 後儲存,完成更新計數器的動作。接下來要處理的就是加總的動作囉,因為可以藉著
name
欄位來查詢,所以程式也變得很簡單:...
counters = Counter.all().filter('name =', 'product_counter')
total = 0
for counter in counters:
total += counter.count
所以
total
的值就是計數器正確的數值了。改善效能
上面的例子雖然改善了寫入衝突的問題,但光是這樣作,除了每次讀取計數器數值時都要做一次查詢(耗費 Datastore API 呼叫次數)以外,碎片如果太多也會增加計算時間,為了改善這個問題,可以為計數器作 cache。
Google App Engine 有提供 Memcache 的服務,我們可以直接拿它來作為計數器的 cache,首先從計數器的加總下手,一旦數值被計算出來後就放進 cache,如此一來,讀取計數器數值時就可以先從 cache 讀取,不在 cache 裡才去作資料查詢:
# 讀取計數器總數時,使用 cache
from google.appengine.api import memcache
counter_name = 'product_counter'
total = memcache.get(counter_name) # 這裡用 counter_name 作為 cache key
if total is None:
total = 0
counters = Counter.all().filter('name =', counter_name)
for counter in counters:
total += counter.count
memcache.set(counter_name, total)
那當計數器被更新的時候怎麼辦?除了更新 entity 之外,若是計數器的 cache 還存在,可以直接把 cache 裡的值加 1,這樣計數器的總數就不必重新計算了:
# 更新計數器時,別忘了更新 cache
from google.appengine.api import memcache
import random
counter_name = 'product_counter'
shard_name = 'product_counter_%d' % random.randint(0, 3) # 假設我分成4個碎片
counter = Counter.get_by_key_name(shard_name)
if counter is None:
counter = Counter(key_name=shard_name, name=counter_name)
counter.count += 1
counter.put()
# 更新 cache
total = memcache.get(counter_name)
if total:
memcache.incr(counter_name) # 使用 incr 函式直接對數值資料加 1
這樣就不必擔心更新了計數器,卻忘記更新快取了。
第二段的程式碼, 這三行
回覆刪除counter = Counter.get_by_key_name(shard_name)
if counter is None:
counter = Counter(key_name=shard_name, name='product_counter')
如果改成
counter = Counter.get_or_insert(shard_name, name='product_counter')
會不會比較好呢? ;)
@scw,
回覆刪除你說的沒錯,感謝你的補充 :-)