Mysql InnoDB 排他鎖
用法: select … for update;mysql
例如:select * from goods where id = 1 for update;sql
排他鎖的申請前提:沒有線程對該結果集中的任何行數據使用排他鎖或共享鎖,不然申請會阻塞。數據庫
for update僅適用於InnoDB,且必須在事務塊(BEGIN/COMMIT)中才能生效。在進行事務操做時,經過「for update」語句,MySQL會對查詢結果集中每行數據都添加排他鎖,其餘線程對該記錄的更新與刪除操做都會阻塞。排他鎖包含行鎖、表鎖。測試
場景分析
假設有一張商品表 goods,它包含 id,商品名稱,庫存量三個字段,表結構以下:url
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
插入以下數據:spa
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');
1、數據一致性.net
假設有A、B兩個用戶同時各購買一件 id=1 的商品,用戶A獲取到的庫存量爲 1000,用戶B獲取到的庫存量也爲 1000,用戶A完成購買後修改該商品的庫存量爲 999,用戶B完成購買後修改該商品的庫存量爲 999,此時庫存量數據產生了不一致。線程
有兩種解決方案:設計
悲觀鎖方案:每次獲取商品時,對該商品加排他鎖。也就是在用戶A獲取獲取 id=1 的商品信息時對該行記錄加鎖,期間其餘用戶阻塞等待訪問該記錄。悲觀鎖適合寫入頻繁的場景。code
begin; select * from goods where id = 1 for update; update goods set stock = stock - 1 where id = 1; commit;
樂觀鎖方案:每次獲取商品時,不對該商品加鎖。在更新數據的時候須要比較程序中的庫存量與數據庫中的庫存量是否相等,若是相等則進行更新,反之程序從新獲取庫存量,再次進行比較,直到兩個庫存量的數值相等才進行數據更新。樂觀鎖適合讀取頻繁的場景。
#不加鎖獲取 id=1 的商品對象 select * from goods where id = 1 begin; #更新 stock 值,這裏須要注意 where 條件 「stock = cur_stock」,只有程序中獲取到的庫存量與數據庫中的庫存量相等才執行更新 update goods set stock = stock - 1 where id = 1 and stock = cur_stock; commit;
若是咱們須要設計一個商城系統,該選擇以上的哪一種方案呢?
查詢商品的頻率比下單支付的頻次高,基於以上我可能會優先考慮第二種方案(固然還有其餘的方案,這裏只考慮以上兩種方案)。
2、行鎖與表鎖
一、只根據主鍵進行查詢,而且查詢到數據,主鍵字段產生行鎖。
begin; select * from goods where id = 1 for update; commit;
二、只根據主鍵進行查詢,沒有查詢到數據,不產生鎖。
begin; select * from goods where id = 1 for update; commit;
三、根據主鍵、非主鍵含索引(name)進行查詢,而且查詢到數據,主鍵字段產生行鎖,name字段產生行鎖。
begin; select * from goods where id = 1 and name='prod11' for update; commit;
四、根據主鍵、非主鍵含索引(name)進行查詢,沒有查詢到數據,不產生鎖。
begin; select * from goods where id = 1 and name='prod12' for update; commit;
五、根據主鍵、非主鍵不含索引(name)進行查詢,而且查詢到數據,若是其餘線程按主鍵字段進行再次查詢,則主鍵字段產生行鎖,若是其餘線程按非主鍵不含索引字段進行查詢,則非主鍵不含索引字段產生表鎖,若是其餘線程按非主鍵含索引字段進行查詢,則非主鍵含索引字段產生行鎖,若是索引值是枚舉類型,mysql也會進行表鎖,這段話有點拗口,你們仔細理解一下。
begin; select * from goods where id = 1 and name='prod11' for update; commit;
六、根據主鍵、非主鍵不含索引(name)進行查詢,沒有查詢到數據,不產生鎖。
begin; select * from goods where id = 1 and name='prod12' for update; commit;
七、根據非主鍵含索引(name)進行查詢,而且查詢到數據,name字段產生行鎖。
begin; select * from goods where name='prod11' for update; commit;
八、根據非主鍵含索引(name)進行查詢,沒有查詢到數據,不產生鎖。
begin; select * from goods where name='prod11' for update; commit;
九、根據非主鍵不含索引(name)進行查詢,而且查詢到數據,name字段產生表鎖。
begin; select * from goods where name='prod11' for update; commit;
十、根據非主鍵不含索引(name)進行查詢,沒有查詢到數據,name字段產生表鎖。
begin; select * from goods where name='prod11' for update; commit;
十一、只根據主鍵進行查詢,查詢條件爲不等於,而且查詢到數據,主鍵字段產生表鎖。
begin; select * from goods where id <> 1 for update; commit;
十二、只根據主鍵進行查詢,查詢條件爲不等於,沒有查詢到數據,主鍵字段產生表鎖。
begin; select * from goods where id <> 1 for update; commit;
1三、只根據主鍵進行查詢,查詢條件爲 like,而且查詢到數據,主鍵字段產生表鎖。
begin; select * from goods where id like '1' for update; commit;
1四、只根據主鍵進行查詢,查詢條件爲 like,沒有查詢到數據,主鍵字段產生表鎖。
begin; select * from goods where id like '1' for update; commit;
測試環境
數據庫版本:5.1.48-community
數據庫引擎:InnoDB Supports transactions, row-level locking, and foreign keys
數據庫隔離策略:REPEATABLE-READ(系統、會話)
總結
一、InnoDB行鎖是經過給索引上的索引項加鎖來實現的,只有經過索引條件檢索數據,InnoDB才使用行級鎖,不然,InnoDB將使用表鎖。
二、因爲MySQL的行鎖是針對索引加的鎖,不是針對記錄加的鎖,因此雖然是訪問不一樣行的記錄,可是若是是使用相同的索引鍵,是會出現鎖衝突的。應用設計的時候要注意這一點。
三、當表有多個索引的時候,不一樣的事務可使用不一樣的索引鎖定不一樣的行,另外,不管是使用主鍵索引、惟一索引或普通索引,InnoDB都會使用行鎖來對數據加鎖。
四、即使在條件中使用了索引字段,可是否使用索引來檢索數據是由MySQL經過判斷不一樣執行計劃的代價來決定的,若是MySQL認爲全表掃描效率更高,好比對一些很小的表,它就不會使用索引,這種狀況下InnoDB將使用表鎖,而不是行鎖。所以,在分析鎖衝突時,別忘了檢查SQL的執行計劃,以確認是否真正使用了索引。
五、檢索值的數據類型與索引字段不一樣,雖然MySQL可以進行數據類型轉換,但卻不會使用索引,從而致使InnoDB使用表鎖。經過用explain檢查兩條SQL的執行計劃,咱們能夠清楚地看到了這一點。