鎖機制在程序中是最經常使用的機制之一,當一個程序須要多線程並行訪問同一資源時,爲了不一致性問題,一般採用鎖機制來處理。在數據庫的操做中也有相同的問題,當兩個線程同時對一條數據進行操做,爲了保證數據的一致性,就須要數據庫的鎖機制。每種數據庫的鎖機制都本身的實現方式,mysql做爲一款工做中常常遇到的數據庫,它的鎖機制在面試中也常常會被問到。因此本文針對mysql數據庫,對其鎖機制進行總結。html
mysql的鎖能夠分爲服務層實現的鎖,例如Lock Tables、全局讀鎖、命名鎖、字符鎖,或者存儲引擎的鎖,例如行級鎖。InnoDB做爲MySQL中最爲常見的存儲引擎,本文默認MySQL選擇InnoDB做爲存儲引擎,將MySQL的鎖和InnoDB實現的鎖同時進行討論。java
鎖的分類按照特性有多種分類,常見的好比顯式鎖和隱式鎖;表鎖和行鎖;共享鎖和排他鎖;樂觀鎖和悲觀鎖等等,後續會在下方補充概念。mysql
表鎖面試
表鎖能夠是顯式也能夠是隱式的。顯示的鎖用Lock Table來建立,但要記得Lock Table以後進行操做,須要在操做結束後,使用UnLock來釋放鎖。Lock Tables有read和write兩種,Lock Tables......Read一般被稱爲共享鎖或者讀鎖,讀鎖或者共享鎖,是互相不阻塞的,多個用戶能夠同一時間使用共享鎖互相不阻塞。Lock Table......write一般被稱爲排他鎖或者寫鎖,寫鎖或者排他鎖會阻塞其餘的讀鎖或者寫鎖,確保在給定時間裏,只有一個用戶執行寫入,防止其餘用戶讀取正在寫入的同一資源。算法
爲了進行測試,咱們先建立兩張測試表,順便加幾條數據sql
CREATE TABLE `test_product` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `code` varchar(255) DEFAULT NULL, `name` varchar(255) DEFAULT NULL, `price` decimal(10,2) DEFAULT NULL, `quantity` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; CREATE TABLE `test_user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `age` int(3) DEFAULT NULL, `gender` int(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; INSERT INTO `test_user` (`id`, `name`, `age`, `gender`) VALUES ('1', '張三', '16', '1'); INSERT INTO `test_user` (`id`, `name`, `age`, `gender`) VALUES ('2', '李四', '18', '1'); INSERT INTO `test_product` (`id`, `code`, `name`, `price`, `quantity`) VALUES ('1', 'S001', '產品1號', '100.00', '200'); INSERT INTO `test_product` (`id`, `code`, `name`, `price`, `quantity`) VALUES ('2', 'S001', '產品2號', '200.00', '200'); INSERT INTO `locktest`.`test_product` (`code`, `name`, `price`, `quantity`) VALUES ('S003', '產品3號', '300.00', 300); INSERT INTO `locktest`.`test_product` (`code`, `name`, `price`, `quantity`) VALUES ('S004', '產品4號', '400.00', 400); INSERT INTO `locktest`.`test_product` (`code`, `name`, `price`, `quantity`) VALUES ('S005', '產品5號', '500.00', 500);
打開兩個客戶端鏈接A和B,在A中輸入數據庫
LOCK TABLES test_product READ;
在B中輸入安全
SELECT * FROM test_product
B能正常查詢並獲取到結果。Lock Tables....Read不會阻塞其餘線程對錶數據的讀取。 服務器
讓A繼續保留鎖,在B中輸入多線程
update test_product set price=250 where id=2;
此時B的線程被阻塞,等待A釋放鎖。
釋放A持有的鎖,在A中輸入
UNLOCK TABLES;
此時B中顯示下圖,而且數據已經被變動。
Lock Tables....Read會阻塞其餘線程對數據變動。
接下來再對Lock Table....write進行測試,在A線程下執行如下語句,用排它鎖鎖定test_product。
LOCK TABLES test_product WRITE;
在B中輸入如下語句,對test_product表進行查詢。
SELECT * FROM test_product;
發現B的查詢語句阻塞,等待A釋放鎖。再開啓一個新命令窗口C,輸入
update test_product set price=250 where id=2;
一樣被阻塞。在A中使用UNLOCK釋放鎖,B、C成功執行。Lock Tables....Write會阻塞其餘線程對數據讀和寫。
假設在A中進行給test_product加讀鎖後,對test_product進行更新或者對test_user進行讀取更新會怎麼樣呢。
LOCK TABLES test_product READ;
以後在A中進行test_product更新
update test_product set price=250 where id=2; [SQL]update test_product set price=250 where id=2; [Err] 1099 - Table 'test_product' was locked with a READ lock and can't be updated
而後在A中讀取test_user
[SQL]SELECT * from test_user [Err] 1100 - Table 'test_user' was not locked with LOCK TABLES
Lock Tables....Read不容許對錶進行更新操做(新增、刪除也不行),而且不容許訪問未被鎖住的表。
對Lock Table....WRITE進行相同的實驗,代碼類似,就再也不貼出。
Lock Tables....WRITE容許對被鎖住的表進行增刪改查,但不容許對其餘表進行訪問。
總結上面的結論:
lock tables主要性質如上所述,當咱們要去查詢mysql是否存在lock tables鎖狀態能夠用下面語句進行查詢。第二條能夠直接看到被鎖的表。也能夠經過show process來查看部分信息。
LOCK TABLES test_product READ,test_user WRITE; show status like "%lock%"; show OPEN TABLES where In_use > 0;
使用LOCK TABLES時候必須當心,《高性能MySQL》中有一段話:
LOCK TABLES和事務之間相互影響的話,狀況會變得很是複雜,在某些MySQL版本中甚至會產生沒法預料的結果。所以,本書建議,除了事務中禁用了AUTOCOMMIT,可使用LOCK TABLES以外,其餘任什麼時候候都不要顯示地執行LOCK TABLES,無論使用什麼存儲引擎。
因此在大部分時候,咱們不須要使用到LOCK TABLE關鍵字。
全局讀鎖
全局鎖能夠經過FLUSH TABLES WITH READ LOCK獲取單個全局讀鎖,與任務表鎖都衝突。解鎖的方式也是UNLOCK TABLES。一樣設置A、B兩個命令窗口,咱們對全局鎖進行測試。
在A中獲取全局讀鎖
FLUSH TABLES WITH READ LOCK;
而後在A窗口依次作如下實驗
1 LOCK TABLES test_user READ; 2 3 LOCK TABLES test_user WRITE; 4 5 SELECT * from test_user; 6 7 update test_product set price=250 where id=1;
第一、5行可以執行成功,第二、7行執行會失敗
在B中執行
1 FLUSH TABLES WITH READ LOCK; 2 3 LOCK TABLES test_user READ; 4 5 LOCK TABLES test_user WRITE; 6 7 SELECT * FROM test_product; 8 9 update test_product set price=250 where id=2;
B窗口中執行一、三、7成功。執行五、9失敗。
全局讀鎖其實就至關於用讀鎖同時鎖住全部表。若是當前線程擁有某個表的寫鎖,則獲取全局寫鎖的時候會報錯。若是其餘線程擁有某張表的寫鎖,則全局讀鎖會阻塞等待其餘表釋放寫鎖。
該命令是比較重量級的命令,會阻塞一切更新操做(表的增刪改和數據的增刪改),主要用於數據庫備份的時候獲取一致性數據。
命名鎖
命名鎖是一種表鎖,服務器建立或者刪除表的時候會建立一個命名鎖。若是一個線程LOCK TABLES,另外一個線程對被鎖定的表進行重命名,查詢會被掛起,經過show open tables能夠看到兩個名字(新名字和舊名字都被鎖住了)。
字符鎖
字符鎖是一種自定義鎖,經過SELECT GET_LOCK("xxx",60)來加鎖 ,經過release_lock()解鎖。假設A線程執行get_lock("xxx",60)後執行sql語句返回結果爲1表示拿到鎖,B線程一樣經過get_lock("xxx",60)獲取相同的字符鎖,則B線程會處理阻塞等待的情況,若是60秒內A線程沒有將鎖釋放,B線程獲取鎖超時就會返回0,表示未拿到鎖。使用get_lock()方法獲取鎖,若是線程A調用了兩次get_lock(),釋放鎖的時候也須要使用兩次release_lock()來進行解鎖。
InnoDB存儲引擎在也實現了本身的數據庫鎖。通常談到InnoDB鎖的時候,首先想到的都是行鎖,行鎖相比表鎖有一些優勢,行鎖比表鎖有更小鎖粒度,能夠更大的支持併發。可是加鎖動做也是須要額外開銷的,好比得到鎖、檢查鎖、釋放鎖等操做都是須要耗費系統資源。若是系統在鎖操做上浪費了太多時間,系統的性能就會受到比較大的影響。
InnoDB實現的行鎖有共享鎖(S)和排它鎖(X)兩種
共享鎖:容許事務去讀一行,阻止其餘事務對該數據進行修改
排它鎖:容許事務去讀取更新數據,阻止其餘事務對數據進行查詢或者修改
行鎖雖然很贊,可是還有一個問題,若是一個事務對一張表的某條數據進行加鎖,這個時候若是有另一個線程想要用LOCK TABLES進行鎖表,這時候數據庫要怎麼知道哪張表的哪條數據被加了鎖,一張張表一條條數據去遍歷是不可行的。InnoDB考慮到這種狀況,設計出另一組鎖,意向共享鎖(IS)和意向排他鎖(IX)。
意向共享鎖:當一個事務要給一條數據加S鎖的時候,會先對數據所在的表先加上IS鎖,成功後才能加上S鎖
意向排它鎖:當一個事務要給一條數據加X鎖的時候,會先對數據所在的表先加上IX鎖,成功後才能加上X鎖
意向鎖之間兼容,不會阻塞。可是會跟S鎖和X鎖衝突,衝突的方式跟讀寫鎖相同。例如當一張表上已經有一個排它鎖(X鎖),此時若是另一個線程要對該表加意向鎖,無論意向共享鎖仍是意向排他鎖都不會成功。
線程 A | 線程 B |
BEGIN;
SELECT * FROM test_product for UPDATE; |
|
SELECT * FROM test_product LOCK IN SHARE MODE; 結果:線程阻塞
SELECT * FROM test_product for UPDATE; 結果:線程阻塞 |
|
COMMIT; | ![]() |
上面的例子中,用的兩個加鎖方式,一個是SELECT........FOR UPDATE,SELECT........LOCK IN SHARE MODE。SELECT FOR UPDATE能爲數據添加排他鎖,LOCK IN SHARE MODE爲數據添加共享鎖。這兩種鎖,在事務中生效,而當事務提交或者回滾的時候,會自動釋放鎖。遺憾的是,當咱們在項目中遇到鎖等待的時候,並無辦法知道是哪一個線程正在持有鎖,也很難肯定是哪一個事務致使問題。可是咱們能夠經過這幾個表來確認消息Information_schema.processList、Information_schema.innodb_lock_waits、Information_schema.innodb_trx、Information_schema.innodb_locks來獲取事務等待的情況,根據片面的鎖等待情況來獲取具體的數據庫信息。
隱式加鎖:SELECT FOR UPDATE和LOCK IN SHARE 這種經過編寫在mysql裏面的方式對須要保護的數據進行加鎖的方式稱爲是顯式加鎖。還有一種加鎖方式是隱式加鎖,除了把事務設置成串行時,會對SELECT到的全部數據加鎖外,SELECT不會對數據加鎖(依賴於MVCC)。當執行update、delete、insert的時候會對數據進行加排它鎖。
自增加鎖:mysql數據庫在不少時候都會設置爲主鍵自增,若是這個時候使用表鎖,當事務比較大的時候,會對性能形成比較大的影響。mysql提供了inodb_atuoinc_lock_mode來處理自增加的安全問題。該參數能夠設置爲0(插入完成以後,即便事務沒結束也當即釋放鎖)、1(在判斷出自增加須要使用的數字後就當即釋放鎖,事務回滾也會形成主鍵不連續)、2(來一個記錄就分配一個值,不使用鎖,性能很好,可是可能致使主鍵不連續)。
外鍵鎖: 當插入和更新子表的時候,首先須要檢查父表中的記錄,並對附表加一條lock in share mode,而這可能會對兩張表的數據檢索形成阻塞。因此通常生產數據庫上不建議使用外鍵。
索引和鎖:InnoDB在給行添加鎖的時候,實際上是經過索引來添加鎖,若是查詢並無用到索引,就會使用表鎖。作個測試
線程 A | 線程 B |
set autocommit=0; BEGIN;
|
|
set autocommit=0;
線程阻塞 |
|
COMMIT; | ![]()
|
如上所示,若是正常鎖行的話,兩條線程鎖住不一樣行,不該該有衝突。咱們如今給price添加索引再試一次。
ALTER TABLE `test_product` ADD INDEX idx_price ( `price` );
線程 A | 線程 B |
set autocommit=0; BEGIN; |
|
set autocommit=0;
|
|
Select * from test_product where price= 300 for UPDATE; 阻塞 |
添加索引之後會發現,線程A、B查詢不一樣的行的時候,兩個線程並無相互阻塞。可是,即便InnoDB中已經使用了索引,仍然有可能鎖住一些不須要的數據。若是不能使用索引查找,InnoDB將會鎖住全部行。由於InnoDB中用索引來鎖行的方式比較複雜,其中牽涉到InnoDB的鎖算法和事務級別,這個後續會講到。
《高性能MySQL》中有一句話:"InnoDB在二級索引上使用共享鎖,但訪問主鍵索引須要排他鎖,這消除了覆蓋索引的可能性,而且使得SELECT FOR UPDATE 比Lock IN SHARE LOCK 或非鎖定查詢要慢不少"。除了上面那句話還有一句話有必要斟酌,"select for update,lock in share mode這兩個提示會致使某些優化器沒法使用,好比覆蓋索引,這些鎖定常常會被濫用,很容易形成服務器的鎖爭用問題,實際上應該儘可能避免使用這兩個提示,一般都有更好的方式能夠實現一樣的目的。
鎖算法:InnoDB的行鎖的算法爲如下三種
Record Lock:單挑記錄上的鎖
Gap Lock:間隙鎖,鎖定一個範圍,但不包括記錄自己
Next-Key Lock:Record Lock+Gap Lock,鎖定一個範圍,而且鎖定記錄自己
InnoDB會根據不一樣的事務隔離級別來使用不一樣的算法。網上關於InnoDB不一樣的事務隔離級別下的鎖的觀點各不一致,有些甚至和MVCC混淆,這一塊有時間再進行整理。能夠去官網上詳細瞭解一下,Mysql官網對InnoDB的事務鎖的介紹。
MVCC:多版本控制,InnoDB實現MVCC是經過在每行記錄後面保存兩個隱藏的列來實現,一個保存建立的事務版本號,一個保存的是刪除的事務版本號。MVCC只有在REPEATABLE READ 和 READ COMMITED兩個隔離級別下工做。另外兩個隔離級別與MVCC並不兼容,由於READ UNCOMMITED老是讀取最新數據,跟事務版本無關,而SERIALIZABLE會對讀取的全部行都進行加鎖。
悲觀鎖:指悲觀的認爲,須要訪問的數據隨時可能被其餘人訪問或者修改。所以在訪問數據以前,對要訪問的數據加鎖,不容許其餘其餘人對數據進行訪問或者修改。上述講到的服務器鎖和InnoDB鎖都屬於悲觀鎖。
樂觀鎖:指樂觀的認爲要訪問的數據不會被人修改。所以不對數據進行加鎖,若是操做的時候發現已經失敗了,則從新獲取數據進行更新(如CAS),或者直接返回操做失敗。
電商賣東西的時候,必須解決的是超賣的問題,超賣是指商品的數量好比只有5件,結果賣出去6件的狀況。咱們用代碼來演示一下怎麼用樂觀鎖和悲觀鎖解決這個問題。假設test_prodcut表中,S001和S002的產品各有100件。
@Service public class ProductService implements IProductService { @Resource private ProductMapper productMapper; private static final String product_code = "S001"; private static final String product_code1 = "S002"; //樂觀鎖下單成功數 private final AtomicInteger optimisticSuccess = new AtomicInteger(0); //樂觀鎖下單失敗數 private final AtomicInteger optimisticFalse = new AtomicInteger(0); //悲觀鎖下單成功數 private final AtomicInteger pessimisticSuccess = new AtomicInteger(0); //悲觀鎖下單失敗數 private final AtomicInteger pessimisticFalse = new AtomicInteger(0); //樂觀鎖下單 @Override @Transactional(rollbackFor = Exception.class) public void orderProductOptimistic() throws TestException { int num = productMapper.queryProductNumByCode(product_code); if (num <= 0) { optimisticFalse.incrementAndGet(); return; } int result = productMapper.updateOrderQuantityOptimistic(product_code); if (result == 0) { optimisticFalse.incrementAndGet(); throw new TestException("商品已經賣完"); } optimisticSuccess.incrementAndGet(); } //獲取售賣記錄 @Override public String getStatistics() { return "optimisticSuccess:" + optimisticSuccess + ", optimisticFalse:" + optimisticFalse + ",pessimisticSuccess:" + pessimisticSuccess + ", pessimisticFalse:" + pessimisticFalse; } //悲觀鎖下單 @Override @Transactional(rollbackFor = Exception.class) public void orderProductPessimistic() { int num = productMapper.queryProductNumByCodeForUpdate(product_code1); if (num <= 0) { pessimisticFalse.incrementAndGet(); return; } productMapper.updateOrderQuantityPessimistic(product_code1); pessimisticSuccess.incrementAndGet(); } //獲取產品詳情 @Override @Transactional public ProductResutl getProductDetail() { Random random = new Random(); String code = random.nextInt() % 2 == 0 ? product_code : product_code1; ProductResutl productResutl = productMapper.selectProductDetail(code); return productResutl; } //清楚記錄 @Override public void clearStatistics() { optimisticSuccess.set(0); optimisticFalse.set(0); pessimisticSuccess.set(0); pessimisticFalse.set(0); } }
對應sql以下。
1 <update id="updateOrderQuantityPessimistic"> 2 update test_product set quantity=quantity-1 where code=#{productCode} 3 </update> 4 5 <update id="updateOrderQuantityOptimistic"> 6 update test_product set quantity=quantity-1 where code=#{productCode} and quantity>0; 7 </update> 8 9 <select id="queryProductNumByCode" resultType="java.lang.Integer"> 10 SELECT quantity From test_product WHERE code=#{productCode} 11 </select> 12 13 14 <select id="queryProductNumByCodeForUpdate" resultType="java.lang.Integer"> 15 SELECT quantity From test_product WHERE code=#{productCode} for update 16 </select> 17 18 <select id="selectProductDetail" resultType="com.chinaredstar.jc.crawler.biz.result.product.ProductResutl"> 19 SELECT 20 id as id, 21 code as code, 22 name as name, 23 price as price, 24 quantity as quantity 25 FROM test_product WHERE code=#{productCode} 26 </select>
測試工具使用JMeter,開啓200個線程,分別對經過樂觀鎖和悲觀鎖進行下單。
悲觀鎖下單結果:
樂觀鎖下單結果:
售賣狀況以下:
結果顯示樂觀鎖和悲觀鎖都能成功的防止產品超賣,上述的數據比較粗糙,不能表明實際生產中的一些狀況,可是在不少時候。使用樂觀鎖由於不須要對數據加鎖,防止鎖衝突,可能獲得更好的性能。可是也不表明樂觀鎖比悲觀鎖更好,仍是看具體的生產狀況,來判斷須要的是樂觀鎖仍是悲觀鎖。