秒殺超賣 解決方案(史上最全)

文章很長,並且持續更新,建議收藏起來,慢慢讀! Java 高併發 發燒友社羣:瘋狂創客圈(總入口) 奉上如下珍貴的學習資源:html


推薦:入大廠 、作架構、大力提高Java 內功 的 精彩博文

入大廠 、作架構、大力提高Java 內功 必備的精彩博文 2021 秋招漲薪1W + 必備的精彩博文
1:Redis 分佈式鎖 (圖解-秒懂-史上最全) 2:Zookeeper 分佈式鎖 (圖解-秒懂-史上最全)
3: Redis與MySQL雙寫一致性如何保證? (面試必備) 4: 面試必備:秒殺超賣 解決方案 (史上最全)
5:面試必備之:Reactor模式 6: 10分鐘看懂, Java NIO 底層原理
7:TCP/IP(圖解+秒懂+史上最全) 8:Feign原理 (圖解)
9:DNS圖解(秒懂 + 史上最全 + 高薪必備) 10:CDN圖解(秒懂 + 史上最全 + 高薪必備)
10: 分佈式事務( 圖解 + 史上最全 + 吐血推薦 )

Java 面試題 30個專題 , 史上最全 , 面試必刷 阿里、京東、美團... 隨意挑、橫着走!!!
1: JVM面試題(史上最強、持續更新、吐血推薦) 2:Java基礎面試題(史上最全、持續更新、吐血推薦
3:架構設計面試題 (史上最全、持續更新、吐血推薦) 4:設計模式面試題 (史上最全、持續更新、吐血推薦)
1七、分佈式事務面試題 (史上最全、持續更新、吐血推薦) 一致性協議 (史上最全)
2九、多線程面試題(史上最全) 30、HR面經,過五關斬六將後,當心陰溝翻船!
9.網絡協議面試題(史上最全、持續更新、吐血推薦) 更多專題, 請參見【 瘋狂創客圈 高併發 總目錄

SpringCloud 精彩博文
nacos 實戰(史上最全) sentinel (史上最全+入門教程)
SpringCloud gateway (史上最全) 更多專題, 請參見【 瘋狂創客圈 高併發 總目錄

前言

先來就庫存超賣的問題做描述:通常電子商務網站都會遇到如團購、秒殺、特價之類的活動,而這樣的活動有一個共同的特色就是訪問量激增、上千甚至上萬人搶購一個商品。然而,做爲活動商品,庫存確定是頗有限的,如何控制庫存不讓出現超買,以防止形成沒必要要的損失是衆多電子商務網站程序員頭疼的問題,這同時也是最基本的問題。前端

在秒殺系統設計中,超賣是一個經典、常見的問題,任何商品都會有數量上限,如何避免成功下訂單買到商品的人數不超過商品數量的上限,這是每一個搶購活動都要面臨的難點。java

1、問題描述

在多個用戶同時發起對同一個商品的下單請求時,先查詢商品庫存,再修改商品庫存,會出現資源競爭問題,致使庫存的最終結果出現異常。問題:
當商品A一共有庫存15件,用戶甲先下單10件,用戶乙下單8件,這時候庫存只能知足一我的下單成功,若是兩我的同時提交,就出現了超賣的問題。python

在這裏插入圖片描述

2、解決的三種方案

  • 悲觀鎖

經過悲觀鎖解決超賣mysql

  • 樂觀鎖

經過樂觀鎖解決超賣程序員

  • 分段執行的排隊方案

經過分段執行的排隊方案解決超賣面試

解決方案1: 悲觀鎖

當查詢某條記錄時,即讓數據庫爲該記錄加鎖,鎖住記錄後別人沒法操做,使用相似以下語法:算法

beginTranse(開啓事務)
 
try{
 
    query('select amount from s_store where goodID = 12345');
 
    if(庫存 > 0){
 
        //quantity爲請求減掉的庫存數量
 
        query('update s_store set amount = amount - quantity where goodID = 12345');
 
    }
 
}catch( Exception e ){
 
    rollBack(回滾)
 
}
 
commit(提交事務)

問題:sql

注意,上面的代碼容易出現死鎖,採用很少。數據庫

有社羣小夥伴,對死鎖的的緣由比較關心,這裏簡單分析一下。

上面的語句,可能出現死鎖的簡單的緣由,在事務的隔離級別爲Serializable時,假設事務t1經過 select拿到了共享鎖,而其餘事務若是拿到了 排他鎖,此時t1 去拿排他鎖的時候, 就有可能會出現死鎖,注意,這裏是可能,並非必定。實際的緣由,與事務的隔離級別和語句的複雜度,都有關係。

總之,避免死鎖的方式之一(稍後介紹):爲了在單個 InnoDB 表上執行多個併發寫入操做時避免死鎖,能夠在事務開始時經過爲預期要修改的每一個元祖(行)使用 SELECT … FOR UPDATE 語句來獲取必要的鎖,即便這些行的更改語句是在以後才執行的。

解決方案:通常提早採用 select for update,提早加上寫鎖。

beginTranse(開啓事務)
 
try{
 
    query('select amount from s_store where goodID = 12345   for update');
 
    if(庫存 > 0){
 
        //quantity爲請求減掉的庫存數量
 
        query('update s_store set amount = amount - quantity where goodID = 12345');
 
    }
 
}catch( Exception e ){
 
    rollBack(回滾)
 
}
 
commit(提交事務)

行鎖和表鎖

行鎖:分爲 共享鎖 和 排它鎖。

共享鎖又稱:讀鎖。當一個事務對某幾行上讀鎖時,容許其餘事務對這幾行進行讀操做,但不容許其進行寫操做,也不容許其餘事務給這幾行上排它鎖,但容許上讀鎖。

上共享鎖的寫法:lock in share mode

例如: select math from zje where math>60 lock in share mode;

排它鎖又稱:寫鎖。當一個事務對某幾個上寫鎖時,不容許其餘事務寫,但容許讀。更不容許其餘事務給這幾行上任何鎖。包括寫鎖。

上排它鎖的寫法:for update

例如:select math from zje where math >60 for update;

死鎖

死鎖:例如說兩個事務,事務A鎖住了15行,同時事務B鎖住了610行,此時事務A請求鎖住610行,就會阻塞直到事務B施放610行的鎖,而隨後事務B又請求鎖住15行,事務B也阻塞直到事務A釋放15行的鎖。死鎖發生時,會產生Deadlock錯誤。

表鎖:不會出現死鎖,發生鎖衝突概率高,併發低。

表鎖是對錶操做的,因此天然鎖住全表的表鎖就不會出現死鎖。可是表鎖效率低。

行鎖:會出現死鎖,發生鎖衝突概率低,併發高。

3.行鎖的要點

注意幾點:

1.行鎖必須有索引才能實現,不然會自動鎖全表,那麼就不是行鎖了。

2.兩個事務不能鎖同一個索引,例如:

事務A先執行:
select math from zje where math>60 for update;
 
事務B再執行:
select math from zje where math<60 for update;
這樣的話,事務B是會阻塞的。若是事務B把 math索引換成其餘索引就不會阻塞,
但注意,換成其餘索引鎖住的行不能和math索引鎖住的行有重複。

3.insert ,delete , update在事務中都會自動默認加上排它鎖。

實現:

會話1: 會話2:
begin;
select math from zje where math>60 for update;
begin;
update zje set math=99 where math=68;
阻塞

MyISAM與InnoDB 的區別

MyISAM:MyISAM是默認存儲引擎(Mysql5.1前),每一個MyISAM在磁盤上存儲成三個文件,每個文件的名字均以表的名字開始,擴展名指出文件類型。

​ .frm文件存儲表定義

​ ·MYD (MYData)文件存儲表的數據

​ .MYI (MYIndex)文件存儲表的索引

InnoDB:MySQL的默認存儲引擎,給 MySQL 提供了具備事務(transaction)、回滾(rollback)和崩潰修復能力(crash recovery capabilities)、多版本併發控制(multi-versioned concurrency control)的事務安全(transaction-safe (ACID compliant))型表。InnoDB 提供了行級鎖(locking on row level),提供與 Oracle 相似的不加鎖讀取(non-locking read in SELECTs)。

MyISAM與InnoDB 的區別

  1. InnoDB支持事務,MyISAM不支持,對於InnoDB每一條SQL語言都默認封裝成事務,自動提交,這樣會影響速度,因此最好把多條SQL語言放在begin和commit之間,組成一個事務;

  2. InnoDB支持外鍵,而MyISAM不支持。對一個包含外鍵的InnoDB錶轉爲MYISAM會失敗;

  3. 彙集索引 VS 非彙集索引

    InnoDB是彙集索引,使用B+Tree做爲索引結構,數據文件是和(主鍵)索引綁在一塊兒的(表數據文件自己就是按B+Tree組織的一個索引結構),必需要有主鍵,經過主鍵索引效率很高。可是輔助索引須要兩次查詢,先查詢到主鍵,而後再經過主鍵查詢到數據。所以,主鍵不該該過大,由於主鍵太大,其餘索引也都會很大。

InnoDB的B+樹主鍵索引的葉子節點就是數據文件,輔助索引的葉子節點是主鍵的值

img

but, MyISAM是非彙集索引,也是使用B+Tree做爲索引結構,索引和數據文件是分離的,索引保存的是數據文件的指針。主鍵索引和輔助索引是獨立的。

img

總結

​ 也就是說:InnoDB的B+樹主鍵索引的葉子節點就是數據文件,輔助索引的葉子節點是主鍵的值;而MyISAM的B+樹主鍵索引和輔助索引的葉子節點都是數據文件的地址指針。

  1. InnoDB不保存表的具體行數,執行select count(*) from table時須要全表掃描。而MyISAM用一個變量保存了整個表的行數,執行上述語句時只須要讀出該變量便可,速度很快(注意不能加有任何WHERE條件);

那麼爲何InnoDB沒有了這個變量呢?

​ 由於InnoDB的事務特性,在同一時刻表中的行數對於不一樣的事務而言是不同的,所以count統計會計算對於當前事務而言能夠統計到的行數,而不是將總行數儲存起來方便快速查詢。InnoDB會嘗試遍歷一個儘量小的索引除非優化器提示使用別的索引。若是二級索引不存在,InnoDB還會嘗試去遍歷其餘聚簇索引。
​ 若是索引並無徹底處於InnoDB維護的緩衝區(Buffer Pool)中,count操做會比較費時。能夠創建一個記錄總行數的表並讓你的程序在INSERT/DELETE時更新對應的數據。和上面提到的問題同樣,若是此時存在多個事務的話這種方案也不太好用。若是獲得大體的行數值已經足夠知足需求能夠嘗試SHOW TABLE STATUS

  1. Innodb不支持全文索引,而MyISAM支持全文索引,在涉及全文索引領域的查詢效率上MyISAM速度更快高;PS:5.7之後的InnoDB支持全文索引了

  2. MyISAM表格能夠被壓縮後進行查詢操做

  3. InnoDB支持表、行(默認)級鎖,而MyISAM支持表級鎖

InnoDB的行鎖是實如今索引上的,而不是鎖在物理行記錄上。潛臺詞是,若是訪問沒有命中索引,也沒法使用行鎖,將要退化爲表鎖。

例如:

    t_user(uid, uname, age, sex) innodb;

    uid PK
    無其餘索引
    update t_user set age=10 where uid=1;             命中索引,行鎖。

    update t_user set age=10 where uid != 1;           未命中索引,表鎖。

    update t_user set age=10 where name='chackca';    無索引,表鎖。

八、InnoDB表必須有惟一索引(如主鍵)(用戶沒有指定的話會本身找/生產一個隱藏列Row_id來充當默認主鍵),而Myisam能夠沒有

九、Innodb存儲文件有frm、ibd,而Myisam是frm、MYD、MYI

​ Innodb:frm是表定義文件,ibd是數據文件

​ Myisam:frm是表定義文件,myd是數據文件,myi是索引文件

如何選擇:

​ 1. 是否要支持事務,若是要請選擇innodb,若是不須要能夠考慮MyISAM;

​ 2. 若是表中絕大多數都只是讀查詢,能夠考慮MyISAM,若是既有讀也有寫,請使用InnoDB。

​ 3. 系統奔潰後,MyISAM恢復起來更困難,可否接受;

​ 4. MySQL5.5版本開始Innodb已經成爲Mysql的默認引擎(以前是MyISAM),說明其優點是有目共睹的,若是你不知道用什麼,那就用InnoDB,至少不會差。

InnoDB爲何推薦使用自增ID做爲主鍵?

​ 答:自增ID能夠保證每次插入時B+索引是從右邊擴展的,能夠避免B+樹和頻繁合併和分裂(對比使用UUID)。若是使用字符串主鍵和隨機主鍵,會使得數據隨機插入,效率比較差。

innodb引擎的4大特性

​ 插入緩衝(insert buffer),二次寫(double write),自適應哈希索引(ahi),預讀(read ahead)

事務與死鎖

在MySQL的InnoDB中,預設的Tansaction isolation level 爲REPEATABLE READ(可重讀)

在SELECT 的讀取鎖定主要分爲兩種方式:

SELECT ... LOCK IN SHARE MODE

SELECT ... FOR UPDATE

這兩種方式在事務(Transaction) 進行當中SELECT 到同一個數據表時,都必須等待其它事務數據被提交(Commit)後纔會執行。

而主要的不一樣在於共享鎖(lock in share mode) 在有一方事務要Update 同一個表單時很容易形成死鎖。

簡單的說,若是SELECT 後面若要UPDATE 同一個表單,最好使用SELECT ... UPDATE。

MySQL SELECT ... FOR UPDATE 的Row Lock 與Table Lock

上面介紹過SELECT ... FOR UPDATE 的用法,不過鎖定(Lock)的數據是判別就得要注意一下了。因爲InnoDB 預設是Row-Level Lock,因此只有「明確」的指定主鍵,MySQL 纔會執行Row lock (只鎖住被選取的數據) ,不然MySQL 將會執行Table Lock (將整個數據表單給鎖住)。

舉個例子:

假設有個表單products ,裏面有id 跟name 二個欄位,id 是主鍵。

例1: (明確指定主鍵,而且有此數據,row lock)

SELECT * FROM products WHERE id='3' FOR UPDATE;

例2: (明確指定主鍵,若查無此數據,無lock)

SELECT * FROM products WHERE id='-1' FOR UPDATE;

例2: (無主鍵,table lock)

SELECT * FROM products WHERE name='Mouse' FOR UPDATE;

例3: (主鍵不明確,table lock)

SELECT * FROM products WHERE id<>'3' FOR UPDATE;

例4: (主鍵不明確,table lock)

SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;

淘寶是如何使用悲觀鎖的

那麼後端的數據庫在高併發和超賣下會遇到什麼問題呢?主要會有以下3個問題:(主要討論寫的問題,讀的問題經過增長cache能夠很容易的解決)

  I: 首先MySQL自身對於高併發的處理性能就會出現問題,通常來講,MySQL的處理性能會隨着併發thread上升而上升,可是到了必定的併發度以後會出現明顯的拐點,以後一路降低,最終甚至會比單thread的性能還要差。

  II: 其次,超賣的根結在於減庫存操做是一個事務操做,須要先select,而後insert,最後update -1。最後這個-1操做是不能出現負數的,可是當多用戶在有庫存的狀況下併發操做,出現負數這是沒法避免的。

  III:最後,當減庫存和高併發碰到一塊兒的時候,因爲操做的庫存數目在同一行,就會出現爭搶InnoDB行鎖的問題,致使出現互相等待甚至死鎖,從而大大下降MySQL的處理性能,最終致使前端頁面出現超時異常。

針對上述問題,如何解決呢? 咱們先看眼淘寶的高大上解決方案:

I: 關閉死鎖檢測,提升併發處理性能。

在一個高併發的MySQL服務器上,事務會遞歸檢測死鎖,當超過必定的深度時,性能的降低會變的不可接受。FACEBOOK早就提出了禁止死鎖檢測。

咱們作了一個實驗,在禁止死鎖檢測後,TPS獲得了極大的提高,以下圖所示:

img

禁止死鎖檢測後,即便死鎖發生,也不會回滾事務,而是所有等待到超時

Mysql 的 innobase_deadlock_check是在innodb裏新加的系統變量,用於控制是否打開死鎖檢測

死鎖是指兩個或兩個以上的進程在執行過程當中,因爭奪資源而形成的一種互相等待的現象,能夠認爲若是一個資源被鎖定,它總會在之後某個時間被釋放。而死鎖發生在當多個進程訪問同一數據庫時,其中每一個進程擁有的鎖都是其餘進程所需的,由此形成每一個進程都沒法繼續下去。
InnoDB的併發寫操做會觸發死鎖,InnoDB也提供了死鎖檢測機制,能夠經過設置innodb_deadlock_detect參數能夠打開或關閉死鎖檢測:

innodb_deadlock_detect = on 打開死鎖檢測,數據庫發生死鎖時自動回滾(默認選項)
innodb_deadlock_detect = off 關閉死鎖檢測,發生死鎖的時候,用鎖超時來處理,經過設置鎖超時參數innodb_lock_wait_timeout 能夠在超時發生時回滾被阻塞的事務

設置mysql 事務鎖超時時間 innodb_lock_wait_timeout

Mysql數據庫採用InnoDB模式,默認參數:innodb_lock_wait_timeout設置鎖等待的時間是50s,一旦數據庫鎖超過這個時間就會報錯。

mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50 |
+--------------------------+-------+
1 row in set (0.00 sec)

mysql> SET GLOBAL innodb_lock_wait_timeout=120;
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 120 |
+--------------------------+-------+
1 row in set (0.00 sec)

mysql>

設置InnoDB Monitors方法

還能夠經過設置InnDB Monitors來進一步觀察鎖衝突詳細信息

創建test庫

mysql>create database test;
Query OK, 1 row affected (0.20 sec)
mysql> use test
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A


Database changed
mysql> create table innodb_monitor(a INT) engine=innodb;
Query OK, 0 rows affected (1.04 sec)


mysql> create table innodb_tablespace_monitor(a INT) engine=innodb;
Query OK, 0 rows affected (0.70 sec)


mysql> create table innodb_lock_monitor(a INT) engine=innodb;
Query OK, 0 rows affected (0.36 sec)


mysql> create table innodb_table_monitor(a INT) engine=innodb;
Query OK, 0 rows affected (0.08 sec)

能夠經過show engine innodb status命令查看死鎖信息

mysql> show engine innodb status \G
*************************** 1. row ***************************
  Type: InnoDB
  Name: 
Status: 
=====================================
2018-05-10 09:17:10 0x7f1fbc21a700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 46 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 53 srv_active, 0 srv_shutdown, 240099 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 2007
OS WAIT ARRAY INFO: signal count 1987
RW-shared spins 3878, rounds 5594, OS waits 1735
RW-excl spins 3, rounds 91, OS waits 4
RW-sx spins 1, rounds 30, OS waits 1
Spin rounds per wait: 1.44 RW-shared, 30.33 RW-excl, 30.00 RW-sx
------------
TRANSACTIONS
------------
Trx id counter 78405
Purge done for trx's n:o < 78404 undo n:o < 10 state: running but idle
History list length 21
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421249967052640, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
--------
FILE I/O
--------
I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
.............................................................................
.............................................................................
.............................................................................

II:請求排隊

修改源代碼,將排隊提到進入引擎層前,下降引擎層面的併發度。

若是請求一股腦的涌入數據庫,勢必會因爲爭搶資源形成性能降低,經過排隊,讓請求從混沌到有序,從而避免數據庫在協調大量請求時過載。

請求排隊:若是請求一股腦的涌入數據庫,勢必會因爲爭搶資源形成性能降低,經過排隊,讓請求從混沌到有序,從而避免數據庫在協調大量請求時過載。

III:請求合併(組提交)

請求合併(組提交),下降server和引擎的交互次數,下降IO消耗。

甲買了一個商品,乙也買了同一個商品,與其把甲乙當作當作單獨的請求分別執行一次商品庫存減一的操做,不如把他們合併後統一執行一次商品庫存減二的操做,請求合併的越多,效率提高的就越大。

實操建議

不過結合咱們的實際,死鎖監測能夠關閉,可是,改mysql源碼這種高大上的解決方案顯然有那麼一點不切實際。

InnoDB鎖定模式及實現機制

考慮到行級鎖定均由各個存儲引擎自行實現,並且具體實現也各有差異,而InnoDB是目前事務型存儲引擎中使用最爲普遍的存儲引擎,因此這裏咱們就主要分析一下InnoDB的鎖定特性。
總的來講,InnoDB的鎖定機制和Oracle數據庫有很多類似之處。InnoDB的行級鎖定一樣分爲兩種類型,共享鎖和排他鎖,而在鎖定機制的實現過程當中爲了讓行級鎖定和表級鎖定共存,InnoDB也一樣使用了意向鎖(表級鎖定)的概念,也就有了意向共享鎖和意向排他鎖這兩種。
當一個事務須要給本身須要的某個資源加鎖的時候,若是遇到一個共享鎖正鎖定着本身須要的資源的時候,本身能夠再加一個共享鎖,不過不能加排他鎖。可是,若是遇到本身須要鎖定的資源已經被一個排他鎖佔有以後,則只能等待該鎖定釋放資源以後本身才能獲取鎖定資源並添加本身的鎖定。而意向鎖的做用就是當一個事務在須要獲取資源鎖定的時候,若是遇到本身須要的資源已經被排他鎖佔用的時候,該事務能夠須要鎖定行的表上面添加一個合適的意向鎖。若是本身須要一個共享鎖,那麼就在表上面添加一個意向共享鎖。而若是本身須要的是某行(或者某些行)上面添加一個排他鎖的話,則先在表上面添加一個意向排他鎖。意向共享鎖能夠同時並存多個,可是意向排他鎖同時只能有一個存在。

InnoDB的鎖定模式實際上能夠分爲四種:共享鎖(S),排他鎖(X),意向共享鎖(IS)和意向排他鎖(IX),咱們能夠經過如下表格來總結上面這四種所的共存邏輯關係:
img

若是一個事務請求的鎖模式與當前的鎖兼容,InnoDB就將請求的鎖授予該事務;反之,若是二者不兼容,該事務就要等待鎖釋放。

意向鎖是InnoDB自動加的,不需用戶干預。對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖(X);對於普通SELECT語句,InnoDB不會加任何鎖;事務能夠經過如下語句顯示給記錄集加共享鎖或排他鎖。

共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE

用SELECT ... IN SHARE MODE得到共享鎖,主要用在須要數據依存關係時來確認某行記錄是否存在,並確保沒有人對這個記錄進行UPDATE或者DELETE操做。

可是若是當前事務也須要對該記錄進行更新操做,則頗有可能形成死鎖,對於鎖定行記錄後須要進行更新操做的應用,應該使用SELECT... FOR UPDATE方式得到排他鎖。

間隙鎖(Next-Key鎖)

當咱們用範圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;
對於鍵值在條件範圍內但並不存在的記錄,叫作「間隙(GAP)」,InnoDB也會對這個「間隙」加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖)。
例:
假如emp表中只有101條記錄,其empid的值分別是 1,2,...,100,101,下面的SQL:

mysql> select * from emp where empid > 100 for update;

是一個範圍條件的檢索,InnoDB不只會對符合條件的empid值爲101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的「間隙」加鎖。
InnoDB使用間隙鎖的目的:
(1)防止幻讀,以知足相關隔離級別的要求。對於上面的例子,要是不使用間隙鎖,若是其餘事務插入了empid大於100的任何記錄,那麼本事務若是再次執行上述語句,就會發生幻讀;
(2)爲了知足其恢復和複製的須要。
很顯然,在使用範圍條件檢索並鎖定記錄時,即便某些不存在的鍵值也會被無辜的鎖定,而形成在鎖定的時候沒法插入鎖定鍵值範圍內的任何數據。在某些場景下這可能會對性能形成很大的危害。
除了間隙鎖給InnoDB帶來性能的負面影響以外,經過索引實現鎖定的方式還存在其餘幾個較大的性能隱患:
(1)當Query沒法利用索引的時候,InnoDB會放棄使用行級別鎖定而改用表級別的鎖定,形成併發性能的下降;
(2)當Query使用的索引並不包含全部過濾條件的時候,數據檢索使用到的索引鍵所只想的數據可能有部分並不屬於該Query的結果集的行列,可是也會被鎖定,由於間隙鎖鎖定的是一個範圍,而不是具體的索引鍵;
(3)當Query在使用索引定位數據的時候,若是使用的索引鍵同樣但訪問的數據行不一樣的時候(索引只是過濾條件的一部分),同樣會被鎖定。
所以,在實際應用開發中,尤爲是併發插入比較多的應用,咱們要儘可能優化業務邏輯,儘可能使用相等條件來訪問更新數據,避免使用範圍條件。
還要特別說明的是,InnoDB除了經過範圍條件加鎖時使用間隙鎖外,若是使用相等條件請求給一個不存在的記錄加鎖,InnoDB也會使用間隙鎖。

併發事務有什麼什麼問題?應該如何解決?

併發事務可能形成:髒讀、不可重複讀和幻讀等問題 ,這些問題其實都是數據庫讀一致性問題,必須由數據庫提供必定的事務隔離機制來解決,解決方案以下:

  • 加鎖:在讀取數據前,對其加鎖,阻止其餘事務對數據進行修改。
  • 提供數據多版本併發控制(MultiVersion Concurrency Control,簡稱 MVCC 或 MCC),也稱爲多版本數據庫:不用加任何鎖, 經過必定機制生成一個數據請求時間點的一致性數據快照(Snapshot), 並用這個快照來提供必定級別 (語句級或事務級) 的一致性讀取,從用戶的角度來看,好象是數據庫能夠提供同一數據的多個版本。

什麼是 MVCC?

MVCC 全稱是多版本併發控制系統,InnoDB 和 Falcon 存儲引擎經過多版本併發控制(MVCC,Multiversion Concurrency Control)機制解決幻讀問題。

MVCC 是怎麼工做的?

InnoDB 的 MVCC 是經過在每行記錄後面保存兩個隱藏的列來實現,這兩個列一個保存了行的建立時間,一個保存行的過時時間(刪除時間)。固然存儲的並非真實的時間而是系統版本號(system version number)。每開始一個新的事務,系統版本號都會自動新增,事務開始時刻的系統版本號會做爲事務的版本號,用來查詢到每行記錄的版本號進行比較。

REPEATABLE READ(可重讀)隔離級別下 MVCC 如何工做?

  • SELECT:InnoDB 會根據如下條件檢查每一行記錄:第一,InnoDB 只查找版本早於當前事務版本的數據行,這樣能夠確保事務讀取的行要麼是在開始事務以前已經存在要麼是事務自身插入或者修改過的。第二,行的刪除版本號要麼未定義,要麼大於當前事務版本號,這樣能夠確保事務讀取到的行在事務開始以前未被刪除。
  • INSERT:InnoDB 爲新插入的每一行保存當前系統版本號做爲行版本號。
  • DELETE:InnoDB 爲刪除的每一行保存當前系統版本號做爲行刪除標識。
  • UPDATE:InnoDB 爲插入的一行新紀錄保存當前系統版本號做爲行版本號,同時保存當前系統版本號到原來的行做爲刪除標識保存這兩個版本號,使大多數操做都不用加鎖。它不足之處是每行記錄都須要額外的存儲空間,須要作更多的行檢查工做和一些額外的維護工做。

快照讀和當前讀

在mysql中select分爲快照讀和當前讀,執行下面的語句

select * from table where id = ?;
執行的是快照讀,讀的是數據庫記錄的快照版本,是不加鎖的。(這種說法在隔離級別爲Serializable中不成立)

select加鎖分析

下面六句Sql的區別呢

select * from table where id = ?
select * from table where id < ?
select * from table where id = ? lock in share mode
select * from table where id < ? lock in share mode
select * from table where id = ? for update
select * from table where id < ? for update

在不一樣的事務隔離級別下,是否加鎖,加的是共享鎖仍是排他鎖,是否存在間隙鎖,您能說出來嘛?
要回答這個問題,先問本身三個問題

  • 當前事務隔離級別是什麼
  • id列是否存在索引
  • 若是存在索引是聚簇索引仍是非聚簇索引呢?

關於mysql的索引,囉嗦一下:

  • innodb必定存在聚簇索引,默認以主鍵做爲聚簇索引
  • 有幾個索引,就有幾棵B+樹(不考慮hash索引的情形)
  • 聚簇索引的葉子節點爲磁盤上的真實數據。非聚簇索引的葉子節點仍是索引,指向聚簇索引B+樹。

鎖類型

  • 共享鎖(S鎖):假設事務T1對數據A加上共享鎖,那麼事務T2能夠讀數據A,不能修改數據A。
  • 排他鎖(X鎖):假設事務T1對數據A加上共享鎖,那麼事務T2不能讀數據A,不能修改數據A。
    咱們經過update、delete等語句加上的鎖都是行級別的鎖。只有LOCK TABLE … READ和LOCK TABLE … WRITE才能申請表級別的鎖。
  • 意向共享鎖(IS鎖):一個事務在獲取(任何一行/或者全表)S鎖以前,必定會先在所在的表上加IS鎖。
  • 意向排他鎖(IX鎖):一個事務在獲取(任何一行/或者全表)X鎖以前,必定會先在所在的表上加IX鎖。

意向鎖存在的目的?

這裏說一下意向鎖存在的目的。假設事務T1,用X鎖來鎖住了表上的幾條記錄,那麼此時表上存在IX鎖,即意向排他鎖。那麼此時事務T2要進行LOCK TABLE … WRITE的表級別鎖的請求,能夠直接根據意向鎖是否存在而判斷是否有鎖衝突。

  • Record Locks:簡單翻譯爲行鎖吧。注意了,該鎖是對索引記錄進行加鎖!鎖是在加索引上而不是行上的。注意了,innodb必定存在聚簇索引,所以行鎖最終都會落到聚簇索引上!
  • Gap Locks:簡單翻譯爲間隙鎖,是對索引的間隙加鎖,其目的只有一個,防止其餘事物插入數據。在Read Committed隔離級別下,不會使用間隙鎖。

這裏我對官網補充一下,隔離級別比Read Committed低的狀況下,也不會使用間隙鎖,如隔離級別爲Read Uncommited時,也不存在間隙鎖。當隔離級別爲Repeatable Read和Serializable時,就會存在間隙鎖。

  • Next-Key Locks:這個理解爲Record Lock + 索引前面的Gap Lock。記住了,鎖住的是索引前面的間隙!好比一個索引包含值,10,11,13和20。那麼,間隙鎖的範圍以下

(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)

索引原理介紹

先來一張帶主鍵的表,以下所示,pId是主鍵

pId name birthday
5 zhangsan 2016-10-02
8 lisi 2015-10-04
11 wangwu 2016-09-02
13 zhaoliu 2015-10-07

畫出該表的結構圖以下
image

如上圖所示,分爲上下兩個部分,上半部分是由主鍵造成的B+樹,下半部分就是磁盤上真實的數據!那麼,當咱們, 執行下面的語句

select * from table where pId='11'

那麼,執行過程以下
image
如上圖所示,從根開始,通過3次查找,就能夠找到真實數據。若是不使用索引,那就要在磁盤上,進行逐行掃描,直到找到數據位置。顯然,使用索引速度會快。可是在寫入數據的時候,須要維護這顆B+樹的結構,所以寫入性能會降低!

聚簇索引、非聚簇索引

聚簇索引:將數據存儲與索引放到了一塊,索引結構的葉子節點保存了行數據

非聚簇索引:將數據與索引分開存儲,索引結構的葉子節點指向了數據對應的位置

在innodb中,在聚簇索引之上建立的索引稱之爲輔助索引,非聚簇索引都是輔助索引,像複合索引、前綴索引、惟一索引。輔助索引葉子節點存儲的再也不是行的物理位置,而是主鍵值,輔助索引訪問數據老是須要二次查找

img

  1. InnoDB使用的是聚簇索引,將主鍵組織到一棵B+樹中,而行數據就儲存在葉子節點上,若使用"where id = 14"這樣的條件查找主鍵,則按照B+樹的檢索算法便可查找到對應的葉節點,以後得到行數據。
  2. 若對Name列進行條件搜索,則須要兩個步驟:第一步在輔助索引B+樹中檢索Name,到達其葉子節點獲取對應的主鍵。第二步使用主鍵在主索引B+樹種再執行一次B+樹檢索操做,最終到達葉子節點便可獲取整行數據。(重點在於經過其餘鍵須要創建輔助索引)

聚簇索引具備惟一性,因爲聚簇索引是將數據跟索引結構放到一塊,所以一個表僅有一個聚簇索引。

表中行的物理順序和索引中行的物理順序是相同的在建立任何非聚簇索引以前建立聚簇索引,這是由於聚簇索引改變了表中行的物理順序,數據行 按照必定的順序排列,而且自動維護這個順序;

聚簇索引默認是主鍵,若是表中沒有定義主鍵,InnoDB 會選擇一個惟一且非空的索引代替。若是沒有這樣的索引,InnoDB 會隱式定義一個主鍵(相似oracle中的RowId)來做爲聚簇索引。若是已經設置了主鍵爲聚簇索引又但願再單獨設置聚簇索引,必須先刪除主鍵,而後添加咱們想要的聚簇索引,最後恢復設置主鍵便可。

MyISAM使用的是非聚簇索引,非聚簇索引的兩棵B+樹看上去沒什麼不一樣,節點的結構徹底一致只是存儲的內容不一樣而已,主鍵索引B+樹的節點存儲了主鍵,輔助鍵索引B+樹存儲了輔助鍵。表數據存儲在獨立的地方,這兩顆B+樹的葉子節點都使用一個地址指向真正的表數據,對於表數據來講,這兩個鍵沒有任何差異。因爲索引樹是獨立的,經過輔助鍵檢索無需訪問主鍵的索引樹

img

使用聚簇索引的優點:

每次使用輔助索引檢索都要通過兩次B+樹查找,看上去聚簇索引的效率明顯要低於非聚簇索引,這不是畫蛇添足嗎?聚簇索引的優點在哪?

1.因爲行數據和聚簇索引的葉子節點存儲在一塊兒,同一頁中會有多條行數據,訪問同一數據頁不一樣行記錄時,已經把頁加載到了Buffer中(緩存器),再次訪問時,會在內存中完成訪問,沒必要訪問磁盤。這樣主鍵和行數據是一塊兒被載入內存的,找到葉子節點就能夠馬上將行數據返回了,若是按照主鍵Id來組織數據,得到數據更快。

2.輔助索引的葉子節點,存儲主鍵值,而不是數據的存放地址。好處是當行數據放生變化時,索引樹的節點也須要分裂變化;或者是咱們須要查找的數據,在上一次IO讀寫的緩存中沒有,須要發生一次新的IO操做時,能夠避免對輔助索引的維護工做,只須要維護聚簇索引樹就行了。另外一個好處是,由於輔助索引存放的是主鍵值,減小了輔助索引佔用的存儲空間大小。

注:咱們知道一次io讀寫,能夠獲取到16K大小的資源,咱們稱之爲讀取到的數據區域爲Page。而咱們的B樹,B+樹的索引結構,葉子節點上存放好多個關鍵字(索引值)和對應的數據,都會在一次IO操做中被讀取到緩存中,因此在訪問同一個頁中的不一樣記錄時,會在內存裏操做,而不用再次進行IO操做了。除非發生了頁的分裂,即要查詢的行數據不在上次IO操做的換村裏,纔會觸發新的IO操做。

3.由於MyISAM的主索引並不是聚簇索引,那麼他的數據的物理地址必然是凌亂的,拿到這些物理地址,按照合適的算法進行I/O讀取,因而開始不停的尋道不停的旋轉。聚簇索引則只需一次I/O。(強烈的對比)

4.不過,若是涉及到大數據量的排序、全表掃描、count之類的操做的話,仍是MyISAM佔優點些,由於索引所佔空間小,這些操做是須要在內存中完成的。

聚簇索引須要注意的地方

當使用主鍵爲聚簇索引時,主鍵最好不要使用uuid,由於uuid的值太過離散,不適合排序且可能出線新增長記錄的uuid,會插入在索引樹中間的位置,致使索引樹調整複雜度變大,消耗更多的時間和資源。

建議使用int類型的自增,方便排序而且默認會在索引樹的末尾增長主鍵值,對索引樹的結構影響最小。並且,主鍵值佔用的存儲空間越大,輔助索引中保存的主鍵值也會跟着變大,佔用存儲空間,也會影響到IO操做讀取到的數據量。

爲何主鍵一般建議使用自增id

聚簇索引的數據的物理存放順序與索引順序是一致的,即:只要索引是相鄰的,那麼對應的數據必定也是相鄰地存放在磁盤上的。若是主鍵不是自增id,那麼能夠想 象,它會幹些什麼,不斷地調整數據的物理地址、分頁,固然也有其餘一些措施來減小這些操做,但卻沒法完全避免。但,若是是自增的,那就簡單了,它只須要一 頁一頁地寫,索引結構相對緊湊,磁盤碎片少,效率也高。

四個隔離級別

咱們先回憶一下事務的四個隔離級別,他們由弱到強以下所示:

  • Read Uncommited(RU):讀未提交,一個事務能夠讀到另外一個事務未提交的數據!
  • Read Committed (RC):讀已提交,一個事務能夠讀到另外一個事務已提交的數據!
  • Repeatable Read:(RR):可重複讀,加入間隙鎖,必定程度上避免了幻讀的產生!注意了,只是必定程度上,並無徹底避免!我會在下一篇文章說明!另外就是記住從該級別纔開始加入間隙鎖(這句話記下來,後面有用到)!
  • Serializable:串行化,該級別下讀寫串行化,且全部的select語句後都自動加上lock in share mode,即便用了共享鎖。所以在該隔離級別下,使用的是當前讀,而不是快照讀。

select 分析的表數據

爲了便於說明,我來個例子,假設有表數據以下,pId爲主鍵索引

pId(int) name(varchar) num(int)
1 aaa 100
2 bbb 200
7 ccc 200

隔離級別:RC/RU ,條件列: 非索引

(1)select * from table where num = 200
不加任何鎖,是快照讀。
(2)select * from table where num > 200
不加任何鎖,是快照讀。
(3)select * from table where num = 200 lock in share mode
當num = 200,有兩條記錄。這兩條記錄對應的pId=2,7,所以在pId=2,7的聚簇索引上加行級S鎖,採用當前讀。
(4)select * from table where num > 200 lock in share mode
當num > 200,有一條記錄。這條記錄對應的pId=3,所以在pId=3的聚簇索引上加上行級S鎖,採用當前讀。
(5)select * from table where num = 200 for update
當num = 200,有兩條記錄。這兩條記錄對應的pId=2,7,所以在pId=2,7的聚簇索引上加行級X鎖,採用當前讀。
(6)select * from table where num > 200 for update
當num > 200,有一條記錄。這條記錄對應的pId=3,所以在pId=3的聚簇索引上加上行級X鎖,採用當前讀。

隔離級別:RC/RU ,條件列: 聚簇索引

你們應該知道pId是主鍵列,所以pId用的就是聚簇索引。此狀況其實和RC/RU+條件列非索引狀況是相似的。
(1)select * from table where pId = 2
不加任何鎖,是快照讀。
(2)select * from table where pId > 2
不加任何鎖,是快照讀。
(3)select * from table where pId = 2 lock in share mode
在pId=2的聚簇索引上,加S鎖,爲當前讀。
(4)select * from table where pId > 2 lock in share mode
在pId=3,7的聚簇索引上,加S鎖,爲當前讀。
(5)select * from table where pId = 2 for update
在pId=2的聚簇索引上,加X鎖,爲當前讀。
(6)select * from table where pId > 2 for update
在pId=3,7的聚簇索引上,加X鎖,爲當前讀。

爲何條件列加不加索引,加鎖狀況是同樣的?

實際上是不同的。在RC/RU隔離級別中,MySQL作了優化。在條件列沒有索引的狀況下,儘管經過聚簇索引來掃描全表,進行全表加鎖。可是,MySQL Server層會進行過濾並把不符合條件的鎖立即釋放掉,所以你看起來最終結果是同樣的。可是RC/RU+條件列非索引比本例多了一個釋放不符合條件的鎖的過程!

隔離級別:RC/RU ,條件列: 非聚簇索引

在num列上建上非惟一索引。此時有一棵聚簇索引(主鍵索引,pId)造成的B+索引樹,其葉子節點爲硬盤上的真實數據。以及另外一棵非聚簇索引(非惟一索引,num)造成的B+索引樹,其葉子節點依然爲索引節點,保存了num列的字段值,和對應的聚簇索引。

(1)select * from table where num = 200
不加任何鎖,是快照讀。
(2)select * from table where num > 200
不加任何鎖,是快照讀。
(3)select * from table where num = 200 lock in share mode
當num = 200,因爲num列上有索引,所以先在 num = 200的兩條索引記錄上加行級S鎖。接着,去聚簇索引樹上查詢,這兩條記錄對應的pId=2,7,所以在pId=2,7的聚簇索引上加行級S鎖,採用當前讀。
(4)select * from table where num > 200 lock in share mode
當num > 200,因爲num列上有索引,所以先在符合條件的 num = 300的一條索引記錄上加行級S鎖。接着,去聚簇索引樹上查詢,這條記錄對應的pId=3,所以在pId=3的聚簇索引上加行級S鎖,採用當前讀。
(5)select * from table where num = 200 for update
當num = 200,因爲num列上有索引,所以先在 num = 200的兩條索引記錄上加行級X鎖。接着,去聚簇索引樹上查詢,這兩條記錄對應的pId=2,7,所以在pId=2,7的聚簇索引上加行級X鎖,採用當前讀。
(6)select * from table where num > 200 for update
當num > 200,因爲num列上有索引,所以先在符合條件的 num = 300的一條索引記錄上加行級X鎖。接着,去聚簇索引樹上查詢,這條記錄對應的pId=3,所以在pId=3的聚簇索引上加行級X鎖,採用當前讀。

隔離級別:RR/Serializable,條件列: 非索引

RR級別須要多考慮的就是gap lock,他的加鎖特徵在於,不管你怎麼查都是鎖全表。接下來分析開始
(1)select * from table where num = 200
在RR級別下,不加任何鎖,是快照讀。
在Serializable級別下,在pId = 1,2,3,7(全表全部記錄)的聚簇索引上加S鎖。而且在
聚簇索引的全部間隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(2)select * from table where num > 200
在RR級別下,不加任何鎖,是快照讀。
在Serializable級別下,在pId = 1,2,3,7(全表全部記錄)的聚簇索引上加S鎖。而且在
聚簇索引的全部間隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(3)select * from table where num = 200 lock in share mode
在pId = 1,2,3,7(全表全部記錄)的聚簇索引上加S鎖。而且在
聚簇索引的全部間隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(4)select * from table where num > 200 lock in share mode
在pId = 1,2,3,7(全表全部記錄)的聚簇索引上加S鎖。而且在
聚簇索引的全部間隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(5)select * from table where num = 200 for update
在pId = 1,2,3,7(全表全部記錄)的聚簇索引上加X鎖。而且在
聚簇索引的全部間隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(6)select * from table where num > 200 for update
在pId = 1,2,3,7(全表全部記錄)的聚簇索引上加X鎖。而且在
聚簇索引的全部間隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock

隔離級別:RR/Serializable,條件列: 聚簇索引

你們應該知道pId是主鍵列,所以pId用的就是聚簇索引。該狀況的加鎖特徵在於,若是where後的條件爲精確查詢(=的狀況),那麼只存在record lock。若是where後的條件爲範圍查詢(>或<的狀況),那麼存在的是record lock+gap lock。
(1)select * from table where pId = 2
在RR級別下,不加任何鎖,是快照讀。
在Serializable級別下,是當前讀,在pId=2的聚簇索引上加S鎖,不存在gap lock。
(2)select * from table where pId > 2
在RR級別下,不加任何鎖,是快照讀。
在Serializable級別下,是當前讀,在pId=3,7的聚簇索引上加S鎖。在(2,3)(3,7)(7,+∞)加上gap lock
(3)select * from table where pId = 2 lock in share mode
是當前讀,在pId=2的聚簇索引上加S鎖,不存在gap lock。
(4)select * from table where pId > 2 lock in share mode
是當前讀,在pId=3,7的聚簇索引上加S鎖。在(2,3)(3,7)(7,+∞)加上gap lock
(5)select * from table where pId = 2 for update
是當前讀,在pId=2的聚簇索引上加X鎖。
(6)select * from table where pId > 2 for update
在pId=3,7的聚簇索引上加X鎖。在(2,3)(3,7)(7,+∞)加上gap lock
(7)select * from table where pId = 6 [lock in share mode|for update]
注意了,pId=6是不存在的列,這種狀況會在(3,7)上加gap lock。
(8)select * from table where pId > 18 [lock in share mode|for update]
注意了,pId>18,查詢結果是空的。在這種狀況下,是在(7,+∞)上加gap lock。

隔離級別:RR/Serializable,條件列: 非聚簇索引

這裏非聚簇索引,須要區分是否爲惟一索引。由於若是是非惟一索引,間隙鎖的加鎖方式是有區別的。
先說一下,惟一索引的狀況。若是是惟一索引,狀況和RR/Serializable+條件列是聚簇索引相似,惟一有區別的是:這個時候有兩棵索引樹,加鎖是加在對應的非聚簇索引樹和聚簇索引樹上!你們能夠自行推敲!
下面說一下,非聚簇索引是非惟一索引的狀況,他和惟一索引的區別就是經過索引進行精確查詢之後,不只存在record lock,還存在gap lock。而經過惟一索引進行精確查詢後,只存在record lock,不存在gap lock。老規矩在num列創建非惟一索引
(1)select * from table where num = 200
在RR級別下,不加任何鎖,是快照讀。
在Serializable級別下,是當前讀,在pId=2,7的聚簇索引上加S鎖,在num=200的非彙集索引上加S鎖,在(100,200)(200,300)加上gap lock。
(2)select * from table where num > 200
在RR級別下,不加任何鎖,是快照讀。
在Serializable級別下,是當前讀,在pId=3的聚簇索引上加S鎖,在num=300的非彙集索引上加S鎖。在(200,300)(300,+∞)加上gap lock
(3)select * from table where num = 200 lock in share mode
是當前讀,在pId=2,7的聚簇索引上加S鎖,在num=200的非彙集索引上加S鎖,在(100,200)(200,300)加上gap lock。
(4)select * from table where num > 200 lock in share mode
是當前讀,在pId=3的聚簇索引上加S鎖,在num=300的非彙集索引上加S鎖。在(200,300)(300,+∞)加上gap lock。
(5)select * from table where num = 200 for update
是當前讀,在pId=2,7的聚簇索引上加S鎖,在num=200的非彙集索引上加X鎖,在(100,200)(200,300)加上gap lock。
(6)select * from table where num > 200 for update
是當前讀,在pId=3的聚簇索引上加S鎖,在num=300的非彙集索引上加X鎖。在(200,300)(300,+∞)加上gap lock
(7)select * from table where num = 250 [lock in share mode|for update]
注意了,num=250是不存在的列,這種狀況會在(200,300)上加gap lock。
(8)select * from table where num > 400 [lock in share mode|for update]
注意了,pId>400,查詢結果是空的。在這種狀況下,是在(400,+∞)上加gap lock。

死鎖

MyISAM表鎖是deadlock free的,這是由於MyISAM老是一次得到所需的所有鎖,要麼所有知足,要麼等待,所以不會出現死鎖。但在InnoDB中,除單個SQL組成的事務外,鎖是逐步得到的,當兩個事務都須要得到對方持有的排他鎖才能繼續完成事務,這種循環鎖等待就是典型的死鎖。

如何避免死鎖?

  • 爲了在單個 InnoDB 表上執行多個併發寫入操做時避免死鎖,能夠在事務開始時經過爲預期要修改的每一個元祖(行)使用 SELECT … FOR UPDATE 語句來獲取必要的鎖,即便這些行的更改語句是在以後才執行的。

  • 在事務中,若是要更新記錄,應該直接申請足夠級別的鎖,即排他鎖,而不該先申請共享鎖、更新時再申請排他鎖,由於這時候當用戶再申請排他鎖時,其餘事務可能又已經得到了相同記錄的共享鎖,從而形成鎖衝突,甚至死鎖

  • 若是事務須要修改或鎖定多個表,則應在每一個事務中以相同的順序使用加鎖語句。在應用中,若是不一樣的程序會併發存取多個表,應儘可能約定以相同的順序來訪問表,這樣能夠大大下降產生死鎖的機會

  • 在程序以批量方式處理數據的時候,若是事先對數據排序,保證每一個線程按固定的順序來處理記錄,也能夠大大下降出現死鎖的可能。

  • 在REPEATABLE-READ隔離級別下,若是兩個線程同時對相同條件記錄用SELECT...FOR UPDATE加排他鎖,在沒有符合該條件記錄狀況下,兩個線程都會加鎖成功。程序發現記錄尚不存在,就試圖插入一條新記錄,若是兩個線程都這麼作,就會出現死鎖。這種狀況下,將隔離級別改爲READ COMMITTED,就可避免問題。

  • 當隔離級別爲READ COMMITTED時,若是兩個線程都先執行SELECT...FOR UPDATE,判斷是否存在符合條件的記錄,若是沒有,就插入記錄。此時,只有一個線程能插入成功,另外一個線程會出現鎖等待,當第1個線程提交後,第2個線程會因主鍵重出錯,但雖然這個線程出錯了,卻會得到一個排他鎖。這時若是有第3個線程又來申請排他鎖,也會出現死鎖。對於這種狀況,能夠直接作插入操做,而後再捕獲主鍵重異常,或者在遇到主鍵重錯誤時,老是執行ROLLBACK釋放得到的排他鎖

InnoDB 默認是如何對待死鎖的?

InnoDB 默認是使用設置死鎖時間來讓死鎖超時的策略,默認 innodblockwait_timeout 設置的時長是 50s。

如何開啓死鎖檢測?

設置 innodbdeadlockdetect 設置爲 on 能夠主動檢測死鎖,在 Innodb 中這個值默認就是 on 開啓的狀態。

解決方案2:樂觀鎖

樂觀鎖

樂觀鎖並非真實存在的鎖,而是在更新的時候判斷此時的庫存是不是以前查詢出的庫存,若是相同,表示沒人修改,能夠更新庫存,不然表示別人搶過資源,再也不執行庫存更新。相似以下操做:

update tb_sku set stock=2 where id=1 and stock=7;

SKU.objects.filter(id=1, stock=7).update(stock=2)

使用樂觀鎖需修改數據庫的事務隔離級別:

使用樂觀鎖的時候,若是一個事務修改了庫存並提交了事務,那其餘的事務應該能夠讀取到修改後的數據值,因此不能使用可重複讀的隔離級別,應該修改成讀取已提交(Read committed)。
修改方式:
在這裏插入圖片描述
在這裏插入圖片描述

MySQL事務隔離級別

事務隔離級別 髒讀 不可重複讀 幻讀
讀未提交(read-uncommitted)
不可重複讀(read-committed)
可重複讀(repeatable-read)
串行化(serializable)

mysql默認的事務隔離級別爲repeatable-read

img

併發事務會帶來哪些問題?

  一、髒讀:事務A讀取了事務B更新的數據,而後B回滾操做,那麼A讀取到的數據是髒數據

  二、不可重複讀:事務 A 屢次讀取同一數據,事務 B 在事務A屢次讀取的過程當中,對數據做了更新並提交,致使事務A屢次讀取同一數據時,結果 不一致。

  三、幻讀:系統管理員A將數據庫中全部學生的成績從具體分數改成ABCDE等級,可是系統管理員B就在這個時候插入了一條具體分數的記錄,當系統管理員A改結束後發現還有一條記錄沒有改過來,就好像發生了幻覺同樣,這就叫幻讀。

  小結:不可重複讀的和幻讀很容易混淆,不可重複讀側重於修改,幻讀側重於新增或刪除。解決不可重複讀的問題只需鎖住知足條件的行,解決幻讀須要鎖表

3、MySQL事務隔離級別

img

Mysql默認的事務隔離級別爲repeatable-read

img

4、用例子說明各個隔離級別的狀況

一、讀未提交:

(1)打開一個客戶端A,並設置當前事務模式爲read uncommitted(未提交讀),查詢表account的初始值:

img

 (2)在客戶端A的事務提交以前,打開另外一個客戶端B,更新表account:

img

 (3)這時,雖然客戶端B的事務還沒提交,可是客戶端A就能夠查詢到B已經更新的數據:

img

(4)一旦客戶端B的事務由於某種緣由回滾,全部的操做都將會被撤銷,那客戶端A查詢到的數據其實就是髒數據:

img

(5)在客戶端A執行更新語句update account set balance = balance - 50 where id =1,lilei的balance沒有變成350,竟然是400,是否是很奇怪,數據不一致啊,若是你這麼想就太天真 了,在應用程序中,咱們會用400-50=350,並不知道其餘會話回滾了,要想解決這個問題能夠採用讀已提交的隔離級別

img

 二、讀已提交

(1)打開一個客戶端A,並設置當前事務模式爲read committed(提交讀),查詢表account的全部記錄:

img

 (2)在客戶端A的事務提交以前,打開另外一個客戶端B,更新表account:

img

(3)這時,客戶端B的事務還沒提交,客戶端A不能查詢到B已經更新的數據,解決了髒讀問題:

img

(4)客戶端B的事務提交

img

(5)客戶端A執行與上一步相同的查詢,結果 與上一步不一致,即產生了不可重複讀的問題

img

  三、可重複讀

(1)打開一個客戶端A,並設置當前事務模式爲repeatable read,查詢表account的全部記錄

img

(2)在客戶端A的事務提交以前,打開另外一個客戶端B,更新表account並提交

img

(3)在客戶端A查詢表account的全部記錄,與步驟(1)查詢結果一致,沒有出現不可重複讀的問題

img

(4)在客戶端A,接着執行update balance = balance - 50 where id = 1,balance沒有變成400-50=350,lilei的balance值用的是步驟(2)中的350來算的,因此是300,數據的一致性卻是沒有被破壞。可重複讀的隔離級別下使用了MVCC機制,select操做不會更新版本號,是快照讀(歷史版本);insert、update和delete會更新版本號,是當前讀(當前版本)。

img

(5)從新打開客戶端B,插入一條新數據後提交

img

(6)在客戶端A查詢表account的全部記錄,沒有 查出 新增數據,因此沒有出現幻讀

img

 4.串行化

(1)打開一個客戶端A,並設置當前事務模式爲serializable,查詢表account的初始值:

mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+------+--------+---------+
| id   | name   | balance |
+------+--------+---------+
|    1 | lilei  |   10000 |
|    2 | hanmei |   10000 |
|    3 | lucy   |   10000 |
|    4 | lily   |   10000 |
+------+--------+---------+
4 rows in set (0.00 sec)

(2)打開一個客戶端B,並設置當前事務模式爲serializable,插入一條記錄報錯,表被鎖了插入失敗,mysql中事務隔離級別爲serializable時會鎖表,所以不會出現幻讀的狀況,這種隔離級別併發性極低,開發中不多會用到。

mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values(5,'tom',0);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

  

  補充:

  一、事務隔離級別爲讀提交時,寫數據只會鎖住相應的行

  二、事務隔離級別爲可重複讀時,若是檢索條件有索引(包括主鍵索引)的時候,默認加鎖方式是next-key 鎖;若是檢索條件沒有索引,更新數據時會鎖住整張表。一個間隙被事務加了鎖,其餘事務是不能在這個間隙插入記錄的,這樣能夠防止幻讀。

  三、事務隔離級別爲串行化時,讀寫數據都會鎖住整張表

   四、隔離級別越高,越能保證數據的完整性和一致性,可是對併發性能的影響也越大。

樂觀鎖在高併發場景下的問題

樂觀鎖在高併發場景下的問題,是嚴重的空自旋

具體能夠參考 入大廠必備的基礎書籍: 《Java高併發核心編程 卷2》

超賣解決方案3:分階段排隊下單方案

分階段排隊下單方案的思想來源

最優的解決方案,其實思想來自於JUC的原理

JUC是如何提升性能的,引入隊列

原始的CLH隊列

用於減小線程爭用的最簡單的隊列,CLH隊列,具體能夠參考 入大廠必備的基礎書籍: 《Java高併發核心編程 卷2》

在這裏插入圖片描述

JUC的AQS內部隊列

AQS內部隊列是JUC高性能的基礎,AQS隊列,具體能夠參考 入大廠必備的基礎書籍: 《Java高併發核心編程 卷2》

在這裏插入圖片描述

分階段排隊下單方案

將提交操做變成兩段式:

  • 第一階段申請,申請預減減庫,申請成功以後,進入消息隊列;

  • 第二階段確認,從消息隊列消費申請令牌,而後完成下單操做。 查庫存 -> 建立訂單 -> 扣減庫存。經過分佈式鎖保障解決多個provider實例併發下單產生的超賣問題。

申請階段:

將存庫從MySQL前移到Redis中,全部的預減庫存的操做放到內存中,因爲Redis中不存在鎖故不會出現互相等待,而且因爲Redis的寫性能和讀性能都遠高於MySQL,這就解決了高併發下的性能問題。

確認階段:

而後經過隊列等異步手段,將變化的數據異步寫入到DB中。

引入隊列,而後數據經過隊列排序,按照次序更新到DB中,徹底串行處理。當達到庫存閥值的時候就不在消費隊列,並關閉購買功能。這就解決了超賣問題。

分階段排隊架構圖

圖解削峯限流技術RabbitMq 消息隊列解決高併發高併發下削峯限流

基於分段的排隊執行方案的性能提高

一個高性能秒殺的場景:

假設一個商品1分鐘6000訂單,每秒的 600個下單操做。

在排隊階段,每秒的 600個預減庫存的操做,對於 Redis 來講,沒有任何壓力。甚至每秒的 6000個預減庫存的操做,對於 Redis 來講,也是壓力不大。

可是在下單階段,就不同了。假設加鎖以後,釋放鎖以前,查庫存 -> 建立訂單 -> 扣減庫存,通過優化,每一個IO操做100ms,大概200毫秒,一秒鐘5個訂單。600個訂單須要120s,2分鐘才能完全消化。

如何提高下單階段的性能呢?

在這裏插入圖片描述

可使用Redis 分段鎖。

爲了達到每秒600個訂單,能夠將鎖分紅 600 /5 =120 個段,每一個段負責5個訂單,600個訂單,在第二個階段1秒鐘下單完成。

在這裏插入圖片描述

有關Redis分段鎖的詳細知識,請閱讀下面的博文:

Redis分佈式鎖 (圖解-秒懂-史上最全)

基於分段的排隊執行方案優勢:

解決超賣問題,庫存讀寫都在內存中,故同時解決性能問題。

基於分段的排隊執行方案缺點:

  • 數據不一致的問題:

因爲異步寫入DB,可能存在數據不一致,存在某一時刻DB和Redis中數據不一致的風險。

  • 可能存在少買

可能存在少買,也就是若是拿到號的人不真正下訂單,可能庫存減爲0,可是訂單數並無達到庫存閥值。

參考文獻:

http://www.javashuo.com/article/p-resvyvjh-ch.html

http://www.javashuo.com/article/p-bjxxhkyq-dx.html

https://www.cnblogs.com/wyaokai/p/10921323.html

相關文章
相關標籤/搜索