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