關於 DB lock 的簡單解釋

有人問了關於 DB 的問題,在於 lock 的部分,這邊大略解釋一下,包含真實的情況就是

A == B ; A.lock vs B.lock ; A & B ~= @item # @item ~= Item.find(params[:id])

一個 lock 的出現在於 SELECT,結束於 UPDATE,換成 Rails 會是 find / where / with_lock,然後到 update / save! 之類的結束,因為一筆資料要 lock 的話必須取得最後的狀況後才能鎖定,而解鎖通常是更新狀態之後

A.lock #(re-select)
   B.lock
A.update(:kind => 1)
   B.lock
   B.update(:kind => 2)
A.save
   B.save

像這樣一定會 deadlock,因為兩個都在搶同一個資源,而 A.lock 的時候, B.lock 會等 A.update 後,才會執行 B.update,anyway lock 通常是 multi thread / process 才會出現,所以一般的code也不會這樣寫才是,所以會是並進的

# A.kind = B.kind = 0
A.with_lock do
  A.update(:kind => 1)     B.with_lock do
end                          B.update(:kind => 2)
                           end

會變成這樣,但是這樣很蠢,因為這個 lock 是有問題的 B 會等 A 沒錯,但是這個 update 還是一樣會做兩次,因為 A 和 B 都是"最新的資料",但是卻沒有檢查"最新資料"的狀況,也就是,也就是 kind = 0 變成 kind = 1 or 2 其中一個,且只能做一次的時候,但這邊卻會做兩次,所必須修改

# A.kind = B.kind = 0
A.with_lock do
  A.update(:kind => 1) if A.kind == 0
end        B.with_lock do
             B.update(:kind => 2) if B.kind == 0
           end

這樣是最漂亮的作法,我這邊推薦類似 @item.with_lock do ... end 的方式來做,這樣可以明確的標示 lock 的起點與終點,就不用詭異的 save! 來做解 lock 的動作,而 lock 內一定要再檢查一次狀態,也就是是否這個狀態允許更新這回事,上面那個順序雖然 A & B 的 code 前後執行不一,但是後面執行的會等前面的結束後再拿回最新的狀態,然後必須檢查後才進行相關動作,因為如上 B 是後面才拿到直,且 kind = 1,不會有"中間狀態"(這就是 lock 的主要目的就是),這樣就可以完成一個完整的 transaction 包裝

而 lock 可以巢狀沒差,所以你可以有一票 with_lock

A.with_lock do
  ...
  B.with_lock do
    ...
  end
  ...
end

但是小心不要交錯就好,類似

A.with_lock do
  ...
  B.with_lock do
    ...
    A.with_lock do
      ...
    end
    ...
  end
  ...
end

這樣會出事的啦…okay大概就這樣子而已,以上

1個讚