MySQL InnoDB鎖機制

概述:

  鎖機制在程序中是最經常使用的機制之一,當一個程序須要多線程並行訪問同一資源時,爲了不一致性問題,一般採用鎖機制來處理。在數據庫的操做中也有相同的問題,當兩個線程同時對一條數據進行操做,爲了保證數據的一致性,就須要數據庫的鎖機制。每種數據庫的鎖機制都本身的實現方式,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容許對被鎖住的表進行增刪改查,但不容許對其餘表進行訪問。

  總結上面的結論

  1. Lock Tables....READ不會阻塞其餘線程對錶數據的讀取,會阻塞其餘線程對數據變動
  2. Lock Tables....WRITE會阻塞其餘線程對數據讀和寫
  3. Lock Tables....READ不容許對錶進行更新操做(新增、刪除也不行),而且不容許訪問未被鎖住的表
  4. 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鎖的時候,首先想到的都是行鎖,行鎖相比表鎖有一些優勢,行鎖比表鎖有更小鎖粒度,能夠更大的支持併發。可是加鎖動做也是須要額外開銷的,好比得到鎖、檢查鎖、釋放鎖等操做都是須要耗費系統資源。若是系統在鎖操做上浪費了太多時間,系統的性能就會受到比較大的影響。

  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;
Select * from test_product where price= 300 for UPDATE;

 

 

 

set autocommit=0;


BEGIN;
Select * from test_product where price=400 for UPDATE;

線程阻塞

COMMIT;  

 

     

  如上所示,若是正常鎖行的話,兩條線程鎖住不一樣行,不該該有衝突。咱們如今給price添加索引再試一次。     

ALTER TABLE `test_product` ADD INDEX idx_price ( `price` );

    

線程 A 線程 B

set autocommit=0;

BEGIN; 
Select * from test_product where price= 300 for UPDATE;

 

 

set autocommit=0;


BEGIN; 
Select * from test_product where price=400 for UPDATE;

 

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個線程,分別對經過樂觀鎖和悲觀鎖進行下單。

  悲觀鎖下單結果:

  樂觀鎖下單結果:

  售賣狀況以下:

  結果顯示樂觀鎖和悲觀鎖都能成功的防止產品超賣,上述的數據比較粗糙,不能表明實際生產中的一些狀況,可是在不少時候。使用樂觀鎖由於不須要對數據加鎖,防止鎖衝突,可能獲得更好的性能。可是也不表明樂觀鎖比悲觀鎖更好,仍是看具體的生產狀況,來判斷須要的是樂觀鎖仍是悲觀鎖。

相關文章
相關標籤/搜索