2012-12-20 148 views
2

我盡力去完成看似簡單,競爭條件

Db的類型:MyISAM數據
表結構:card_id的,狀態
查詢:從表中選擇一個未使用的card_id的,並設置行作爲「用過的」。

是它的競爭條件,當更新狀態前兩個查詢在同一時間運行,並且,同樣是card_id的兩次取?

我做了一些搜索了。看起來鎖表是一個解決方案,但它對我來說是矯枉過正的,需要鎖特權。

任何想法?

謝謝!

+0

爲了避免正常db用戶的'LOCK PRIVILEGE',你可以使用'STORED PROCEDURE'來執行此操作,因爲你可以切換用戶上下文執行 –

+0

我更新了我的答案;我提供了一個使用用戶變量來返回已更新行的'cardid'值的示例。 – spencer7593

回答

2

這真的取決於你正在運行的語句。

對於MyISAM表中普通的舊的UPDATE語句,MySQL將在整個表上獲得一個鎖,所以兩個會話之間沒有「競爭」條件。一個會話將等待,直到鎖定被釋放,然後繼續它自己的更新(或將等待指定的時間,並以「超時」中止。)

但是,如果你問的是兩個會話既針對表運行SELECT,又檢索要更新的行的標識符,並且兩個會話檢索相同的行標識符,然後兩個會話試圖更新同一行,那麼是的,這是一個明確的可能性,真的不得不考慮。

如果條件不解決,那麼它基本上會是「最後更新勝」的問題,在第二屆會議將(可能)覆蓋由先前的更新所做的更改。

如果這對於您的應用程序來說是不成熟的情況,那麼需要使用不同的設計來解決這個問題,或者使用一些機制來防止第二次更新覆蓋第一次更新所應用的更新。

正如您所提到的,一種方法是通過首先獲得對錶的排他鎖(使用LOCK TABLES語句),然後運行SELECT獲取標識符,然後運行UPDATE以更新確定行,然後終於解除鎖定(使用UNLOCK TABLES語句。)

這對於一些低量,低併發應用的一個可行的辦法。但它確實有一些明顯的缺點。主要關心的是併發性的降低,這是由於在單個資源上獲得的排它鎖,這可能導致性能瓶頸。

另一種方法是所謂的「樂觀鎖定」的戰略。 (與之前描述的可能被描述爲「悲觀鎖定」的方法相反)。

對於「樂觀鎖定」策略,將額外的「計數器」列添加到表中。每當將更新應用於表中的一行時,該行的計數器就會加1。

要使用此「計數器」列,當查詢檢索將要(或可能)稍後更新的行時,該查詢還會檢索計數器列的值。

當UPDATE嘗試,語句也是「計數器」列的當前值的行與所述計數器列的先前檢索值進行比較。 (我們只是有一個謂語(如UPDATE語句的WHERE子句)的,例如,

UPDATE mytable 
    SET counter = counter + 1 
    , col = :some_new_value  
WHERE id = :previously_fetched_row_identifier 
    AND counter = :previously_fetched_row_counter 

如果其他會話已應用的更新,我們正試圖更新(時間之間的某個行我們會議檢索行和之前我們會嘗試做更新),然後在該行的「計數器」列中的值將被改變。

謂語我們的UPDATE語句檢查的是,如果「計數器」已經改變,這會導致我們的更新不被應用,然後我們可以檢測到這種情況(即受影響的行數將是0而不是1),我們的會話可以採取一些適當的行動。 !有些其他呃會話更新我們打算更新行!「)

有關於如何實現的一些好寫起坐‘樂觀鎖定’策略。

一些ORM框架(例如Hibernate,JPA)爲這種類型的鎖定策略提供支持。


不幸的是,MySQL不會在UPDATE語句提供了一個returning子句的支持,如:

UPDATE ... 
    SET status = 'used' 
WHERE status = 'unused' 
    AND ROWNUM = 1 
RETURNING card_id INTO ... 

其他RDBMS(如Oracle)的辦提供這種功能。利用UPDATE語句的這一特性,我們可以簡單地運行UPDATE語句來查找1)找到一行status = 'unused',2)更改status = 'used'的值,3)返回該行的card_id(或任何我們想要的列)我們剛剛更新。

,圍繞其運行SELECT,然後運行一個單獨的更新,與其他一些會議上更新我們的選擇和我們的UPDATE之間的行的潛在的問題得到。

RETURNING子句不能在MySQL的支持。而且我還沒有找到任何可靠的方式來在MySQL中模擬這種類型的功能。


這對於工作,你

我不完全知道爲什麼我放棄了以前使用的用戶變量這種方法(我在上面,我曾與此玩耍了提及。我想也許我需要更一般的東西,它會更新多行並返回一組id值;或者,可能有些東西不能保證用戶變量的行爲(然後,我只是仔細地引用用戶變量SELECT語句;我沒有在DML中使用用戶變量;這可能是因爲我沒有保證它們的行爲。)

既然你有興趣只有一個排,三所陳述這個序列可以爲你工作:

SELECT @id := NULL ; 

UPDATE mytable 
    SET card_id = (@id := card_id) 
    , status = 'used' 
WHERE status = 'unused' 
LIMIT 1 ; 

SELECT ROW_COUNT(), @id AS updated_card_id ; 

重要的是,這三個語句在同一個數據庫會話中運行(即保留數據庫會話;不要放棄它,並得到一個新的。)

首先,我們初始化用戶變量(@id)這是我們不會從表中真正的價值card_id的混淆值。 (A SET @id := NULL聲明將工作爲好,沒有返回結果,如SELECT語句一樣。)

接下來,我們運行UPDATE聲明:1)找到一排,其中status = 'unused'; 2)改變status列的值到'used',以及3)將@id用戶變量的值設置爲我們更改的行的card_id值。 (我們希望card_id列是整數類型,而不是字符,以避免任何可能的字符集轉換問題。)

接下來,我們運行一個查詢獲取由前一個UPDATE語句更改的行數,使用ROW_COUNT()函數(我們將需要驗證,這是1上的客戶端),並檢索@id用戶變量,這將是從該改變該行的值card_id的的值。

+0

感謝您的詳細回覆! –

+0

很難在這裏添加長評論,所以我必須回答我的問題。請檢查我的答案併發布您的想法。謝謝! –

1

後,我張貼此問題,我想到了一個解決方案,它是完全一樣的,你在最後提到的一個。我使用update語句,它是「update TABLE set status ='used'where status ='unused'limit 1」,它返回TABLE的主Id,然後我可以使用這個主ID來獲取cart_id。就像你說的那樣,同時發生兩個更新語句,「MySQL將獲得整個表的鎖定,所以兩個會話之間不存在」競爭「狀況,所以這應該解決我的問題。但我不確定你爲什麼這麼說,「MySQL不提供對樣式聲明的支持」。

+0

「UPDATE」語句的返回值是該語句影響的行數。在UPDATE語句後面對'mysql_info'(C API)函數的調用會給出一個字符串,其中包含匹配的行數,更改的行數以及警告的數量。如果你有一些機制讓MySQL返回受影響的行的主鍵值,我會非常感興趣的看到這一點,因爲我有幾種情況,這將是非常有用的。 – spencer7593

+0

我重寫了一下我的答案。當我說「MySQL不提供支持」時,我特別提到了UPDATE語句的RETURNING子句,我們在Oracle等人中支持該語句。 'UPDATE t SET c = 1 WHERE c = 0 LIMIT 1'形式的聲明完全符合你的要求,除了你沒有任何可靠的方法來確定哪一行是更新的。 (我用一些用戶變量和timestamp列來演示一些東西,但我還沒有找到一種可靠,簡單的識別行的方法。 – spencer7593