MySQL InnoDB鎖機制


概述:

  鎖機制在程序中是最常用的機制之一,當一個程序需要多線程並行訪問同一資源時,為了避免一致性問題,通常采用鎖機制來處理。在數據庫的操作中也有相同的問題,當兩個線程同時對一條數據進行操作,為了保證數據的一致性,就需要數據庫的鎖機制。每種數據庫的鎖機制都自己的實現方式,mysql作為一款工作中經常遇到的數據庫,它的鎖機制在面試中也經常會被問到。所以本文針對mysql數據庫,對其鎖機制進行總結。

  mysql的鎖可以分為服務層實現的鎖,例如Lock Tables、全局讀鎖、命名鎖、字符鎖,或者存儲引擎的鎖,例如行級鎖。InnoDB作為MySQL中最為常見的存儲引擎,本文默認MySQL選擇InnoDB作為存儲引擎,將MySQL的鎖和InnoDB實現的鎖同時進行討論。

  鎖的分類按照特性有多種分類,常見的比如顯式鎖和隱式鎖;表鎖和行鎖;共享鎖和排他鎖;樂觀鎖和悲觀鎖等等,后續會在下方補充概念。

服務級別鎖:

  表鎖

  表鎖可以是顯式也可以是隱式的。顯示的鎖用Lock Table來創建,但要記得Lock Table之后進行操作,需要在操作結束后,使用UnLock來釋放鎖。Lock Tables有read和write兩種,Lock Tables......Read通常被稱為共享鎖或者讀鎖,讀鎖或者共享鎖,是互相不阻塞的,多個用戶可以同一時間使用共享鎖互相不阻塞。Lock Table......write通常被稱為排他鎖或者寫鎖,寫鎖或者排他鎖會阻塞其他的讀鎖或者寫鎖,確保在給定時間里,只有一個用戶執行寫入,防止其他用戶讀取正在寫入的同一資源。

  為了進行測試,我們先創建兩張測試表,順便加幾條數據

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;

  第1、5行能夠執行成功,第2、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窗口中執行1、3、7成功。執行5、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個線程,分別對通過樂觀鎖和悲觀鎖進行下單。

  悲觀鎖下單結果:

  樂觀鎖下單結果:

  售賣情況如下:

  結果顯示樂觀鎖和悲觀鎖都能成功的防止產品超賣,上述的數據比較粗糙,不能代表實際生產中的一些情況,但是在很多時候。使用樂觀鎖因為不需要對數據加鎖,防止鎖沖突,可能得到更好的性能。但是也不代表樂觀鎖比悲觀鎖更好,還是看具體的生產情況,來判斷需要的是樂觀鎖還是悲觀鎖。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM