《PHP實戰(zhàn):深入理解Yii2.0樂觀鎖與悲觀鎖的原理與使用》要點:
本文介紹了PHP實戰(zhàn):深入理解Yii2.0樂觀鎖與悲觀鎖的原理與使用,希望對您有用。如果有疑問,可以聯(lián)系我們。
相關(guān)主題:YII框架
PHP實例本文介紹了深入理解Yii2.0樂觀鎖與悲觀鎖的原理與使用,分享給大家,具體如下:
PHP實例Web應(yīng)用往往面臨多用戶環(huán)境,這種情況下的并發(fā)寫入控制, 幾乎成為每個開發(fā)人員都必須掌握的一項技能.
PHP實例在并發(fā)環(huán)境下,有可能會出現(xiàn)臟讀(Dirty Read)、不可重復(fù)讀(Unrepeatable Read)、 幻讀(Phantom Read)、更新丟失(Lost update)等情況.具體的表現(xiàn)可以自行搜索.
PHP實例為了應(yīng)對這些問題,主流數(shù)據(jù)庫都提供了鎖機制,并引入了事務(wù)隔離級別的概念. 這里我們都不作解釋了,拿這些關(guān)鍵詞一搜,網(wǎng)上大把大把的.
PHP實例但是,就于具體開發(fā)過程而言,一般分為悲觀鎖和樂觀鎖兩種方式來解決并發(fā)沖突問題.
PHP實例樂觀鎖
PHP實例樂觀鎖(optimistic locking)表現(xiàn)出大膽、務(wù)實的態(tài)度.使用樂觀鎖的前提是, 實際應(yīng)用當(dāng)中,發(fā)生沖突的概率比較低.他的設(shè)計和實現(xiàn)直接而簡潔. 目前Web應(yīng)用中,樂觀鎖的使用占有絕對優(yōu)勢.
PHP實例因此,Yii也為ActiveReocrd提供了樂觀鎖支持.
PHP實例根據(jù)Yii的官方文檔,使用樂觀鎖,總共分4步:
PHP實例從本質(zhì)上來講,樂觀鎖并沒有像悲觀鎖那樣使用數(shù)據(jù)庫的鎖機制. 樂觀鎖通過在表中增加一個計數(shù)字段,來表示當(dāng)前記錄被修改的次數(shù)(版本號).
PHP實例然后在更新、刪除前通過比對版本號來實現(xiàn)樂觀鎖.
PHP實例聲明版本號字段
PHP實例版本號是實現(xiàn)樂觀鎖的根本所在.所以第一步,我們要告訴Yii,哪個字段是版本號字段. 這個由 yii\db\BaseActiveRecord 負(fù)責(zé):
PHP實例
public function optimisticLock()
{
return null;
}
PHP實例這個方法返回 null ,表示不使用樂觀鎖.那么我們的Model中,要對此進(jìn)行重載. 返回一個字符串,表示我們用于標(biāo)識版本號的字段.比如可以這樣:
PHP實例
public function optimisticLock()
{
return 'ver';
}
PHP實例說明當(dāng)前的ActiveRecord中,有一個 ver 字段,可以為樂觀鎖所用. 那么Yii具體是如何借助這個 ver 字段實現(xiàn)樂觀鎖的呢?
PHP實例更新過程
PHP實例具體來講,使用樂觀鎖之后的更新過程,就是這么一個流程:
PHP實例由于ActiveRecord的更新過程最終都需要調(diào)用 yii\db\BaseActiveRecord::updateInteranl()
,理所當(dāng)然地,處理樂觀鎖的代碼, 也就隱藏在這個方法中:
PHP實例
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);
// 如果已經(jīng)啟用了樂觀鎖,但是卻沒有完成更新,或者更新的記錄數(shù)為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;
}
PHP實例從上面的代碼中,我們不難得出:
PHP實例刪除過程
PHP實例與更新過程相比,刪除過程的樂觀鎖,更簡單,更好理解.代碼仍在 yii\db\BaseActiveRecord 中:
PHP實例
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;
}
PHP實例比起更新過程,刪除過程確實要簡單得多.唯一的區(qū)別就是省去了版本號+1的步驟. 都要刪除了,版本號+1有什么意義?
PHP實例樂觀鎖失效
PHP實例樂觀鎖存在失效的情況,屬小概率事件,需要多個條件共同配合才會出現(xiàn).如:
PHP實例樂觀鎖此時的失效,根本原因在于應(yīng)用所使用的主鍵ID管理策略, 正好與樂觀鎖存在極小程度上的不兼容.
PHP實例兩者分開來看,都是沒問題的.組合到一起之后,大致看去好像也沒問題. 但是bug之所以成為bug,坑之所以能夠坑死人,正是由于其隱蔽性.
PHP實例對此,也有一些意見提出來,使用時間戳作為版本號字段,就可以避免這個問題. 但是,時間戳的話,如果精度不夠,如毫秒級別,那么在高并發(fā),或者非常湊巧情況下, 仍有失效的可能.而如果使用高精度時間戳的話,成本又太高.
PHP實例使用時間戳,可靠性并不比使用整型好.問題還是要回到使用嚴(yán)謹(jǐn)?shù)闹麈I成生策略上來.
PHP實例悲觀鎖
PHP實例正如其名字,悲觀鎖(pessimistic locking)體現(xiàn)了一種謹(jǐn)慎的處事態(tài)度.其流程如下:
PHP實例悲觀鎖確實很嚴(yán)謹(jǐn),有效保證了數(shù)據(jù)的一致性,在C/S應(yīng)用上有諸多成熟方案. 但是他的缺點與優(yōu)點一樣的明顯:
PHP實例總體來看,悲觀鎖不大適應(yīng)于Web應(yīng)用,Yii團(tuán)隊也認(rèn)為悲觀鎖的實現(xiàn)過于麻煩, 因此,ActiveRecord也沒有提供悲觀鎖.
PHP實例作為Yii的構(gòu)成基因之一的Ruby on rails,他的ActiveReocrd模型,倒是提供了悲觀鎖, 但是使用起來也很麻煩.
PHP實例悲觀鎖的實現(xiàn)
PHP實例雖然悲觀鎖在Web應(yīng)用上存在諸多不足,實現(xiàn)悲觀鎖也需要解決各種麻煩.但是, 當(dāng)用戶提出他就是要用悲觀鎖時,牙口再不好的碼農(nóng),就是咬碎牙也是要啃下這塊骨頭來.
PHP實例對于一個典型的Web應(yīng)用而言,這里提供個人常用的方法來實現(xiàn)悲觀鎖.
PHP實例首先,在要鎖定的表里,加一個字段如 locked_at ,表示當(dāng)前記錄被鎖定時的時間, 當(dāng)為 0 時,表示該記錄未被鎖定,或者認(rèn)為這是1970年時加的鎖.
PHP實例當(dāng)要修改某個記錄時,先看看當(dāng)前時間與 locked_at 字段相差是否超過預(yù)定的一個時長T,比如 30 min ,1 h 之類的.
PHP實例如果沒超過,說明該記錄有人正在修改,我們暫時不能打開(讀取)他來修改. 否則,說明可以修改,我們先將當(dāng)前時間戳保存到該記錄的 locked_at 字段. 那么之后的時長T內(nèi)如果有人要來改這個記錄,他會由于加鎖失敗而無法讀取, 從而無法修改.
PHP實例我們在完成修改后,即將保存時,要比對現(xiàn)在的 locked_at .只有在 locked_at 一致時,才認(rèn)為剛剛是我們加的鎖,我們才可以保存. 否則,說明在我們加鎖后,又有人加了鎖正在修改, 或者已經(jīng)完成了修改,使得 locked_at 歸 0.
PHP實例這種情況主要是由于我們的修改時長過長,超過了預(yù)定的T.原先的加鎖自動解開, 其他用戶可以在我們加鎖時刻再過T之后,重新加上自己的鎖.換句話說, 此時悲觀鎖退化為樂觀鎖.
PHP實例大致的原理性代碼如下:
PHP實例
// 悲觀鎖AR基類,需要使用悲觀鎖的AR可以由此派生
class PLockAR extends \yii\db\BaseActiveRecord {
// 聲明悲觀鎖使用的標(biāo)記字段,作用類似于 optimisticLock() 方法
public function pesstimisticLock() {
return null;
}
// 定義鎖定的最大時長,超過該時長后,自動解鎖.
public function maxLockTime() {
return 0;
}
// 嘗試加鎖,加鎖成功則返回true
public function lock() {
$lock = $this->pesstimisticLock();
$now = time();
$values = [$lock => $now];
// 以下2句,更新條件為主鍵,且上次鎖定時間距現(xiàn)在超過規(guī)定時長
$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);
// 改為獲取悲觀鎖標(biāo)識字段
$lock = $this->pesstimisticLock();
// 如果 $lock 為 null,那么,不啟用悲觀鎖.
if ($lock !== null) {
// 等下保存時,要把標(biāo)識字段置0
$values[$lock] = 0;
// 這里把原來的標(biāo)識字段值作為更新的另一個條件
$condition[$lock] = $this->$lock;
}
$rows = $this->updateAll($values, $condition);
// 如果已經(jīng)啟用了悲觀鎖,但是卻沒有完成更新,或者更新的記錄數(shù)為0;
// 那就說明之前的加鎖已經(jīng)自動失效了,記錄正在被修改,
// 或者已經(jīng)完成修改,于是拋出異常.
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;
}
}
PHP實例上面的代碼對比樂觀鎖,主要不同點在于:
PHP實例在具體使用方法上,可以參照以下代碼:
PHP實例
// 從PLockAR派生模型類
class Post extends PLockAR {
// 重載定義悲觀鎖標(biāo)識字段,如 locked_at
public function pesstimisticLock() {
return 'locked_at';
}
// 重載定義最大鎖定時長,如1小時
public function maxLockTime() {
return 3600000;
}
}
// 修改前要嘗試加鎖
class SectionController extends Controller {
public function actionUpdate($id)
{
$model = $this->findModel($id);
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
} else {
// 加入一個加鎖的判斷
if (!$model->lock()) {
// 加鎖失敗
// ... ...
}
return $this->render('update', [
'model' => $model,
]);
}
}
}
PHP實例上述方法實現(xiàn)的悲觀鎖,避免了使用數(shù)據(jù)庫自身的鎖機制,契合Web應(yīng)用的特點, 具有一定的適用性,但是也存在一定的缺陷:
PHP實例以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持維易PHP.
轉(zhuǎn)載請注明本頁網(wǎng)址:
http://www.snjht.com/jiaocheng/427.html