原文:Yii2.0的樂觀鎖與悲觀鎖php
Web應用每每面臨多用戶環境,這種狀況下的併發寫入控制, 幾乎成爲每一個開發人員都必須掌握的一項技能。html
在併發環境下,有可能會出現髒讀(Dirty Read)、不可重複讀(Unrepeatable Read)、 幻讀(Phantom Read)、更新丟失(Lost update)等狀況。具體的表現能夠自行搜索。數據庫
爲了應對這些問題,主流數據庫都提供了鎖機制,並引入了事務隔離級別的概念。 這裏咱們都不做解釋了,拿這些關鍵詞一搜,網上大把大把的。併發
可是,就於具體開發過程而言,通常分爲悲觀鎖和樂觀鎖兩種方式來解決併發衝突問題。app
樂觀鎖(optimistic locking)表現出大膽、務實的態度。使用樂觀鎖的前提是, 實際應用當中,發生衝突的機率比較低。他的設計和實現直接而簡潔。 目前Web應用中,樂觀鎖的使用佔有絕對優點。yii
所以,Yii也爲ActiveReocrd提供了樂觀鎖支持。高併發
根據Yii的官方文檔,使用樂觀鎖,總共分4步:post
從本質上來說,樂觀鎖並無像悲觀鎖那樣使用數據庫的鎖機制。 樂觀鎖經過在表中增長一個計數字段,來表示當前記錄被修改的次數(版本號)。性能
而後在更新、刪除前經過比對版本號來實現樂觀鎖。this
版本號是實現樂觀鎖的根本所在。因此第一步,咱們要告訴Yii,哪一個字段是版本號字段。 這個由yii\db\BaseActiveRecord 負責:
public function optimisticLock() { return null; }
這個方法返回 null ,表示不使用樂觀鎖。那麼咱們的Model中,要對此進行重載。 返回一個字符串,表示咱們用於標識版本號的字段。好比能夠這樣:
public function optimisticLock() { return 'ver'; }
說明當前的ActiveRecord中,有一個 ver 字段,能夠爲樂觀鎖所用。 那麼Yii具體是如何藉助這個 ver 字段實現樂觀鎖的呢?
具體來說,使用樂觀鎖以後的更新過程,就是這麼一個流程:
因爲ActiveRecord的更新過程最終都須要調用 yii\db\BaseActiveRecord::updateInteranl() ,理所固然地,處理樂觀鎖的代碼, 也就隱藏在這個方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
protected function updateInternal($attributes = null) { if (!$this->beforeSave(false)) { return false; } // 獲取等下要更新的字段及新的字段值 $values = $this->getDirtyAttributes($attributes); if (empty($values)) { $this->afterSave(false, $values); return 0; } // 把原來ActiveRecord的主鍵做爲等下更新記錄的條件, // 也就是說,等下更新的,最多隻有1個記錄。 $condition = $this->getOldPrimaryKey(true); // 獲取版本號字段的字段名,好比 ver $lock = $this->optimisticLock(); // 若是 optimisticLock() 返回的是 null,那麼,不啓用樂觀鎖。 if ($lock !== null) { // 這裏的 $this->$lock ,就是 $this->ver 的意思; // 這裏把 ver+1 做爲要更新的字段之一。 $values[$lock] = $this->$lock + 1; // 這裏把舊的版本號做爲更新的另外一個條件 $condition[$lock] = $this->$lock; } $rows = $this->updateAll($values, $condition); // 若是已經啓用了樂觀鎖,可是卻沒有完成更新,或者更新的記錄數爲0; // 那就說明是因爲 ver 不匹配,記錄被修改過了,因而拋出異常。 if ($lock !== null && !$rows) { throw new StaleObjectException('The object being updated is outdated.'); } $changedAttributes = []; foreach ($values as $name => $value) { $changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; $this->_oldAttributes[$name] = $value; } $this->afterSave(false, $changedAttributes); return $rows; }
|
從上面的代碼中,咱們不可貴出:
與更新過程相比,刪除過程的樂觀鎖,更簡單,更好理解。代碼仍在 yii\db\BaseActiveRecord 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public function delete() { $result = false; if ($this->beforeDelete()) { // 刪除的SQL語句中,WHERE部分是主鍵 $condition = $this->getOldPrimaryKey(true); // 獲取版本號字段的字段名,好比 ver $lock = $this->optimisticLock(); // 若是啓用樂觀鎖,那麼WHERE部分再加一個條件,版本號 if ($lock !== null) { $condition[$lock] = $this->$lock; } $result = $this->deleteAll($condition); if ($lock !== null && !$result) { throw new StaleObjectException('The object being deleted is outdated.'); } $this->_oldAttributes = null; $this->afterDelete(); } return $result; }
|
比起更新過程,刪除過程確實要簡單得多。惟一的區別就是省去了版本號+1的步驟。 都要刪除了,版本號+1有什麼意義?
樂觀鎖存在失效的狀況,屬小几率事件,須要多個條件共同配合纔會出現。如:
樂觀鎖此時的失效,根本緣由在於應用所使用的主鍵ID管理策略, 正好與樂觀鎖存在極小程度上的不兼容。
二者分開來看,都是沒問題的。組合到一塊兒以後,大體看去好像也沒問題。 可是bug之因此成爲bug,坑之因此可以坑死人,正是因爲其隱蔽性。
對此,也有一些意見提出來,使用時間戳做爲版本號字段,就能夠避免這個問題。 可是,時間戳的話,若是精度不夠,如毫秒級別,那麼在高併發,或者很是湊巧狀況下, 仍有失效的可能。而若是使用高精度時間戳的話,成本又過高。
使用時間戳,可靠性並不比使用整型好。問題仍是要回到使用嚴謹的主鍵成生策略上來。
正如其名字,悲觀鎖(pessimistic locking)體現了一種謹慎的處事態度。其流程以下:
悲觀鎖確實很嚴謹,有效保證了數據的一致性,在C/S應用上有諸多成熟方案。 可是他的缺點與優勢同樣的明顯:
整體來看,悲觀鎖不大適應於Web應用,Yii團隊也認爲悲觀鎖的實現過於麻煩, 所以,ActiveRecord也沒有提供悲觀鎖。
做爲Yii的構成基因之一的Ruby on rails,他的ActiveReocrd模型,卻是提供了悲觀鎖, 可是使用起來也很麻煩。
雖然悲觀鎖在Web應用上存在諸多不足,實現悲觀鎖也須要解決各類麻煩。可是, 當用戶提出他就是要用悲觀鎖時,牙口再很差的碼農,就是咬碎牙也是要啃下這塊骨頭來。
對於一個典型的Web應用而言,這裏提供我的經常使用的方法來實現悲觀鎖。
首先,在要鎖定的表裏,加一個字段如 locked_at ,表示當前記錄被鎖定時的時間, 當爲 0 時,表示該記錄未被鎖定,或者認爲這是1970年時加的鎖。
當要修改某個記錄時,先看看當前時間與 locked_at 字段相差是否超過預約的一個時長T,好比 30 min ,1 h 之類的。
若是沒超過,說明該記錄有人正在修改,咱們暫時不能打開(讀取)他來修改。 不然,說明能夠修改,咱們先將當前時間戳保存到該記錄的 locked_at 字段。 那麼以後的時長T內若是有人要來改這個記錄,他會因爲加鎖失敗而沒法讀取, 從而沒法修改。
咱們在完成修改後,即將保存時,要比對如今的 locked_at 。只有在 locked_at 一致時,才認爲剛剛是咱們加的鎖,咱們才能夠保存。 不然,說明在咱們加鎖後,又有人加了鎖正在修改, 或者已經完成了修改,使得 locked_at 歸 0。
這種狀況主要是因爲咱們的修改時長過長,超過了預約的T。原先的加鎖自動解開, 其餘用戶能夠在咱們加鎖時刻再過T以後,從新加上本身的鎖。換句話說, 此時悲觀鎖退化爲樂觀鎖。
大體的原理性代碼以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
// 悲觀鎖AR基類,須要使用悲觀鎖的AR能夠由此派生
class PLockAR extends \yii\db\BaseActiveRecord { // 聲明悲觀鎖使用的標記字段,做用相似於 optimisticLock() 方法 public function pesstimisticLock() { return null; } // 定義鎖定的最大時長,超過該時長後,自動解鎖。 public function maxLockTime() { return 0; } // 嘗試加鎖,加鎖成功則返回true public function lock() { $lock = $this->pesstimisticLock(); $now = time(); $values = [$lock => $now]; // 如下2句,更新條件爲主鍵,且上次鎖定時間距如今超過規定時長 $condition = $this->getOldPrimaryKey(true); $condition[] = ['<', $lock, $now - $this->maxLockTime()]; $rows = $this->updateAll($values, $condition); // 加鎖失敗,返回 false if (! $rows) { return false; } return true; } // 重載updateInternal() protected function updateInternal($attributes = null) { // 這些與原來代碼同樣 if (!$this->beforeSave(false)) { return false; } $values = $this->getDirtyAttributes($attributes); if (empty($values)) { $this->afterSave(false, $values); return 0; } $condition = $this->getOldPrimaryKey(true); // 改成獲取悲觀鎖標識字段 $lock = $this->pesstimisticLock(); // 若是 $lock 爲 null,那麼,不啓用悲觀鎖。 if ($lock !== null) { // 等下保存時,要把標識字段置0 $values[$lock] = 0; // 這裏把原來的標識字段值做爲更新的另外一個條件 $condition[$lock] = $this->$lock; } $rows = $this->updateAll($values, $condition); // 若是已經啓用了悲觀鎖,可是卻沒有完成更新,或者更新的記錄數爲0; // 那就說明以前的加鎖已經自動失效了,記錄正在被修改, // 或者已經完成修改,因而拋出異常。 if ($lock !== null && !$rows) { throw new StaleObjectException('The object being updated is outdated.'); } $changedAttributes = []; foreach ($values as $name => $value) { $changedAttributes[$name] = isset($this-> |