網(wǎng)站手機(jī)訪問跳轉(zhuǎn)萬(wàn)網(wǎng)官網(wǎng)域名注冊(cè)
?一、首先來(lái)說(shuō)一下什么是共享鎖?什么是排他鎖?
共享:我可以讀
?寫
?加鎖
?, 別人可以?讀
?加鎖
。
排他:只有我?才?可以?讀
?寫
?加鎖
?, 也就是說(shuō),必須要等我提交事務(wù),其他的才可以操作。
二、簡(jiǎn)單例子實(shí)現(xiàn)加鎖
?鎖和事務(wù)在使用時(shí)需要配合使用,也就是用鎖時(shí)需要先開啟事務(wù),事務(wù)提交時(shí),會(huì)自動(dòng)解鎖。
DB::beginTransaction(); // 開啟事務(wù)$good = \App\Models\Good::sharedLock()->first(); //共享鎖 s鎖 讀鎖
// $good = \App\Models\Good::lockForUpdate()->first(); //排他鎖 x鎖 寫鎖...DB::commit();
DB::beginTransaction();
$goodsInfo?= Goods::where('goods_id',$gid)->lockForUpdate()->first();
$goodsInfo->seckill_stock-=1;
$goodsInfo->save();
DB::commit();
三、怎樣利用鎖和事務(wù)解決并發(fā)問題?
在我們的工作中,常常會(huì)出現(xiàn)一些對(duì)數(shù)量控制有精確要求的需求,比如商品庫(kù)存量、獎(jiǎng)品數(shù)量、報(bào)名人數(shù)限制等等,這些應(yīng)用場(chǎng)景往往都存在高并發(fā)可能,比較容易出現(xiàn)數(shù)據(jù)量超量問題。以下做一下示例探索:
(1)首先設(shè)計(jì)一個(gè)存量表
CREATE TABLE `product` (`id` int(11) NOT NULL AUTO_INCREMENT,`product_name` varchar(255) NOT NULL DEFAULT '',`count` int(10) NOT NULL DEFAULT '0',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
(2)添加一行數(shù)據(jù)如下,設(shè)定基礎(chǔ)庫(kù)存量為 10
(3)問題代碼如下:
$process_num = 50; //開50個(gè)進(jìn)程,模擬50個(gè)用戶for ($i = 0; $i < $process_num; $i++) {MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {if (Db::name('product')->where('id', 1)->value('count') > 0) {$res = Db::name('product')->where('id', 1)->setDec('count');if ($res) {dump('獲取到更新資源權(quán)限:' . $i);}}});}
執(zhí)行結(jié)果,50 個(gè)用戶都獲取到了更新資源的權(quán)限,但是數(shù)據(jù)庫(kù)相應(yīng)數(shù)據(jù)存量變成了 - 40
高并發(fā)帶來(lái)的問題,同一時(shí)刻有多個(gè)進(jìn)程讀取同一條數(shù)據(jù),同一時(shí)刻有多個(gè)進(jìn)程更新同一條數(shù)據(jù)
(4)解決方案
1.方案1
要進(jìn)行 DML 層面的限制(最后關(guān)卡安全,報(bào)錯(cuò)總比出現(xiàn)數(shù)據(jù)問題產(chǎn)生的影響小),主要的修改是將 count 的類型改成了無(wú)符號(hào)整數(shù),這樣該值就不可能再出現(xiàn)負(fù)數(shù)值
CREATE TABLE `product` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_name` varchar(255) NOT NULL DEFAULT '',
`count` int(10) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
執(zhí)行一下代碼,當(dāng) count 值從 10 減到 0 時(shí),就不能再減少了,再減就會(huì)出現(xiàn)數(shù)據(jù)庫(kù)報(bào)錯(cuò)
2.方案2
mysql 提供的行級(jí)鎖 select ... lock in share mode(阻塞寫),select ... for update(阻塞讀寫,悲觀鎖),所以 for update 機(jī)制能滿足我們的原子要求。編輯代碼如下:
$process_num = 50; //開50個(gè)進(jìn)程,模擬50個(gè)用戶for ($i = 0; $i < $process_num; $i++) {MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {Db::startTrans(); //行級(jí)鎖必須在事務(wù)中才能生效//設(shè)置for update,進(jìn)程會(huì)阻塞在這里,只能允許一個(gè)進(jìn)程獲取到行鎖,其他等待獲取if (Db::name('product')->where('id', 1)->lock('for update')->value('count') > 0) { $res = Db::name('product')->where('id', 1)->setDec('count');if ($res) {dump('獲取到更新資源權(quán)限:' . $i);}}Db::commit();});}
只有十個(gè)進(jìn)程獲取到了更新權(quán)限,消費(fèi)正常
3.方案3
將條件語(yǔ)句放到 update 上,保持語(yǔ)句執(zhí)行的原子性,杜絕并發(fā)幻讀
修改代碼如下:
$process_num = 50; //開50個(gè)進(jìn)程,模擬50個(gè)用戶for ($i = 0; $i < $process_num; $i++) {MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {//合并兩條語(yǔ)句為一條更新語(yǔ)句$res = Db::name('product')->where('id', 1)->where('count', '>', 0)->setDec('count');if ($res) {dump('獲取到更新資源權(quán)限:' . $i);}});}
只有十個(gè)進(jìn)程獲取到了更新權(quán)限,消費(fèi)正常
4.方案4
文件鎖機(jī)制解決
$process_num = 50; //開50個(gè)進(jìn)程,模擬50個(gè)用戶for ($i = 0; $i < $process_num; $i++) {MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {$filename = app()->getRootPath() . 'runtime/lock';$file = fopen($filename, 'w'); //打開文件$lock = flock($file, LOCK_EX);// $lock=flock($handle, LOCK_EX|LOCK_NB); (異步非阻塞,所有進(jìn)程如果出現(xiàn)獲取不到鎖,不等待跳過,加鎖失敗)//獲取文件排他鎖:LOCK_EX(異步阻塞,只有一個(gè)進(jìn)程獲得鎖,其他競(jìng)爭(zhēng)進(jìn)程等待)//還有一種共享鎖:LOCK_SH(所有進(jìn)程都可以獲取共享鎖,讀取文件,當(dāng)且只有一個(gè)鎖時(shí),才允許寫操作,否則操作失敗,容易出現(xiàn)死鎖問題)if ($lock) {try {if (Db::name('product')->where('id', 1)->lock('for update')->value('count') > 0) {$res = Db::name('product')->where('id', 1)->setDec('count');if ($res) {dump('獲取到更新資源權(quán)限:' . $i);}}} catch (\Exception $e) {dump($e->getMessage());} finally {flock($file, LOCK_UN); //無(wú)論如何都要釋放鎖}}fclose($file); //關(guān)閉文件句柄});}
只有十個(gè)進(jìn)程獲取到了更新權(quán)限,消費(fèi)正常
5.方案5
分布式鎖機(jī)制解決
以上文件鎖,只適應(yīng)于單體架構(gòu)的需求,在集群架構(gòu)、分布式等多機(jī)聯(lián)網(wǎng)結(jié)構(gòu)中就是掩耳盜鈴了,所以適應(yīng)性更好地鎖機(jī)制還是要使用分布式鎖,分布式鎖最常用和最易用就是 redis 的 setnx 鎖了。
$process_num = 50; //開50個(gè)進(jìn)程,模擬50個(gè)用戶for ($i = 0; $i < $process_num; $i++) {MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {//獲取redis鎖//關(guān)于CacheHelper::getRedisLock是怎樣獲取鎖的,注意幾個(gè)點(diǎn)就行:1.如何避免死鎖;2.如何設(shè)置過期時(shí)間;3.如何設(shè)置搶占條件;4.如何循環(huán)等待判斷。這些不在本文討論范圍,可自行研究,以后有空我也可以寫一篇博文$lock = CacheHelper::getRedisLock('redis_lock');if ($lock) {try {if (Db::name('product')->where('id', 1)->lock('for update')->value('count') > 0) {$res = Db::name('product')->where('id', 1)->setDec('count');if ($res) {dump('獲取到更新資源權(quán)限:' . $i);}}} catch (\Exception $e) {dump($e->getMessage());}} else {
// dump('獲取redis鎖失敗');}});}
參考鏈接:淺談并發(fā)加鎖 | Laravel China 社區(qū) (learnku.com)