本文介紹了深入理解Yii2.0樂觀鎖與悲觀鎖的原理與使用,分享給大家,具體如下:
Web應用往往面臨多用戶環境,這種情況下的并發寫入控制, 幾乎成為每個開發人員都必須掌握的一項技能。
在并發環境下,有可能會出現臟讀(Dirty Read)、不可重復讀(Unrepeatable Read)、 幻讀(Phantom Read)、更新丟失(Lost update)等情況。具體的表現可以自行搜索。
為了應對這些問題,主流數據庫都提供了鎖機制,并引入了事務隔離級別的概念。 這里我們都不作解釋了,拿這些關鍵詞一搜,網上大把大把的。
但是,就于具體開發過程而言,一般分為悲觀鎖和樂觀鎖兩種方式來解決并發沖突問題。
樂觀鎖
樂觀鎖(optimistic locking)表現出大膽、務實的態度。使用樂觀鎖的前提是, 實際應用當中,發生沖突的概率比較低。他的設計和實現直接而簡潔。 目前Web應用中,樂觀鎖的使用占有絕對優勢。
因此,Yii也為ActiveReocrd提供了樂觀鎖支持。
根據Yii的官方文檔,使用樂觀鎖,總共分4步:
- 為需要加鎖的表增加一個字段,用于表示版本號。 當然相應的Model也要為該字段的加入,作出適當調整。比如, rules() 中要加入該字段。
- 重載 yii\db\ActiveRecord::optimisticLock() 方法,返回上一步中的字段名。
- 在記錄的修改頁面表單中,加入一個 <input type="hidden"> 用于暫存讀取時的記錄的版本號。
- 在保存代碼的地方,使用 try ... catch 看看是否能捕獲一個 yii\db\StaleObjectException 異常。如果是,說明在本次修改這個記錄的過程中, 該記錄已經被修改過了。簡單應對的話,可以作出相應提示。智能點的話, 可以合并不沖突的修改,或者顯示一個diff頁面。
從本質上來講,樂觀鎖并沒有像悲觀鎖那樣使用數據庫的鎖機制。 樂觀鎖通過在表中增加一個計數字段,來表示當前記錄被修改的次數(版本號)。
然后在更新、刪除前通過比對版本號來實現樂觀鎖。
聲明版本號字段
版本號是實現樂觀鎖的根本所在。所以第一步,我們要告訴Yii,哪個字段是版本號字段。 這個由 yii\db\BaseActiveRecord 負責:
public function optimisticLock() { return null; }
這個方法返回 null ,表示不使用樂觀鎖。那么我們的Model中,要對此進行重載。 返回一個字符串,表示我們用于標識版本號的字段。比如可以這樣:
public function optimisticLock() { return 'ver'; }
說明當前的ActiveRecord中,有一個 ver 字段,可以為樂觀鎖所用。 那么Yii具體是如何借助這個 ver 字段實現樂觀鎖的呢?
更新過程
具體來講,使用樂觀鎖之后的更新過程,就是這么一個流程:
- 讀取要更新的記錄。
- 對記錄按照用戶的意愿進行修改。當然,這個時候不會修改 ver 字段。 這個字段對用戶是沒意義的。
- 在保存記錄前,再次讀取這個記錄的 ver 字段,與之前讀取的值進行比對。
- 如果 ver 不同,說明在用戶修改過程中,這個記錄被別人改動過了。那么, 我們要給出提示。
- 如果 ver 相同,說明這個記錄未被修改過。那么,對 ver +1, 并保存這個記錄。這樣子就完成了記錄的更新。同時,該記錄的版本號也加了1。
由于ActiveRecord的更新過程最終都需要調用 yii\db\BaseActiveRecord::updateInteranl()
,理所當然地,處理樂觀鎖的代碼, 也就隱藏在這個方法中:
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; }