### 2018 年 12 月 29 日 發布
## 悲觀鎖和樂觀鎖
業務邏輯的實現過程中,往往需要保證數據訪問的排他性。如在金融系統的日終結算處理中,我們希望針對某個時間點的數據進行處理,而不希望在結算進行過程中(可能是幾秒種,也可能是幾個小時),數據再發生變化。此時,我們就需要通過一些機制來保證這些數據在某個操作過程中不會被外界修改,這樣的機制,在這里,也就是所謂的 “ 鎖 ” ,即給我們選定的目標數據上鎖,使其無法被其他程序修改。 通常有兩種鎖機制:即通常所說的 “ 悲觀鎖( Pessimistic Locking ) ”和 “ 樂觀鎖( Optimistic Locking ) ” 。
### 悲觀鎖( Pessimistic Locking )
悲觀鎖,正如其名,它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處于鎖定狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。 通常是使用`for update`子句來實現悲觀鎖機制。
ThinkPHP5支持悲觀鎖機制,要啟用悲觀鎖功能,可以通過使用`lock`鎖定方法,例如:
~~~
// 使用悲觀鎖功能
Db::name('user')->lock(true)->find(1);
~~~
就會自動在生成的SQL語句最后加上`FOR UPDATE`或者`FOR UPDATE NOWAIT`(Oracle數據庫)。
`lock`方法還支持傳入字符串,以實現特殊的鎖機制。
```
Db::name('user')->lock('LOCK IN SHARE MODE')->find(1);
```
### 樂觀鎖( Optimistic Locking )
相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。 如一個金融系統,當某個操作員讀取用戶的數據,并在讀出的用戶數據的基礎上進行修改時(如更改用戶帳戶余額),如果采用悲觀鎖機制,也就意味著整個操作過程中(從操作員讀出數據、開始修改直至提交修改結果的全過程,甚至還包括操作員中途去煮咖啡的時間),數據庫記錄始終處于加鎖狀態,可以想見,如果面對幾百上千個并發,這樣的情況將導致怎樣的后果。樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基于數據版本( `Version `)記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基于數據庫表的版本解決方案中,一般是通過為數據庫表增加一個`version`字段來實現。
ThinkPHP`5.1`版本中并沒有內置樂觀鎖功能,因此需要自己實現,本文就來利用`Trait`特性實現樂觀鎖的功能。
## 樂觀鎖的實現
要實現樂觀鎖功能,主要涉及三個地方:
**記錄樂觀鎖**:第一次寫入數據的時候自動記錄`version`字段,當然也可以使用數據庫默認值功能。
**讀取樂觀鎖**:每次讀取數據的時候都要單獨記錄下當前的`version`數據值。
**檢測樂觀鎖**:每次更新數據的時候要重新檢測下最新數據的`version`數據值,如果記錄的版本號和最新的不一致,表示數據需要更新,否則把當前記錄的版本號加1后更新到數據庫。
而ThinkPHP`5.1`的模型`save`方法會統一調用`checkBeforeSave`方法,因此我們可以通過重寫該方法來實現樂觀鎖的檢測樂觀鎖功能。而每次查詢后都會調用模型的`newInstance`方法,因此可以重寫該方法添加讀取樂觀鎖功能。
### 繼承方式實現
我們可以創建一個公共的模型繼承系統的`think\Model`類,當你的模型需要使用樂觀鎖功能的話就單獨繼承。
```
<?php
namespace app\common\model;
use think\Exception;
use think\Model;
class OptimLock extends Model
{
protected $optimLock = 'version';
/**
* 創建新的模型實例
* @access public
* @param array $data 數據
* @param bool $isUpdate 是否為更新
* @param mixed $where 更新條件
* @return Model
*/
public function newInstance($data = [], $isUpdate = false, $where = null)
{
// 緩存樂觀鎖
$this->cacheLockVersion($data);
return (new static($data))->isUpdate($isUpdate, $where);
}
/**
* 寫入之前檢查數據
* @access protected
* @param array $data 數據
* @param array $where 保存條件
* @return bool
*/
protected function checkBeforeSave($data, $where)
{
if (!empty($data)) {
// 數據對象賦值
foreach ($data as $key => $value) {
$this->setAttr($key, $value, $data);
}
if (!empty($where)) {
$this->isUpdate(true, $where);
}
}
// 數據自動完成
$this->autoCompleteData($this->auto);
// 事件回調
if (false === $this->trigger('before_write')) {
return false;
}
if ($this->isExists() && !$this->checkLockVersion()) {
throw new Exception('record has update');
}
return true;
}
/**
* 緩存樂觀鎖
* @access protected
* @param array $data 數據
* @return void
*/
protected function cacheLockVersion($data): void
{
$pk = $this->getPk();
if ($this->optimLock && isset($data[$this->optimLock]) && is_string($pk) && isset($data[$pk])) {
$key = $this->name . '_' . $data[$pk] . '_lock_version';
$_SESSION[$key] = $data[$this->optimLock];
}
}
/**
* 檢查樂觀鎖
* @access protected
* @param array $data 數據
* @return bool
*/
protected function checkLockVersion()
{
// 檢查樂觀鎖
$id = $this->getKey();
if (empty($id)) {
return true;
}
$key = $this->name . '_' . $id . '_lock_version';
if ($this->optimLock && isset($_SESSION[$key])) {
$lockVer = $_SESSION[$key];
$vo = $this->field($this->optimLock)->find($id);
$_SESSION[$key] = $lockVer;
$currVer = $vo[$optimLock];
if (isset($currVer)) {
if ($currVer > 0 && $lockVer != $currVer) {
// 記錄已經更新
return false;
}
// 更新樂觀鎖
$lockVer++;
if ($this->data[$this->optimLock] != $lockVer) {
$this->data[$this->optimLock] = $lockVer;
}
$_SESSION[$key] = $lockVer;
}
}
return true;
}
}
```
對需要使用樂觀鎖的模型,可以使用
```
namespace app\index\model;
use app\common\model\OptimLock;
class User extends OptimLock
{
}
```
### 利用`Trait`特性實現
但由于PHP不支持多繼承,因此并不建議使用模型繼承功能來擴展功能。我們可以利用`Trait`特性來更方便的引入`OptimLock`后開啟樂觀鎖功能。
因為`Trait`機制的問題,我們對上面的代碼進行了一些必要的調整。
```
<?php
namespace app\common\traits;
use think\Exception;
use think\Model;
trait OptimLock
{
protected function getOptimLockField()
{
return property_exists($this, 'optimLock') && isset($this->optimLock) ? $this->optimLock : 'version';
}
/**
* 創建新的模型實例
* @access public
* @param array $data 數據
* @param bool $isUpdate 是否為更新
* @param mixed $where 更新條件
* @return Model
*/
public function newInstance($data = [], $isUpdate = false, $where = null)
{
// 緩存樂觀鎖
$this->cacheLockVersion($data);
return (new static($data))->isUpdate($isUpdate, $where);
}
/**
* 寫入之前檢查數據
* @access protected
* @param array $data 數據
* @param array $where 保存條件
* @return bool
*/
protected function checkBeforeSave($data, $where)
{
if (!empty($data)) {
// 數據對象賦值
foreach ($data as $key => $value) {
$this->setAttr($key, $value, $data);
}
if (!empty($where)) {
$this->isUpdate(true, $where);
}
}
// 數據自動完成
$this->autoCompleteData($this->auto);
// 事件回調
if (false === $this->trigger('before_write')) {
return false;
}
if ($this->isExists()) {
if (!$this->checkLockVersion()) {
throw new Exception('record has update');
}
} else {
$this->recordLockVersion();
}
return true;
}
/**
* 緩存樂觀鎖
* @access protected
* @param array $data 數據
* @return void
*/
protected function cacheLockVersion($data): void
{
$optimLock = $this->getOptimLockField();
$pk = $this->getPk();
if ($optimLock && isset($data[$optimLock]) && is_string($pk) && isset($data[$pk])) {
$key = $this->getName() . '_' . $data[$pk] . '_lock_version';
$_SESSION[$key] = $data[$optimLock];
}
}
/**
* 檢查樂觀鎖
* @access protected
* @param array $data 數據
* @return bool
*/
protected function checkLockVersion()
{
// 檢查樂觀鎖
$id = $this->getKey();
if (empty($id)) {
return true;
}
$key = $this->getName() . '_' . $id . '_lock_version';
$optimLock = $this->getOptimLockField();
if ($optimLock && isset($_SESSION[$key])) {
$lockVer = $_SESSION[$key];
$vo = $this->field($optimLock)->find($id);
$_SESSION[$key] = $lockVer;
$currVer = $vo[$optimLock];
if (isset($currVer)) {
if ($currVer > 0 && $lockVer != $currVer) {
// 記錄已經更新
return false;
}
// 更新樂觀鎖
$lockVer++;
$data = $this->getData();
if ($data[$optimLock] != $lockVer) {
$this->data($optimLock, $lockVer);
}
$_SESSION[$key] = $lockVer;
}
}
return true;
}
}
```
對需要使用樂觀鎖的模型,可以使用
```
namespace app\index\model;
use app\common\traits\OptimLock;
use think\Model;
class User extends Model
{
use OptimLock;
}
```
>[info] 值得注意的是,`5.2`版本目前已經內置了一個`OptimLock`的`Trait`實現。
- 值得升級到5.1的18個理由
- 5.1.7版本新特性
- JSON字段類型在ORM中的使用
- 文件下載響應對象
- 教你使用5.1的數組對象查詢
- 模型三大利器之一:搜索器
- 在ThinkPHP中使用Yaconf
- 掌握命令行的表格輸出
- 5.1.25查詢參數綁定的改進
- ThinkPHP安全規范指引
- 巧用數據集的排序功能實現統計排序
- think-orm ——基于5.1的獨立ORM庫
- think-template——基于ThinkPHP的獨立模板引擎
- ThinkPHP5.1.26版本發布——修正版本,包含安全更新
- ThinkPHP5.0和3.2再發安全更新
- 官宣:ThinkPHP發布首個LTS版本
- 你真的了解Db類和模型的正確使用姿勢么?
- 如何更有效的記錄和管理日志
- 模型三大利器之二:修改器
- ThinkPHP5.1.28版本發布——修正上一版本問題,改進關聯查詢
- 模型三大利器之三:獲取器
- API版本控制的幾種思路
- ThinkPHP5.2第一個Beta版本發布測試
- 讓你少犯錯的數據查詢基本原則
- ThinkPHP發布5.1.29版本——常規更新
- 這15個好習慣讓你更容易升級到5.2
- 如何有效提高ThinkPHP的應用性能
- 讓你提高開發效率的查詢技巧
- 模型關聯查詢不完全指南
- 5.2發布Beta2版本——統一和精簡大量用法
- ThinkPHP發布5.1.30版本——支持微秒時間字段寫入
- ThinkPHP的數據緩存使用
- ThinkPHP5.2安裝及入口文件
- ThinkPHP榮獲2018 年度最受歡迎中國開源開發框架第1名
- 5.1路由使用心得技巧
- ThinkPHP5.*版本發布安全更新
- ThinkPHP項目及代碼規范指北
- 5.2版本的設計規范指導
- ThinkPHP5.1.32版本發布——圣誕快樂
- 利用Trait特性給模型增加樂觀鎖功能
- 5.2數據庫和模型的變化(摘要)
- ThinkPHP模板引擎實現和常見問題
- ThinkPHP5.0.24版本發布——安全更新
- 不忘初心,方得始終——ThinkPHP十三周年報告
- ThinkPHP5+相關資源匯總
- 異步社區ThinkPHP周年慶專享優惠活動
- 5.2路由的調整和改進
- ThinkPHP發布5.1.33版本——包含安全更新
- ThinkPHP擴展開發指南
- ThinkPHP發布5.2Beta3版本
- ThinkPHP發布5.1.34版本——喜迎新年
- ThinkPHP發布5.2RC1版本
- ThinkPHP發布5.1.35版本——常規更新
- 5.2配置類的調整
- 5.2時間查詢的改進和優化
- 5.2RC版本升級不完全指導(僅供學習參考)
- ThinkPHP5.2版本正式變更為6.0版本
- ThinkPHP百度云云虛擬主機專享免費活動
- 事件系統以及查詢事件、模型事件的使用
- ThinkPHP6.0RC2版本發布——架構升級、精簡核心
- ThinkPHP5.1.36LTS版本發布——常規更新
- 新版Session和Cookie設計變化
- ThinkPHP5.1.37版本發布——常規更新
- ThinkPHP6.0RC3版本發布——細節完善,體驗優化
- 6.0中間件使用詳解
- Composer各大廠商鏡像地址
- ThinkPHP6.0發布計劃公告
- 「ThinkPHP開發者周刊」招募志愿者
- ThinkPHP6.0日志變化
- ThinkPHP5.1.38版本發布——常規更新
- ThinkPHP6.0RC4版本發布——ORM獨立,日志多通道支持
- ThinkORM2.0開發指南上線
- ThinkPHP6.0RC5版本發布——多應用模式獨立,中間件機制調整
- ThinkPHP6.0版本發布——程序員節福利
- ThinkPHP5.1.39LTS版本發布——常規更新
- ThinkPHP6.0.1版本發布——圣誕快樂!
- 回顧2019,展望2020!
- ThinkPHPV6.0.2版本發布——2020新春快樂!
- 周年福利系列:Swoole合作優惠
- 億速云成為ThinkPHPV6.0獨家贊助發布商??
- 新冠疫情工具和限免資源專題(保持更新中)
- 周年福利系列:創宇信用認證合作優惠
- 周年福利系列:碼云企業版限時10%優惠
- 周年福利系列:想天短說抵現優惠
- think-swoole直播:從零開始掌握swoole開發
- 周年福利系列:B2C開源電商ShopXO授權8折優惠
- 周年福利系列:LayuiAdmin 永久授權限時優惠
- ThinkPHP資源導航站上線——構建生態 服務未來
- ThinkPHP官方技術支持服務和應用服務市場上線公測
- ThinkPHP市場精選——推廣基本要素
- ThinkPHP市場精選——客服聊天專題
- ThinkPHPV6.0.3版本發布——端午安康
- ThinkPHP開發者扶持計劃
- 6.0.3版本關鍵更新及升級事項
- 「ThinkPHP開發者周刊」改版重啟
- ThinkPHP市場精選——企業建站專題
- ThinkPHP 提供統一API接口服務
- ThinkPHP市場精選——直播電商專題
- ThinkAPI服務SDK發布
- 官方服務市場啟用獨立子域名
- ThinkPHP市場精選——刷臉支付專題
- ThinkAPI推出會員服務計劃
- ThinkPHPV6.0.4版本發布——中秋國慶雙節快樂
- ThinkPHPV5.1.40版本發布——常規更新
- 1024程序員節福利走一波
- ThinkPHP V6.0.5版本發布——兼容Composer2.0
- 知識圖譜應用場景——源論技術沙龍
- ThinkPHP5.*版本改進Composer2.0的兼容
- 官方市場雙十一精選推薦
- 技術人做產品有機會么(文末送課程)
- 本周秒殺——古德云售后獲客營銷系統
- ThinkAPI服務更新——支持接口分組和PHP版本依賴調整
- PHP8新特性盤點
- PHP8新特性系列:構造器屬性提升使用及注意事項
- ThinkPHP2021新年寄語
- ThinkPHP V6.0.6&V5.1.41版本發布——兼容PHP8.0
- PHP如何更優雅地調用API接口
- ThinkPHP V6.0.7發布——修正版本
- ThinkAPI服務更新——IP白名單
- 最新版ThinkORM對于時間字段的調整
- ThinkAPI短信接口正式上線
- ThinkPHP V6.0.8版本發布——多環境變量配置支持
- 頂想云寫作服務開啟第一次公測
- ThinkSSL上線——官方SSL/TLS證書服務
- MDBootstrap國內用戶福利——ThinkPHP官方市場首發
- ThinkPHP V6.0.9版本發布——常規更新
- ThinkORM功能盤點——虛擬模型
- 全面支持主流GIT版本庫——云寫作服務第二次公測
- 云寫作服務私有化部署方案之:版本庫私有化
- 看云雙十一活動
- ThinkPHP V6.0.10LTS發布——兼容PHP8.1
- ThinkPHP V6.0.12發布——命令行兼容8.1
- 頂想云知識管理上線公測——構建企業文檔中心和知識庫
- 頂想云上線——助力生態數字化建設
- 618活動進行中——官方市場迎來一波更新
- 頂想云知識管理正式上線——看云文檔啟動遷移服務
- ThinkPHP V6.0.13發布——常規更新
- 頂想云網站助理服務上線——構建產品支持服務
- ThinkPHP發布6.1.0&6.0.14版本——安全更新
- ThinkPHP新版社區上線試運營
- ThinkAPI上架人臉核身接口——助力網站實名認證
- 辭舊迎新——舊版社區停止注冊及發帖
- ThinkPHP6.1.2版本發布——兼容PHP8.2