2008-11-11 249 views
24

我想實現(我認爲是)一個櫃檯一個很簡單的數據模型:Django中的原子操作?

class VisitorDayTypeCounter(models.Model): 
    visitType = models.CharField(max_length=60) 
    visitDate = models.DateField('Visit Date') 
    counter = models.IntegerField() 

當有人經過,它會尋找一個排的visitType和visitDate匹配;如果該行不存在,則會使用counter = 0創建它。

然後我們增加計數器並保存。

我擔心的是這個過程完全是一場比賽。兩個請求可以同時檢查,看看實體是否在那裏,並且他們都可以創建它。在讀取計數器並保存結果之間,可能會發出另一個請求並將其增加(導致計數丟失)。

到目前爲止,我還沒有找到一個很好的解決方法,無論是在Django文檔還是在教程中(實際上,它看起來像教程在它的投票部分有一個競爭條件)。

我該如何安全地做到這一點?

回答

1

這是一個黑客。原始的SQL將使你的代碼更加便於移植,但它會擺脫計數器增加的競爭條件。理論上,每當你做查詢時,這應該增加計數器。我沒有測試過這個,所以你應該確保列表在查詢中正確插入。

class VisitorDayTypeCounterManager(models.Manager): 
    def get_query_set(self): 
     qs = super(VisitorDayTypeCounterManager, self).get_query_set() 

     from django.db import connection 
     cursor = connection.cursor() 

     pk_list = qs.values_list('id', flat=True) 
     cursor.execute('UPDATE table_name SET counter = counter + 1 WHERE id IN %s', [pk_list]) 

     return qs 

class VisitorDayTypeCounter(models.Model): 
    ... 

    objects = VisitorDayTypeCounterManager() 
5

兩個建議:

添加unique_together到模型,幷包裹創建一個異常處理程序來捕獲重複:

class VisitorDayTypeCounter(models.Model): 
    visitType = models.CharField(max_length=60) 
    visitDate = models.DateField('Visit Date') 
    counter = models.IntegerField() 
    class Meta: 
     unique_together = (('visitType', 'visitDate')) 

在此之後,你可以stlll對未成年人的比賽狀態計數器的更新。如果您獲得足夠的流量來關注這個問題,我會建議查看更細粒度數據庫控制的交易。我不認爲ORM直接支持鎖定/同步。交易文件可用here

+0

的unique_together肯定讓我覺得有點更舒適。 可能的話,這裏沒有足夠的流量導致比賽受到打擊,但由於我在同一時間學習Django,我想我想「做對」。 感謝您的幫助! – 2008-11-11 06:07:58

+0

是的,我聽到你的聲音。也許在這裏的其他人會意識到一個ORM功能來處理這個問題,或者可以清除一些內置的安全措施是否對這種情況安全。 – 2008-11-11 06:35:07

1

爲什麼不使用數據庫作爲併發層?將表的主鍵或唯一約束添加到visitType和visitDate。如果我沒有弄錯,django在他們的數據庫模型類中並不完全支持這個,或者至少我沒有看到過一個例子。

一旦你添加的約束/鍵的表,那麼所有你需要做的是:如果

  1. 檢查行是存在的。如果是,取回它。
  2. 插入該行。如果沒有錯誤,你很好,可以繼續前進。
  3. 如果有錯誤(即競爭條件),請重新獲取該行。如果沒有行,那麼這是一個真正的錯誤。否則,你沒事。

這樣做很討厭,但它看起來足夠快,可以覆蓋大多數情況。

+0

它不處理兩個人同時更新計數器的情況。 – 2008-11-11 06:28:07

0

您應該使用數據庫事務來避免這種競爭條件。通過事務處理,您可以執行在「全部或全部」基礎上創建,讀取,遞增和保存計數器的整個操作。如果出現任何問題,它會回滾整個事情,你可以再試一次。

查看Django docs.有一個事務中間件,或者你可以在視圖或方法周圍使用裝飾器來創建事務。

+0

我同意事務似乎在這裏的答案,但不清楚的是,功能將實際解決增量問題 - 獲取該行的SELECT仍然會成功,而更改計數器值的UPDATE仍然會成功。如果我錯了,一個例子會很棒。 – 2008-11-11 13:39:57

+0

您需要在選擇過程中鎖定表格以便以這種方式進行,正如Sam所提到的那樣會拖慢您的表現。如果你不經常招惹櫃檯,這是最好的方法。 – 2008-11-12 03:20:56

12

如果您確實希望計數器準確無誤,您可以使用事務,但所需的併發量將真正拖累您的應用程序和數據庫在任何重要負載下下降。相反,考慮採用更多消息傳遞風格的方法,並將每次訪問的計數記錄存入表中,以便增加計數器。然後,當您希望總訪問次數在訪問表上進行計數時。您也可以有一個後臺進程,每天運行任意次數,將訪問總和,然後將其存儲在父表中。爲了節省空間,它也會刪除它總結的子訪問表中的任何記錄。如果您沒有多個代理爭奪相同資源(計數器),您將減少併發成本。

+0

嘿,好的電話!我一直在做App Engine的工作,而且我掛斷了「事務只能在一個入口上運行」和「執行聚合函數非常昂貴」。這是解決問題的一個非常簡單的方法。謝謝! – 2008-11-11 18:27:24

+0

我想這取決於這個過程是否會變得重讀或寫入重量。計數會比我的系統中增加更多,所以對於所述的問題,這可能不是最好的計劃。但是,它解決了我的其他問題,非常感謝! – 2008-11-11 19:28:21

6

您可以使用http://code.djangoproject.com/ticket/2705的補丁來支持數據庫級鎖定。

隨着補丁此代碼將是原子:

visitors = VisitorDayTypeCounter.objects.get(day=curday).for_update() 
visitors.counter += 1 
visitors.save()