2012-10-01 52 views
3

作爲註冊新用戶的一部分;我們從一個預編譯列表(一個表)中爲它們分配一個資源(在這種情況下是一個Solr核心)。不同的交易必須保證選擇不同的項目;避免爭用

如果5個用戶註冊後,它們必須被分配5個不同的核;如果用戶成功註冊,那麼分配將成爲最終的(參見下面的描述)。

但在現實世界中,同時註冊新用戶爭奪同一行,不選擇不同的行。如果X需要5秒註冊,Y和Z的註冊這是在X的「期限」將作爲他們爭奪同一行的X.失敗

問題:如何進行交易選擇不爭,即使在高併發性,如每秒100次註冊?

table: User 
user_id name core 
     1 Amy h1-c1 
     2 Anu h1-c1 
     3 Raj h1-c1 
     4 Ron h1-c2 
     5 Jon h1-c2 

table: FreeCoreSlots 
core_id core status 
     1 h1-c1 used 
     2 h1-c1 used 
     3 h1-c1 used 
     4 h1-c2 used 
     5 h1-c2 used #these went to above users already 
     6 h1-c2 free 
     7 h1-c2 free 
     8 h1-c2 free 
     9 h1-c2 free 

僞代碼,如果東西分離:

sql = SQLTransaction() 
core_details = sql.get("select * from FreeCoreSlots limit 1") 
sql.execute("update FreeCoreSlots set status = 'used' where id = {id}".format(
    id = core_details["id"])) 
sql.execute("insert into users (name,core) values ({name},{core})".format(
    name = name, 
    id = core_details["id"])) 
sql.commit() 

如果100註冊等發生第二,他們會爭在FreeCoreSlots第一行並造成嚴重的失敗。

有一個SELECT ... FOR UPDATE在InnoDB SELECT ... FOR UPDATE statement locking all rows in a table的解決方案,但他們似乎表明降低隔離。這種方法是否正確?

+0

我認爲這個例子不需要使用存儲過程:http://stackoverflow.com/a/562744/1584772。至少,這是一個開始的好地方(關鍵是要讓這一切都發生在一次交易中)。 – dan1111

回答

2

我想問的問題是,爲什麼它可能需要5秒,用戶來完成。在START TRANSACTIONCOMMIT之間應該只有幾分之一秒。

爲了防止您在FreeCoreSlots中將同一行重新分配給同一用戶,必須使用SELECT for UPDATE。在我看來,鎖定水平並不是真正的問題。您設計數據庫的方式在FreeCoreSlots中的下一空閒行實際上被鎖定,直到事務完成。見下面我的測試結果。而且我認爲即使對於每秒100個新用戶,它仍應該足夠。但是,如果你甚至想要克服這一點,你必須找到一種方法來鎖定每個用戶的FreeCoreSlots中的另一個空閒行。不幸的是,除非有鎖,否則沒有「選擇第一行」的功能。也許改爲使用一些隨機或模數邏輯。但正如我已經說過,我認爲這不應該成爲你的問題,即使每秒不可思議的100個新用戶數量。如果我錯了,隨時留下評論,我願意再看看它。

這裏是我的測試結果。默認InnoDB的鎖級別:重複的讀取沒有FOR UPDATE。這樣它不起作用。

User 1: 
    START TRANSACTION 
    SELECT * FROM FreeCoreSlots WHERE status = 'FREE' LIMIT 1 -- returns id 1 
User 2: 
    START TRANSACTION 
    SELECT * FROM FreeCoreSlots WHERE status = 'FREE' LIMIT 1 -- also returns id 1 !!! 
User 1: 
    UPDATE FreeCoreSlots SET status = 'USED' where ID = 1; 
User 2: 
    UPDATE FreeCoreSlots SET status = 'USED' where ID = 1; -- WAITS !!! 
User 1: 
    INSERT INTO user VALUES (... 
    COMMIT; 
USER 2: 
    wait ends and updates also ID = 1 which is WRONG 

鎖定級別重複的讀取FOR UPDATE。這樣做它的工作。

User 1: 
    START TRANSACTION 
    SELECT * FROM FreeCoreSlots WHERE status = 'FREE' LIMIT 1 FOR UPDATE -- returns id 1 
User 2: 
    START TRANSACTION 
    SELECT * FROM FreeCoreSlots WHERE status = 'FREE' LIMIT 1 FOR UPDATE -- WAITS 
User 1: 
    UPDATE FreeCoreSlots SET status = 'USED' where ID = 1; 
User 2: 
    still waits 
User 1: 
    INSERT INTO user VALUES (... 
    COMMIT; 
USER 2: 
    Gets back Id 2 from the select as the next free Id 
+0

無論如何5秒鐘都是10倍誇張:-)然而,儘管通過設置讀取未提交的隔離並選擇一個(選擇一個空閒行作爲子查詢)進行更新,我仍然得到它的工作,這是比我更高的了。但是你的方法在SQLAlchemy ORM中「做」更簡單。 – aitchnyu

0

將分配推遲到最後可能的時刻。不要試圖在註冊過程中綁定它。

利用狀態列作爲併發策略。應用一個謂詞,確保只有在該行仍然「空閒」時纔會更新該行。使用默認隔離級別時,讀取將是非阻塞的。您的更新將更新1行或無。如果不成功,您可以循環,並對嘗試次數設置限制(以防空閒時隙耗盡)。您還可以預取超過1個免費ID,以節省您嘗試的往返行程。最後,作爲返回新分配槽的id的存儲過程,這會更好。在這種情況下,你在你的存儲過程中採用了類似的策略,在這種策略中,你直接獲取/更新,直到更新成功。在高爭用環境中,如果串行分配不是非常重要,我從前N個自由分支中選擇一個隨機行。

sql = SQLTransaction() 

core_details = sql.get("select * from FreeCoreSlots where status = 'free' limit 1") 
sql.execute("update FreeCoreSlots set status = 'used' where id = {id} and status = 'free'".format(
    id = core_details["id"])) 

//CHECK ROW COUNT HERE (I don't know what language or library you are using. 
//Perhaps sql.execute returns the number of rows affected). 
//REPEAT IF ROW COUNT < 1 

sql.execute("insert into users (name,core) values ({name},{core})".format(
    name = name, 
    id = core_details["id"])) 
sql.commit() 
相關問題