說明
遇見并發(fā)情況,需要保證數(shù)據(jù)的準(zhǔn)確性,也就是與正確的預(yù)期一致,此時就會用到鎖。
鎖是在并發(fā)下控制程序的執(zhí)行邏輯,以此來保證數(shù)據(jù)按照預(yù)期變動。
如果不加鎖,并發(fā)情況下的可能數(shù)據(jù)不一致的情況,這是個概率問題。
樂觀鎖CAS
簡介
樂觀鎖很樂觀,假設(shè)數(shù)據(jù)一般情況不會造成沖突,屬于程序?qū)用娴倪壿嬫i,在數(shù)據(jù)進(jìn)行更新時,才進(jìn)行鎖的檢測。是通過添加一個版本號的方式實現(xiàn)的,每當(dāng)數(shù)據(jù)這一行所在的數(shù)據(jù)發(fā)生變化,則對應(yīng)的版本號+1,更新數(shù)據(jù)時,將版本號作為查詢條件。
至于是否要加事務(wù),看寫操作單條數(shù)據(jù)還是寫操作多條數(shù)據(jù)。
注意:網(wǎng)上很多解決方案用時間戳來做version字段,我持反對意見,并發(fā)可能是一瞬間的事,不到一秒就有好多請求,用時間戳粒度太大,用隨機(jī)字符串都比用這個強(qiáng)。
用法
#示例
update test set score = score + 1 where id = 1
#優(yōu)化為,這種簡單,但是會有ABA的問題:
select score as old_score from test where id = 1;
update test set score = score + 1 where id = 1 and score = old_score;
#或者添加一個version字段,這種不存在ABA的問題
select version from test where id = 1;
update test set score = score + 1 where id = 1 and version = version;
適用場景
- 讀多寫少:由于并發(fā)寫操作較少,樂觀鎖的修改數(shù)據(jù)受影響行數(shù)為0概率也較低。
- 允許一定量的重試或不需要重試的場景:這個要根據(jù)業(yè)務(wù),否則來回重試會降低性能。
優(yōu)點
實現(xiàn)簡單:樂觀鎖在代碼上就可以實現(xiàn),不需要額外對數(shù)據(jù)庫額外操作。
無死鎖風(fēng)險:悲觀鎖有死鎖風(fēng)險,樂觀鎖沒有。
無需重試情況下,性能較高:樂觀鎖機(jī)制在并發(fā)訪問情況下,不需要像悲觀鎖那樣阻塞其他事務(wù),提供了更高的并發(fā)性能,前提當(dāng)前業(yè)務(wù)需求能容忍寫操作失敗的情況。
缺點
并發(fā)沖突:多加了一個where條件,只能保證數(shù)據(jù)最終不會出錯,不能保證每條寫操作的SQL都執(zhí)行成功(也就是受影響行數(shù)>0)。
不提供強(qiáng)一致性:強(qiáng)一致性要求數(shù)據(jù)的狀態(tài)在任何時刻都保持一致,悲觀鎖是到寫操作那一步才去驗證,期間只是做了個where條件的過濾。
ABA問題:一個字段的值在請求X中查詢出來是A,后續(xù)代碼實現(xiàn)樂觀鎖,因為并發(fā)量大,同時過來一個Y請求,將A值改成了B,因為一些業(yè)務(wù)原因又改成了A,整個過程雖然不影響請求X的結(jié)果,且能正常執(zhí)行,但是聯(lián)合其它數(shù)據(jù),這個情況是否符合業(yè)務(wù)場景,不好說,所以最好的解決方案,就是專門做一個version字段,且不會與之前的version重復(fù),即可,把這個version字段作為where條件,而不是存A或者B字段的所在字段作為where條件。
悲觀鎖
簡介
悲觀鎖比較悲觀,假設(shè)數(shù)據(jù)一定會造成沖突,屬于MySQL層面的鎖。通過加鎖阻塞其他事務(wù),悲觀鎖可以保證在任何時刻,只有一個事務(wù)能夠修改或訪問共享資源,從而實現(xiàn)了強(qiáng)一致性。這意味著在悲觀鎖機(jī)制下,每個事務(wù)的讀寫操作都是有序、線性的。
需要事務(wù)的參與。
用法
在事務(wù)中的查詢語句添加for update即可。
如果此時執(zhí)行了三行內(nèi)容沒有commit,再次執(zhí)行update test set score = score + 1 where id = 1;則處于阻塞狀態(tài),需要等commit之后,才能執(zhí)行。
start transaction;
select * from test where id = 1 for update;
update test set score = score + 1 where id = 1;
commit;
適用場景
寫多寫操作的前提,是保證數(shù)據(jù)不出錯,悲觀鎖的機(jī)制很符合。
優(yōu)點
強(qiáng)一致性:基于事務(wù)又加鎖,一致性可以保證。
實現(xiàn)簡單:在事務(wù)中for update即可,開發(fā)者不需要在這上面關(guān)注太多。
缺點
死鎖風(fēng)險:悲觀鎖在使用不當(dāng)?shù)那闆r下可能導(dǎo)致死鎖。如果多個事務(wù)持有鎖并相互等待對方釋放鎖的情況發(fā)生,就可能發(fā)生死鎖。
性能較低:悲觀鎖通常需要在整個事務(wù)過程中鎖定資源,這可能導(dǎo)致其他事務(wù)阻塞。
模擬實現(xiàn)
前置準(zhǔn)備
#創(chuàng)建一個非常簡單的表,并插入一條數(shù)據(jù)
CREATE TABLE `test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`score` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `test` (`id`, `score`) VALUES (1, 0);
需求模擬
查詢test表id為1的數(shù)據(jù),檢測到score值為0,則自增,否則終止。
不加鎖實現(xiàn)
為了提升性能,使用了原生PDO操作MySQL去實現(xiàn)。
//連接數(shù)據(jù)庫
$pdo = new \PDO("mysql:host=127.0.0.1;port=3306;dbname=temp;", 'root', 'root');
$pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION);
$pdo->query('set names utf8mb4');
//查詢
$query = $pdo->query('select score from test');
$query->setFetchMode(\PDO::FETCH_ASSOC);
$res = $query->fetchALL();
if($res[0]['score'] == 0) {
$res = $pdo->exec('update test set score = score + 1 where id = 1');
var_dump($res);
}
并發(fā)模擬
用ab壓測,發(fā)現(xiàn)效果不明顯,可能是ab工具不夠力或者電腦線程數(shù)量太少導(dǎo)致。
這里用的是ApiPost的壓測工具。500個并發(fā)去多次壓測一輪,發(fā)現(xiàn)score值是3,證明確實因為并發(fā)造成了與預(yù)期結(jié)果不一致的情況。文章來源:http://www.zghlxwxcb.cn/news/detail-819482.html
樂觀鎖解決方案(忽略ABA問題)
#將sql改為如下所示,實測多次,score最大值是1
#注意這種行為,只能保證score的值最大是1,無法保證執(zhí)行這個SQL的時候,受影響行數(shù)>0
update test set score = score + 1 where id = 1 and score = 0
悲觀鎖解決方案
$pdo = new \PDO("mysql:host=127.0.0.1;port=3306;dbname=temp;", 'root', 'root');
$pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION);
$pdo->query('set names utf8mb4');
$redis = new Redis;
$redis->connect('127.0.0.1', 6379);
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare("select * from test where id = 1 for update");
$stmt->execute();
$res = $stmt->fetch(PDO::FETCH_ASSOC);
if($res['score'] == 0) {
$stmt = $pdo->prepare("UPDATE test SET score = (score + 1) where id = 1");
$stmt->execute();
$pdo->commit();
$redis->incr('commit');
} else {
$redis->incr('rollback');
$pdo->rollBack();
}
} catch (PDOException $e) {
$pdo->rollBack();
}
// 關(guān)閉數(shù)據(jù)庫連接
$pdo = null;
500個并發(fā)壓測一輪,查看redis數(shù)據(jù),commit數(shù)量為1,其余499全部都是rollback,這么多的回滾不代表大錯特錯(演示效果),而是因為第一個事務(wù)執(zhí)行成功后,再執(zhí)行其它事務(wù),正因為一個一個排隊,就不會出現(xiàn)同時讀取多個score值為0的情況了。文章來源地址http://www.zghlxwxcb.cn/news/detail-819482.html
到了這里,關(guān)于MySQL樂觀鎖與悲觀鎖的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!