# 什么是鎖?
如果從日常生活中理解什么是鎖,很好理解,每個人家門上都有鎖,用來防止他人竊取自家財產。但是在計算機中,鎖的概念稍有不同,在計算機中只有涉及到資源競爭的時候,才會用到鎖。
比如在單線程中,不需要用到鎖,資源都是順序化被持有,不存在競爭。但是在多線程中,同時會有多個請求需要同一個資源,這個時候,就需要進行加鎖操作,一個線程獲取到鎖之后,其他的線程只有等待資源被釋放才能接著執行。
# 鎖的作用
鎖本質是為了保證串行,比如在購買訂單的時候,同時涌入大量的請求,如何保證商品不多賣以及不少賣,保證數據的準確性,這個時候就要用鎖來控制并發,讓本來并行執行的問題轉換為串行執行。
# 鎖的分類
說到鎖的分類,在各種文章以及研究中都提供了不同的分類,比較繁雜,但是如果從思想上來說,總體來說分為兩類,一類是悲觀鎖,一類是樂觀鎖。
# 樂觀鎖
樂觀鎖是相對于悲觀鎖而言,樂觀鎖機制采用了更加寬松的加鎖機制。樂觀鎖字如其名,就是持有比較樂觀的態度。就是假設認為數據一般情況下不會造成沖突,所以在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測,如果發現沖突了,則給用戶返回錯誤信息,讓用戶決定如何去做。
樂觀鎖的實現也比較簡單,就是使用數據版本version記錄機制實現。接下來,我們利用mysql數據庫來實現一下樂觀鎖,但是這里要特別注意,第一點,mysql本身并沒有提供實現樂觀鎖,而且也沒有樂觀鎖這個概念,mysql的鎖都是悲觀鎖,那么我們就懂了,樂觀鎖是一種思想,使用其他的所有方式都可以實現,只要實現了這個思想的,都叫樂觀鎖。比如可以使用文件,使用redis都可以實現。
我們首先建立一張表叫good\_num貨物表。
創建表的語句如下所示
~~~
CREATE TABLE `good_nums` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
`nums` int(11) DEFAULT NULL,
`version` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
~~~
| 字段 | 描述 |
| --- | --- |
| id | 自增ID |
| name | 名字 |
| nums | 數量 |
| version | 版本 |
接下來我們給表增加一條記錄。
~~~
INSERT INTO good_amount (name,nums,version) VALUES('張三',100,1)
~~~
目前記錄如下所示:
| id | name | nums | version |
| --- | --- | --- | --- |
| 1 | 張三 | 100 | 1 |
OK,基本情況我們已經構建完成。
那么現在我們假定有這么一個需求,用戶每買一筆,就要把數量減少1個。理想情況下,如果用戶一個一個來去購買的話,我們的數量會按照情況去一個一個減少。但是這僅僅是理想情況,實際情況是,用戶會同時涌入來購買,在這種情況下,數量的減少根本無法保障。
我們看一下在未使用鎖的情況下,是如何實現程序的,這也是初級程序員最常寫的代碼如下所示:
~~~
<?php
//連接數據庫
$conn = new mysqli_connect(xxxxx);
//查詢當前的數量
$sql = "SELECT * from good_nums where id =1";
$source = $conn->query($sql);
$row = $source->fetch_assoc();
if($row['nums'] <= 0){
echo "很遺憾,商品被搶光了";
exit;
}
//將當前的數量減少1
$nums = $row['nums'] - 1;
$sql1 = "update good_nums set nums=".$nums." where id=1";
$source1 = $conn->query($sql1);
~~~
通過上面的代碼可以很明顯的理解,當大量的請求同時涌來的時候,程序在同一時間可能同時讀到當前數量為100,那么每個用戶可能都搶到了物品,但是數據庫記錄數到最后居然是99,然而此時可能已經賣了1千個了。此時就發生了超賣的問題。
那么如果此時使用樂觀鎖,就很容解決這個問題了。代碼如下所示:
~~~
// 連接數據庫
$conn = new mysqli(xxxx);
//查詢當前的數量
$sql = "SELECT * from good_nums where id =1";
$source = $conn->query($sql);
$row = $source->fetch_assoc();
if($row['nums'] <= 0){
echo "很遺憾,商品被搶光了";
exit;
}
//給當前的數量減少1
$nums = $row['nums'] - 1;
//當前的版本
$version = $row['version'];
//更新數據庫的數量
$sql1 = "update good_nums set nums=".$nums.",version=version+1 where id=1 and version=".$version;
$source1 = $conn->query($sql1);
if(!$source1){
echo "您未搶到本商品,請繼續努力";
}
$conn->close();
~~~
可以對比現在的程序,比之前的程序多增加了一個version的條件控制,對的,這就是樂觀鎖的精髓所在。
我們現在來模擬一下用戶的請求。
當第一個用戶涌進來,他拿到的貨物數量為100,此時拿到的version為1。
當第二個用戶涌進來,他此時拿到的貨物數量也為100,此時的version為1。
還有第三個,第四個,都是同樣的情況。
此時,第一個用戶的請求去更新數據庫,他更新的條件,是version必須等于剛剛拿到1,此時數據庫的version還未更新,于是他將這條記錄更新成功,nums數量成功減少為99,并且于此同時,將version更新為2。
那么第二個用戶也去執行這個條件,數據庫此時去檢查發現version居然不是1了,于是這個條件不成立,此時這條sql就會執行失敗,然后告訴用戶你沒有搶到。
一直第三個,第四個用戶都是如此。
只有第n個用戶可能拿到新的version,并且可能成功更新。
上面就是樂觀鎖的完整實現,當前了,上面我們寫的程序還有問題,下面是一個正確的程序。
~~~
// 連接數據庫
$conn = new mysqli(xxxx);
//查詢當前的數量
$sql = "SELECT * from good_nums where id =1";
$source = $conn->query($sql);
$row = $source->fetch_assoc();
if($row['nums'] <= 0){
echo "很遺憾,商品被搶光了";
exit;
}
//當前的版本
$version = $row['version'];
//更新數據庫的數量
$sql1 = "update good_nums set nums=nums-1,version=version+1 where id=1 and version=".$version;
$source1 = $conn->query($sql1);
if(!$source1){
echo "您未搶到本商品,請繼續努力";
}
$conn->close();
~~~
仔細對比就會發現,之前我們是用程序去減少的數量1,但是現在的代碼是讓數據庫去自動減少1。這個也是比較關鍵的一點,在高并發的情況下,請記住一定要如此寫,不然可能會發生不可想像的錯誤。
在很多時候,如果面試官繼續深究,他就會問你,如果大量的用戶同時涌入,上面的程序只能保證少數人能拿到商品啊,可能100個用戶同時涌入,到最后,只賣了10個,但是公司又想這100都賣掉,這個時候咋辦?
這個時候我們就引入了自旋鎖的概念。這個概念如果第一次聽說,就會有點蒙,啥是自旋,怎么自旋,好,帶著這個問題,我們回到剛剛100個人來搶,可能只賣了10個問題。
我們想,既然用戶會發生更新失敗的問題,我們為啥不如讓用戶等待一下,重新獲取一遍新的值,然后讓用戶搶到呢?對的,這就是自旋鎖了。
程序如下:
~~~
// 連接數據庫
$conn = new mysqli(xxxx);
$tips = true;
while($tips){
//查詢當前的數量
$sql = "SELECT * from good_nums where id =1";
$source = $conn->query($sql);
$row = $source->fetch_assoc();
if($row['nums'] <= 0){
echo "很遺憾,商品被搶光了";
$tips = false;
exit;
}
//當前的版本
$version = $row['version'];
//更新數據庫的數量
$sql1 = "update good_nums set nums=nums-1,version=version+1 where id=1 and version=".$version;
$source1 = $conn->query($sql1);
//當數量更新失敗了
if($source1 == true){
$tips = fasle;
}
}
echo "恭喜您搶到了商品";
$conn->close();
~~~
我們看到,首先我們會給一個標識位為true,就是只有不滿足條件的時候才退出,也就是只有用戶搶到了,才結束掉本次請求。如果沒有搶到,程序會不斷的請求,讓當前的用戶去搶,這就是自旋鎖,實際上,就是用了一個循環,不端的去請求,當然了,也可以用遞歸,但是本質都是相同的,都是循環。
但是上面的程序有個問題,如果用戶一直沒有搶到,程序就會一直執行,如果請求數量巨大,就會發生大量的timeout,我們能不能像個辦法,提前結束掉循環呢?程序如下:
~~~
// 連接數據庫
$conn = new mysqli(xxxx);
$tips = true;
$count = 5;
while($tips){
//查詢當前的數量
$sql = "SELECT * from good_nums where id =1";
$source = $conn->query($sql);
$row = $source->fetch_assoc();
if($row['nums'] <= 0){
echo "很遺憾,商品被搶光了";
$tips = false;
exit;
}
//當前的版本
$version = $row['version'];
//更新數據庫的數量
$sql1 = "update good_nums set nums=nums-1,version=version+1 where id=1 and version=".$version;
$source1 = $conn->query($sql1);
//當數量更新失敗了
if($source1 == true){
$tips = fasle;
}
$count--;
if($count <= 0){
$tips = false;
}
}
echo "恭喜您搶到了商品";
$conn->close();
~~~
上面的程序,我們給程序增加了一個count值,當程序執行超過了限定,我們就會釋放掉本次循環,需要說明的是,這不是自旋鎖的概念,只是為了優化請求,才這么寫的,為的是保證程序不超時。
自旋鎖如果按照簡拼來說,通常被叫做CAS。面試的時候,如果被問題到CAS是什么,一定要知道,指代的是自旋鎖。
但是上面的自旋鎖還是有缺點的,就是在未加count的控制的時候,程序會不斷地循環,會給CPU造成多余的計算能力,為了解決這個問題,對于自旋鎖又提出了其他的實現方法,如果有興趣可以自行百度。一般來說,自旋鎖的概念多在Java
面試中提及,因為Java本身支持鎖操作,也支持多線程,可以利用多線程對自旋鎖進行優化,但是本質都是一樣的,即循環搶占鎖。
# 悲觀鎖
悲觀鎖,顧名思義,就是很悲觀,在每次操作的時候,都認為別人已經進行了修改,所以,每次去拿數據的時候都會先進行加鎖操作,防止其他人搶占資源。
接下來,將使用給文件加鎖的形式實現悲觀鎖。代碼如下:
~~~
<?php
$file = "/home/work/abc.txt";
//給文件加鎖
if(flock($file,LOCK_EX|LOCK_NB)){
//這里表示搶占到了鎖,可以執行業務邏輯了
//todo .....
//執行完成之后,記得要釋放掉鎖
flock($file, LOCK_UN);
}else{
echo "很不好意思,您沒有搶到鎖";
}
~~~
通過上面的代碼,我們就很容易理解悲觀鎖,當請求過來的時候,我們先將文件鎖定,搶占到這個資源,如果于此同時再有其他到請求過來,他們沒有搶占到鎖,其他的任務都會失敗。只有等待搶占鎖的任務成功釋放掉鎖之后,其他的任務才可以繼續搶占鎖從而繼續任務。
當然了,我這里寫的代碼是直接告訴用戶
# 樂觀鎖和悲觀鎖的對比
通過上面樂觀鎖和悲觀鎖的例子,我們就很容易理解了。
樂觀鎖保持樂觀的態度,不會一進來就認為別人動了自己的資源,只有更新的時候,才會檢查一下,如果此時發現被別人更改過了,那么就直接返回失敗,樂觀鎖適用于讀多寫少的場景,我們想,如果一個任務需要大量的更新,使用了樂觀鎖,那么大部分任務不都將失敗了嗎?所以要考慮好場景再使用。
悲觀鎖是保持悲觀的態度,一進來就先將資源占有起來,只有自己的任務全部完成之后,才釋放資源。
當然了,最后一點,最重要的是理解悲觀鎖和樂觀鎖是一種思想,也就是不局限于任何形式的實現。
原文:http://www.hmoore.net/missyou/interview/2234864
- 后端
- PHP
- php接收base64格式的圖片
- php 下載文件
- 位,字節,字符的區別
- 求模技巧
- php curl
- php 瀏覽器禁用cookie后需要使用session 就可以用url傳遞session_id
- 有用小方法
- phpDoc
- php 文件鎖來解決高并發
- php小知識
- PHP根據身份證號碼,獲取性別、獲取生日、計算年齡等多個信息
- php 獲取今天,明天、本周、本周末、本月的起始時間戳和結束時間戳的方法
- php 無限級分類
- xdebug設置
- curl
- 獲取現在距離當天結束的還有多少秒
- win10安裝php8版本報錯(Fix PHP Warning: vcruntime140.dll 14.0 is not compatible with this PHP build.)
- 有趣代碼注釋
- php array_diff用法
- parse_str 處理http的query參數
- PHP文件上傳限制
- php操作html
- php trim 函數的使用
- thinkphp5
- 定時任務不能連接數據庫
- 寶塔設置計劃任務
- 控制方法 return $data ,不能直接返回json
- tp5.1命令行
- tp3.2.3 報internal server error
- 悟空crm
- web-msg-sender的使用
- 杉德支付
- laravel
- laravel 遷移文件的使用
- laravel的安裝
- laravel 單元測試
- laravel seeder的使用
- 模型相關
- restful理解
- laravel 的表單驗證
- laravel 隊列的使用
- laravel響應宏應用macro
- laravel 判斷集合是否為空
- laravel 使用ymondesigns/jwt-auth jwt
- laravel 模型工廠
- laravel 自定義助手函數
- laravel 自帶auth的登錄
- 寶塔開啟laravel隊列
- laravel 蘋果內購
- laravel 中的.env.example
- laravel 監聽執行過的sql語句
- laravel-websockets 替代pusher 發送頻道消息
- 記laravel config配置文件目錄中不能使用 url()助手函數
- laravel使用 inspector 進行實時監控
- laravel 項目部署的配置
- laravel 刪除mongodb集合
- laravel 自定義項目命名空間
- laravel 易錯提醒
- laravel 自己組裝分頁
- laravel 設置定時任務
- laravel事件和隊列指定隊列名
- laravel 使用validate檢測名字是否唯一
- laravel + nginx 偽靜態分析
- fastadmin
- cms
- 標簽
- 模板
- dact-admin
- dcat-admin的安裝
- dcat-admin的curd使用
- dact-admin表單使用
- dcat-admin行為表單使用
- dcat-admin使用技巧
- dcat-admin自定義文件上傳
- dcat-admin的js彈窗
- dcat-admin 工具表單傳參
- dcat-admin listbox編輯回顯用法
- weixin
- 微信支付
- 支付類
- 小程序
- 微信提現類
- jwt
- lcobucci/jwt
- Firebase\JWT
- phpstudy
- nginx配置tp5 505 404 錯誤
- tp5重寫 apache
- 織夢模板 使用weight 排序
- phpstudy 添加php8.1版本
- phpstudy ERR_CONNECTION_REFUSED
- phpstudy 設置多個版本php
- 阿里云
- 支付寶支付
- 阿里云短信
- 阿里云OSS上傳圖片報錯
- 阿里云號碼認證(一鍵登錄)
- send login code error: 發送驗證碼失敗:cURL error 28: Connection timed out after 5001 milliseconds
- 極光號碼認證(一鍵登錄)
- git使用
- git
- sentry專欄
- sentry的私有化部署
- sentry設置郵箱
- sentry設置url地址
- sentry中KafkaError OFFSET_OUT_OF_RANGE error
- centos
- tar 壓縮解壓
- centos 8 Errors during downloading metadata for repository 'appstream'
- vim的使用
- ssh秘鑰登錄
- 修改了.bashrc不能立即生效
- 設置軟連接
- 使用echo清空文件內容
- 查看文件大小
- centos8 設置靜態ip
- nginx
- nginx的學習
- nginx配置wss
- supervisor的使用
- shell的使用
- 數據庫
- mysql
- mysql的事務隔離級別
- mysql共享鎖和排它鎖
- mysql的三范式
- mysql 在那些場景下索引會失效
- mysql 的書寫順序
- mysql case用法
- mysql 以逗號分割字符串
- msyql innodb 行鎖解決高并發
- mysql修改字符集
- 鎖
- 樂觀鎖悲觀鎖
- mysql 最左索引原則
- mysql 同表兩列值互換
- mysql升序排列字段為0的在最后
- mysql case when then else end 語法
- mysql 常見錯誤
- mysql json用法
- MongoDB
- mongodb安裝
- redis
- redis 常用通用命令
- string類型的常見命令
- 連接遠程redis刪除指定的值
- markdown
- markdown的使用
- github
- github使用小技巧
- jenkins
- 安裝jenkins
- jenkins設置時區
- docker
- 安裝docker
- docker容器設為自啟動和取消容器自啟動
- docker 安裝mysql
- docker-compose
- docker 安裝php
- docker-compose安裝nginx
- docker-compose安裝php
- docker安裝php+supervisor
- composer使用
- composer
- win10檢查端口占用
- 局域網內同事訪問自己的項目
- 本地測試設置https辦法
- 正則表達式
- 前端代碼和后臺代碼部署在一起的解決方法
- pc微信抓包小程序
- xshell一年后提示需要更新才能打開
- 使用ssh秘鑰登錄服務器
- supervisor
- supervisor的使用
- 瀏覽器的強制緩存和協商緩存
- window11下ssh遠程登錄服務器
- chatgpt
- 注冊chatgpt
- 第三方chatgpt地址
- 前端
- jquery 常用方法
- jquery 省市區三級聯動
- 百度地圖短地址
- npm
- webpack
- vue
- 谷歌安裝vue-devtools的使用
- swiper 一屏顯示頁面
- 騰訊地圖
- jquery點擊圖片放大
- 移動端rem適配
- 彈性布局flex
- CSS
- box-sizing
- 移動端去掉滾動條
- 三角形
- 樹形結構
- require.js的使用
- 微擎人人商城
- 人人商城彈出框
- 常用方法
- 客服消息
- 企業支付到零錢
- 修復權限問題
- 獲取access_token
- 其他管理員沒有應用 調用不了p方法
- 修改公眾號推送消息
- 人人商城
- 人人商城二開常見問題
- 人人商城應用顯示隱藏
- 微擎
- 人人商城小程序解密登錄
- 面試題
- 遍歷目錄中的文件和目錄
- 冒泡排序
- php 在字符串中找到最長對稱字符串
- 地圖相關
- 百度地圖根據ip獲取地址
- 百度,騰訊,高德,地圖點擊跳轉
- 百度地圖根據地址獲取經緯度
- 百度地圖和騰訊地圖經緯度互轉
- 其他
- B站跳過充電環節
- 可愛貓咪回收站制作(附圖)
- 程序員變量命名網站
- 解決谷歌瀏覽器強制跳轉https
- 隨機密碼生成
- 編輯器
- vs code使用
- phpstrom
- phpstrom 常用命令
- phpstrom ctrl+b后想回到之前的位置
- phpstrom 批量操作下劃線轉駝峰
- phpstrom 插件
- phpstrom 使用ctrl+shift+f后搜索不能輸入中文
- phpstrom中項目.env文件會自動消失,不顯示
- vscode插件