前言
近期開(kāi)發(fā)與錢(qián)相關(guān)的項(xiàng)目,在高并發(fā)場(chǎng)景下對(duì)數(shù)據(jù)的準(zhǔn)確行有很高的要求瓮钥,用到了for update筋量,故總結(jié)一波以便日后留戀。
for update的使用場(chǎng)景
如果遇到存在高并發(fā)并且對(duì)于數(shù)據(jù)的準(zhǔn)確性很有要求的場(chǎng)景碉熄,是需要了解和使用for update的桨武。
比如涉及到金錢(qián)、庫(kù)存等锈津。一般這些操作都是很長(zhǎng)一串并且是開(kāi)啟事務(wù)的呀酸。如果庫(kù)存剛開(kāi)始讀的時(shí)候是1,而立馬另一個(gè)進(jìn)程進(jìn)行了update將庫(kù)存更新為0了琼梆,而事務(wù)還沒(méi)有結(jié)束性誉,會(huì)將錯(cuò)的數(shù)據(jù)一直執(zhí)行下去,就會(huì)有問(wèn)題茎杂。所以需要for upate 進(jìn)行數(shù)據(jù)加鎖防止高并發(fā)時(shí)候數(shù)據(jù)出錯(cuò)错览。
記住一個(gè)原則:一鎖二判三更新
排他鎖的申請(qǐng)前提
沒(méi)有線程對(duì)該結(jié)果集中的任何行數(shù)據(jù)使用排他鎖或共享鎖,否則申請(qǐng)會(huì)阻塞煌往。
for update僅適用于InnoDB倾哺,且必須在事務(wù)塊(BEGIN/COMMIT)中才能生效。在進(jìn)行事務(wù)操作時(shí),通過(guò)“for update”語(yǔ)句羞海,MySQL會(huì)對(duì)查詢(xún)結(jié)果集中每行數(shù)據(jù)都添加排他鎖忌愚,其他線程對(duì)該記錄的更新與刪除操作都會(huì)阻塞。排他鎖包含行鎖却邓、表鎖硕糊。
場(chǎng)景分析
假設(shè)有一張商品表 goods,它包含 id申尤,商品名稱(chēng)癌幕,庫(kù)存量三個(gè)字段,表結(jié)構(gòu)如下:
CREATE TABLE goods
(
id
int(11) NOT NULL AUTO_INCREMENT,
name
varchar(100) DEFAULT NULL,
stock
int(11) DEFAULT NULL,
PRIMARY KEY (id
),
UNIQUE KEY idx_name
(name
) USING HASH
) ENGINE=InnoDB
插入如下數(shù)據(jù):
INSERT INTO goods
VALUES ('1', 'prod11', '1000');
INSERT INTO goods
VALUES ('2', 'prod12', '1000');
INSERT INTO goods
VALUES ('3', 'prod13', '1000');
INSERT INTO goods
VALUES ('4', 'prod14', '1000');
INSERT INTO goods
VALUES ('5', 'prod15', '1000');
INSERT INTO goods
VALUES ('6', 'prod16', '1000');
INSERT INTO goods
VALUES ('7', 'prod17', '1000');
INSERT INTO goods
VALUES ('8', 'prod18', '1000');
INSERT INTO goods
VALUES ('9', 'prod19', '1000');
一昧穿、數(shù)據(jù)一致性
假設(shè)有A、B兩個(gè)用戶(hù)同時(shí)各購(gòu)買(mǎi)一件 id=1 的商品橙喘,用戶(hù)A獲取到的庫(kù)存量為 1000时鸵,用戶(hù)B獲取到的庫(kù)存量也為 1000,用戶(hù)A完成購(gòu)買(mǎi)后修改該商品的庫(kù)存量為 999厅瞎,用戶(hù)B完成購(gòu)買(mǎi)后修改該商品的庫(kù)存量為 999饰潜,此時(shí)庫(kù)存量數(shù)據(jù)產(chǎn)生了不一致。
有兩種解決方案:
悲觀鎖方案:每次獲取商品時(shí)和簸,對(duì)該商品加排他鎖彭雾。也就是在用戶(hù)A獲取獲取 id=1 的商品信息時(shí)對(duì)該行記錄加鎖,期間其他用戶(hù)阻塞等待訪問(wèn)該記錄锁保。悲觀鎖適合寫(xiě)入頻繁的場(chǎng)景薯酝。
begin;
select * from goods where id = 1 for update;
update goods set stock = stock - 1 where id = 1;
commit;
樂(lè)觀鎖方案:每次獲取商品時(shí),不對(duì)該商品加鎖爽柒。在更新數(shù)據(jù)的時(shí)候需要比較程序中的庫(kù)存量與數(shù)據(jù)庫(kù)中的庫(kù)存量是否相等吴菠,如果相等則進(jìn)行更新,反之程序重新獲取庫(kù)存量浩村,再次進(jìn)行比較做葵,直到兩個(gè)庫(kù)存量的數(shù)值相等才進(jìn)行數(shù)據(jù)更新。樂(lè)觀鎖適合讀取頻繁的場(chǎng)景心墅。
不加鎖獲取 id=1 的商品對(duì)象
select * from goods where id = 1
begin;
更新 stock 值酿矢,這里需要注意 where 條件 “stock = cur_stock”,只有程序中獲取到的庫(kù)存量與數(shù)據(jù)庫(kù)中的庫(kù)存量相等才執(zhí)行更新
update goods set stock = stock - 1 where id = 1 and stock = cur_stock;
commit;
如果我們需要設(shè)計(jì)一個(gè)商城系統(tǒng)怎燥,該選擇以上的哪種方案呢瘫筐?
查詢(xún)商品的頻率比下單支付的頻次高,基于以上我可能會(huì)優(yōu)先考慮第二種方案(當(dāng)然還有其他的方案刺覆,這里只考慮以上兩種方案)严肪。
二、行鎖與表鎖
InnoDB默認(rèn)是行級(jí)別的鎖,當(dāng)有明確指定的主鍵時(shí)候驳糯,是行級(jí)鎖篇梭。否則是表級(jí)別。
for update的注意點(diǎn)
for update 僅適用于InnoDB酝枢,并且必須開(kāi)啟事務(wù)恬偷,在begin與commit之間才生效。
要測(cè)試for update的鎖表情況帘睦,可以利用MySQL的Command Mode袍患,開(kāi)啟二個(gè)視窗來(lái)做測(cè)試。
1竣付、只根據(jù)主鍵進(jìn)行查詢(xún)诡延,并且查詢(xún)到數(shù)據(jù),主鍵字段產(chǎn)生行鎖古胆。
begin;
select * from goods where id = 1 for update;
commit;
2肆良、只根據(jù)主鍵進(jìn)行查詢(xún),沒(méi)有查詢(xún)到數(shù)據(jù)逸绎,不產(chǎn)生鎖惹恃。
begin;
select * from goods where id = 1 for update;
commit;
3、根據(jù)主鍵棺牧、非主鍵含索引(name)進(jìn)行查詢(xún)巫糙,并且查詢(xún)到數(shù)據(jù),主鍵字段產(chǎn)生行鎖颊乘,name字段產(chǎn)生行鎖参淹。
begin;
select * from goods where id = 1 and name='prod11' for update;
commit;
4、根據(jù)主鍵疲牵、非主鍵含索引(name)進(jìn)行查詢(xún)承二,沒(méi)有查詢(xún)到數(shù)據(jù),不產(chǎn)生鎖纲爸。
begin;
select * from goods where id = 1 and name='prod12' for update;
commit;
5亥鸠、根據(jù)主鍵、非主鍵不含索引(name)進(jìn)行查詢(xún)识啦,并且查詢(xún)到數(shù)據(jù)负蚊,如果其他線程按主鍵字段進(jìn)行再次查詢(xún),則主鍵字段產(chǎn)生行鎖颓哮,如果其他線程按非主鍵不含索引字段進(jìn)行查詢(xún)家妆,則非主鍵不含索引字段產(chǎn)生表鎖,如果其他線程按非主鍵含索引字段進(jìn)行查詢(xún)冕茅,則非主鍵含索引字段產(chǎn)生行鎖伤极,如果索引值是枚舉類(lèi)型蛹找,mysql也會(huì)進(jìn)行表鎖,這段話(huà)有點(diǎn)拗口哨坪,大家仔細(xì)理解一下庸疾。
begin;
select * from goods where id = 1 and name='prod11' for update;
commit;
6、根據(jù)主鍵当编、非主鍵不含索引(name)進(jìn)行查詢(xún)届慈,沒(méi)有查詢(xún)到數(shù)據(jù),不產(chǎn)生鎖忿偷。
begin;
select * from goods where id = 1 and name='prod12' for update;
commit;
7金顿、根據(jù)非主鍵含索引(name)進(jìn)行查詢(xún),并且查詢(xún)到數(shù)據(jù)鲤桥,name字段產(chǎn)生行鎖揍拆。
begin;
select * from goods where name='prod11' for update;
commit;
8、根據(jù)非主鍵含索引(name)進(jìn)行查詢(xún)茶凳,沒(méi)有查詢(xún)到數(shù)據(jù)礁凡,不產(chǎn)生鎖。
begin;
select * from goods where name='prod11' for update;
commit;
9慧妄、根據(jù)非主鍵不含索引(stock)進(jìn)行查詢(xún),并且查詢(xún)到數(shù)據(jù)剪芍,stock字段產(chǎn)生表鎖塞淹。
begin;
select * from goods where stock='1000' for update;
commit;
10、根據(jù)非主鍵不含索引(stock)進(jìn)行查詢(xún)罪裹,沒(méi)有查詢(xún)到數(shù)據(jù)饱普,stock字段產(chǎn)生表鎖。
begin;
select * from goods where stock='2000' for update;
commit;
11状共、只根據(jù)主鍵進(jìn)行查詢(xún)套耕,查詢(xún)條件為不等于,并且查詢(xún)到數(shù)據(jù)峡继,主鍵字段產(chǎn)生表鎖冯袍。
begin;
select * from goods where id <> 1 for update;
commit;
12、只根據(jù)主鍵進(jìn)行查詢(xún)碾牌,查詢(xún)條件為不等于康愤,沒(méi)有查詢(xún)到數(shù)據(jù),主鍵字段產(chǎn)生表鎖舶吗。
begin;
select * from goods where id <> 1 for update;
commit;
13征冷、只根據(jù)主鍵進(jìn)行查詢(xún),查詢(xún)條件為 like誓琼,并且查詢(xún)到數(shù)據(jù)检激,主鍵字段產(chǎn)生表鎖肴捉。
begin;
select * from goods where id like '1' for update;
commit;
14、只根據(jù)主鍵進(jìn)行查詢(xún)叔收,查詢(xún)條件為 like齿穗,沒(méi)有查詢(xún)到數(shù)據(jù),主鍵字段產(chǎn)生表鎖今穿。
begin;
select * from goods where id like '1' for update;
commit;
測(cè)試環(huán)境
數(shù)據(jù)庫(kù)版本:5.1.48-community
數(shù)據(jù)庫(kù)引擎:InnoDB Supports transactions, row-level locking, and foreign keys
數(shù)據(jù)庫(kù)隔離策略:REPEATABLE-READ(系統(tǒng)缤灵、會(huì)話(huà))
總結(jié)
1、InnoDB行鎖是通過(guò)給索引上的索引項(xiàng)加鎖來(lái)實(shí)現(xiàn)的蓝晒,只有通過(guò)索引條件檢索數(shù)據(jù)腮出,InnoDB才使用行級(jí)鎖,否則芝薇,InnoDB將使用表鎖胚嘲。
2、由于MySQL的行鎖是針對(duì)索引加的鎖洛二,不是針對(duì)記錄加的鎖馋劈,所以雖然是訪問(wèn)不同行的記錄,但是如果是使用相同的索引鍵晾嘶,是會(huì)出現(xiàn)鎖沖突的妓雾。應(yīng)用設(shè)計(jì)的時(shí)候要注意這一點(diǎn)。
3垒迂、當(dāng)表有多個(gè)索引的時(shí)候械姻,不同的事務(wù)可以使用不同的索引鎖定不同的行,另外机断,不論是使用主鍵索引楷拳、唯一索引或普通索引,InnoDB都會(huì)使用行鎖來(lái)對(duì)數(shù)據(jù)加鎖吏奸。
4欢揖、即便在條件中使用了索引字段,但是否使用索引來(lái)檢索數(shù)據(jù)是由MySQL通過(guò)判斷不同執(zhí)行計(jì)劃的代價(jià)來(lái)決定的奋蔚,如果MySQL認(rèn)為全表掃描效率更高她混,比如對(duì)一些很小的表,它就不會(huì)使用索引旺拉,這種情況下InnoDB將使用表鎖产上,而不是行鎖。因此蛾狗,在分析鎖沖突時(shí)晋涣,別忘了檢查SQL的執(zhí)行計(jì)劃,以確認(rèn)是否真正使用了索引沉桌。
5谢鹊、檢索值的數(shù)據(jù)類(lèi)型與索引字段不同算吩,雖然MySQL能夠進(jìn)行數(shù)據(jù)類(lèi)型轉(zhuǎn)換,但卻不會(huì)使用索引佃扼,從而導(dǎo)致InnoDB使用表鎖偎巢。通過(guò)用explain檢查兩條SQL的執(zhí)行計(jì)劃,我們可以清楚地看到了這一點(diǎn)兼耀。