數據庫鎖機制


一 數據庫的鎖機制

什么是鎖?為何要加入鎖機制?

鎖是計算機協調多個進程或線程並發訪問某一資源的機制,那為何要加入鎖機制呢?

因為在數據庫中,除了傳統的計算資源(如CPU、RAM、I/O等)的爭用以外,數據也是一種供需要用戶共享的資源。

當並發事務同時訪問一個共享的資源時,有可能導致數據不一致、數據無效等問題,

例如我們在數據庫的讀現象中介紹過,在並發訪問情況下,可能會出現臟讀、不可重復讀和幻讀等讀現象

為了應對這些問題,主流數據庫都提供了鎖機制,以及事務隔離級別的概念,

而鎖機制可以將並發的數據訪問順序化,以保證數據庫中數據的一致性與有效性

此外,鎖沖突也是影響數據庫並發訪問性能的一個重要因素,鎖對數據庫而言顯得尤其重要,也更加復雜。

並發控制

在計算機科學,特別是程序設計、操作系統、多處理機和數據庫等領域,並發控制(Concurrency control)是確保及時糾正由並發操作導致的錯誤的一種機制。

數據庫管理系統(DBMS)中的並發控制的任務是確保在多個事務同時存取數據庫中同一數據時不破壞事務的隔離性和統一性以及數據庫的統一性。下面舉例說明並發操作帶來的數據不一致性問題:

現有兩處火車票售票點,同時讀取某一趟列車車票數據庫中車票余額為 X。兩處售票點同時賣出一張車票,同時修改余額為 X -1寫回數據庫,這樣就造成了實際賣出兩張火車票而數據庫中的記錄卻只少了一張。 產生這種情況的原因是因為兩個事務讀入同一數據並同時修改,其中一個事務提交的結果破壞了另一個事務提交的結果,導致其數據的修改被丟失,破壞了事務的隔離性。並發控制要解決的就是這類問題。

封鎖、時間戳、樂觀並發控制(樂觀鎖)和悲觀並發控制(悲觀鎖)是並發控制主要采用的技術手段。

二 鎖的分類

鎖的分類(oracle)

一、按鎖的粒度划分,可分為行級鎖表級鎖頁級鎖(mysql支持)

二、按鎖級別划分,可分為共享鎖排他鎖

三、按使用方式划分,可分為樂觀鎖悲觀鎖

四、按加鎖方式划分,可分為自動鎖、顯式

五、按操作划分,可分為DML鎖DDL鎖

DML鎖(data locks,數據鎖),用於保護數據的完整性,其中包括行級鎖(Row Locks (TX鎖))、表級鎖(table lock(TM鎖))。

DDL鎖(dictionary locks,數據字典鎖),用於保護數據庫對象的結構,如表、索引等的結構定義。其中包排他DDL鎖(Exclusive DDL lock)、共享DDL鎖(Share DDL lock)、可中斷解析鎖(Breakable parse locks) 

 

三 MySQL中的行級鎖,表級鎖,頁級鎖(粒度)

在DBMS中,可以按照鎖的粒度把數據庫鎖分為行級鎖(INNODB引擎)、表級鎖(MYISAM引擎)和頁級鎖(BDB引擎 )。

行級鎖

行級鎖是Mysql中鎖定粒度最細的一種鎖,表示只針對當前操作的行進行加鎖。行級鎖能大大減少數據庫操作的沖突。其加鎖粒度最小,但加鎖的開銷也最大。行級鎖分為共享鎖 和 排他鎖

  • 特點:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,並發度也最高
  • 支持引擎:InnoDB
  • 行級鎖定分為行共享讀鎖(共享鎖)與行獨占寫鎖(排他鎖) ,如下所示,用法詳見下一小節
共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE

 


表級鎖(偏向於讀)

表級鎖是MySQL中鎖定粒度最大的一種鎖,表示對當前操作的整張表加鎖,它實現簡單,資源消耗較少,被大部分MySQL引擎支持。最常使用的MYISAM與INNODB都支持表級鎖定。表級鎖定分為表共享讀鎖(共享鎖)與表獨占寫鎖(排他鎖)。

  • 特點開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發出鎖沖突的概率最高,並發度最低
  • 支持引擎:MyISAM、MEMORY、InNoDB
  • 分類:表級鎖定分為表共享讀鎖(共享鎖)與表獨占寫鎖(排他鎖),如下所示
復制代碼
lock table 表名 read(write),表名 read(write),.....;
//給表加讀鎖或者寫鎖,例如

mysql> lock table employee write;

Query OK, 0 rows affected (0.00 sec)

mysql> show open tables where in_use>= 1;

+----------+----------+--------+-------------+

| Database | Table    | In_use | Name_locked |

+----------+----------+--------+-------------+

| ttt      | employee |      1 |           0 |

+----------+----------+--------+-------------+

1 row in set (0.00 sec)

mysql> unlock tables; -- UNLOCK TABLES釋放被當前會話持有的任何鎖

Query OK, 0 rows affected (0.00 sec)

 

mysql> show open tables where in_use>= 1;

Empty set (0.00 sec)

 

mysql> 

復制代碼

頁級鎖

頁級鎖是MySQL中鎖定粒度介於行級鎖和表級鎖中間的一種鎖。表級鎖速度快,但沖突多,行級沖突少,但速度慢。所以取了折衷的頁級,一次鎖定相鄰的一組記錄。BDB支持頁級鎖

特點:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,並發度一般。


四 行級鎖之共享鎖與排他鎖(級別)

行級鎖分為共享鎖和排他鎖兩種。

與行處理相關的sql有:insert、update、delete、select,這四類sql在操作記錄行時,可以為行加上鎖,但需要知道的是:

1、對於insert、update、delete語句,InnoDB會自動給涉及的數據加鎖,而且是排他鎖(X);

2、對於普通的select語句,InnoDB不會加任何鎖,需要我們手動自己加,可以加兩種類型的鎖,如下所示

# 共享鎖(S):SELECT ... LOCK IN SHARE MODE;  -- 查出的記錄行都會被鎖住

# 排他鎖(X):SELECT ... FOR UPDATE;  -- 查出的記錄行都會被鎖住

 

准備表和數據,sql如下

create table employee(
id int primary key auto_increment,
name varchar(20) not null,
age int(3) unsigned not null default 20
);

insert into employee(name) values
('egon'),
('alex'),
('wupeiqi'),
('yuanhao'),
('liwenzhou'),
('jingliyang'),
('jinxin'),
('成龍'),
('歪歪'),
('丫丫'),
('丁丁'),
('星星'),
('格格'),
('張野'),
('程咬金'),
('程咬銀'),
('程咬銅'),
('程咬鐵')
;
update employee set age = 18 where id <=3;
View Code

實驗:驗證insert、update、delete是默認加排他鎖的

需要知道的是,針對事務,mysql是隱式開啟、隱式提交,即mysql默認會把每條sql語句都放入一個事務中,並在該條sql語句執行完畢后自動提交,所以如果我們如果開啟兩個session,在一個session中直接運行update,很快就運行完畢並自動提交,而提交事務或回滾事務都會釋放鎖,這樣話就無法驗證效果了,所以我們采用手動開啟事務方式進行驗證,如此我們就可以自己控制事務的提交了(如果忘記了該知識點,請點擊這里

  事務一 事務二
步驟1  

start transaction;

select name from employee where id = 1;  -- name 為 jason

start transaction;

select name from employee where id = 1;  -- name 為 jason

步驟2   -- 把小寫的jason變為大寫,此時的update會自動加鎖

update employee set name = "jason" where id = 1;

 

-- 立即查看修改結果,name變為jason,但此時還沒有commit

select name from employee where id = 1;

步驟3 -- 此處的update會阻塞在原地,因為事務二並未提交事務,即尚未釋放排他鎖

update employee set name = concat(name,"_NB") where id = 1;

 
步驟4  

-- 事務二一旦提交,事務一阻塞在步驟3的操作即會運行成功

commit;

-- 查看修改結果,name變為大寫的jason

select name from employee where id = 1;

步驟5

 -- 此處查詢到的結果為jason_NB

select name from employee where id = 1;

 
步驟6

-- 提交之后,name持久化為jason_NB

commit;

select name from employee where id = 1;

 

共享鎖(Share Lock) 

共享鎖又稱為讀鎖,簡稱S鎖,顧名思義,共享鎖就是多個事務對於同一數據可以共享一把鎖,獲准共享鎖的事務只能讀數據,不能修改數據直到已釋放所有共享鎖,所以共享鎖可以支持並發讀(參考下述實驗三)。

如果事務T對數據A加上共享鎖后,則其他事務只能對A再加共享鎖或不加鎖(在其他事務里一定不能再加排他鎖,但是在事務T自己里面是可以加的),反之亦然。

用法

SELECT ... LOCK IN SHARE MODE;

在查詢語句后面增加LOCK IN SHARE MODE,Mysql會對查詢結果中的每行都加共享鎖,當沒有其他線程對查詢結果集中的任何一行使用排他鎖時,可以成功申請共享鎖,否則會被阻塞。其他線程也可以讀取使用了共享鎖的表,而且這些線程讀取的是同一個版本的數據。

 

排他鎖(eXclusive Lock)

排他鎖又稱為寫鎖,簡稱X鎖,顧名思義,排他鎖就是不能與其他所並存,如一個事務獲取了一個數據行的排他鎖,其他事務就不能再對該行加任何類型的其他他鎖(共享鎖和排他鎖),但是獲取排他鎖的事務是可以對數據就行讀取和修改。

用法

SELECT ... FOR UPDATE;

在查詢語句后面增加FOR UPDATE,Mysql會對查詢結果中的每行都加排他鎖,當沒有其他線程對查詢結果集中的任何一行使用排他鎖時,可以成功申請排他鎖,否則會被阻塞。

特例:加過排他鎖的數據行在其他事務種是不能修改數據的,也不能通過for update和lock in share mode鎖的方式查詢數據,但可以直接通過select ...from...查詢數據,因為普通select查詢沒有任何鎖機制。

 

實驗一:事務二獲取了排他鎖,在事務一中驗證上述特例

  事務一 事務二
步驟1 -- 開啟事務 

start transaction;

-- 開啟事務

start transaction; 

步驟2  

-- 加排他鎖,鎖住id<3的所有行

select * from employee where id < 3 for update;  

步驟3

-- 阻塞在原地

select * from employee where id = 1 for update;

 

-- 阻塞在原地

select * from employee where id = 1 lock in share mode;

 

-- 我們看到開了排他鎖查詢和共享鎖查詢都會處於阻塞狀態

-- 因為id=1的數據已經被加上了排他鎖,此處阻塞是等待排他鎖釋放。

 
步驟4

 -- ctrl+c終止步驟3的阻塞狀態

 -- 注意:

 -- 不要按多次ctrl+c多次按會結束鏈接,按一次就可以了

 -- 下述實驗遇到阻塞都可以用采用ctrl+c的方式結束,或者等待鎖超時

 

步驟5

-- 如果我們直接使用以下查詢,即便id<3的行都被事務二鎖住了

-- 但此處仍可以查看到數據

-- 證明普通select查詢沒有任何鎖機制

select name from employee where id = 1;

 

 

步驟6

-- 提交一下事務,不要影響下一次實驗

commit;

-- 提交一下事務,不要影響下一次實驗

commit;

實驗二:事務二獲取了共享鎖,在其他事務里也只能加共享鎖或不加鎖,在事務二中驗證 

  事務一 事務二
步驟1  

start transaction;

-- 開啟事務、加共享鎖,鎖住id<3的所有行

start transaction;

select * from employee where id < 3 lock in share mode;  

步驟2

-- 加排他鎖,會阻塞在原地

select * from employee where id = 1 for update;

 

-- 加共享鎖,可以查出結果,不會阻塞在原地

select * from employee where id = 1 lock in share mode;

 

-- 不加鎖,必然也可以查出結果,不會阻塞在原地

select name from employee where id = 1;

 
步驟3  

-- 提交一下事務,不要影響下一次實驗

commit;

 

-- 提交一下事務,不要影響下一次實驗

commit;

實驗三:事務二獲取了共享鎖,在其他事務里也只能加共享鎖或不加鎖,反之亦然,並驗證在多個事務加了共享鎖后,大家對加鎖的數據行只能讀不能寫

  事務一 事務二
步驟1  

start transaction;

-- 開啟事務、加共享鎖,鎖住id<3的所有行

start transaction;

select * from employee where id < 3 lock in share mode;  

步驟2

 

-- 此時尚未有其他事務為id<3的行加過鎖,所以在本事務里還是可以使用update的

-- 而update默認是加了排他鎖

update employee set name="jason" where id = 1;

select name from employee where id = 1;  -- 修改成功,結果為jason

步驟3

-- 加排他鎖,阻塞在原地,因為事務二剛在步驟2中對id=1的行加了排他鎖

-- 一旦某一個事物對記錄行加了排他鎖后

-- 其他事務均無法加任何鎖

select name from employee where id = 1 for update;

 

-- 也無法加共享鎖,加上共享鎖,同樣阻塞在原地

select name from employee where id = 1 lock in share mode;

 

-- 普通select查詢,不受鎖影響,可以查查結果

-- 查詢結果仍為修改前的結果,即jason_NB

-- 因為事務二的update行為尚未提交

select name from employee where id=1;

 
步驟4   

 -- 釋放事務一加過的所鎖,包括update加的互斥鎖,還有select加的共享鎖

rollback;

 -- 手動開啟事務的方式,在rollback之后事務就結束了,需要重新開啟,切記

start transaction;

步驟5  

-- 重新對id<3的行加共享鎖

select * from employee where id < 3 lock in share mode;  

步驟6

-- 在事務1中也對id<3的行加共享鎖,可以加成功

-- 因為當一個事務(比如事務2)對記錄加了共享鎖后

-- 其他事務(比如事務1)只能對記錄行加共享鎖或不加鎖

select * from employee where id < 3 lock in share mode;  

 
步驟7  

-- 事務一已經對id<3的所有行都加了共享鎖,id=1的行當然包含在內

-- 所以在事物二中只能對id=1行加共享鎖或者不加鎖

-- 而下述語句中update默認會加排他鎖,所以會阻塞在原地,

-- 即只能讀不能改了

update employee set name="jason" where id=1;

步驟8

-- 注意:

-- 需要在ctrl+c掉事務二的步驟7,或者等一會顯示鎖超時,再執行下述sql

update employee set name="jason" where id=1;

 

-- 原因如下:

-- 因為如果在事務二的步驟7阻塞的同時,也在事務一里執行了下述sql,

-- 事務一會報死鎖錯誤(詳見5.3),事務一會因此結束,此時事務二原本

-- 的阻塞狀態會立即執行成功,為啥???

-- 1、首先:為何會出現死鎖?

--     因為事務二中,步驟7的update語句是想獲取互斥鎖,

--     會阻塞在原地,需要等待事務一先釋放共享鎖。

--     而事務一執行下述了下述update語句同樣是想獲取互斥鎖,

--     同樣需要等事務二先釋放共享鎖,至此雙方互相鎖死

-- 2、然后,事務一在拋出死鎖異常之后,會被強行終止,只剩事務二自己

--     因為不要有事務二以外的其他事務存在並對記錄行加鎖了

--     於是事務二的sql不存在鎖爭搶問題,會立即執行成功

--     此時,若想繼續實驗,則需要也rollback結束事務二,重新開啟兩個

--     事務並加共享鎖來進行試驗

 

-- 所以,根據上述分析,我們就先ctrl+c掉事務二的步驟7

-- 然后執行上述sql,該sql語句默認會加排他鎖,所以會阻塞在原地

-- 因為

-- 事務二已經對id<3的所有行都加了共享鎖,id=1的行當然包含在內

-- 所以在事務一中只能對id=1行加共享鎖或者不加鎖

-- 即只能讀不能改了

 
步驟9  

-- 提交一下事務,不要影響下一次實驗

commit;

 

-- 提交一下事務,不要影響下一次實驗

commit;

 

 

意向鎖

意向鎖是表級鎖,其設計目的主要是為了在一個事務中揭示下一行將要被請求鎖的類型。

意向鎖的作用就是當一個事務在需要獲取資源鎖定的時候,如果遇到自己需要的資源已經被排他鎖占用的時候,該事務可以需要鎖定行的表上面添加一個合適的意向鎖。
如果自己需要一個共享鎖,那么就在表上面添加一個意向共享鎖。而如果自己需要的是某行(或者某些行)上面添加一個排他鎖的話,則先在表上面添加一個意向排他鎖

InnoDB中有兩個意向鎖(表鎖):

(1)意向共享鎖(IS):事務打算給數據行共享鎖;,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖
(2)意向排他鎖(IX)事務打算給數據行加排他鎖;事務在給一個數據行加排他鎖前必須先取得該表的IX鎖

意向鎖是InnoDB自動加的,不需要用戶干預。

五 Innodb存儲引擎的鎖機制

mysql常用存儲引擎的鎖機制

MyISAM和MEMORY采用表級鎖(table-level locking)

BDB采用頁面鎖(page-level locking)或表級鎖,默認為頁面鎖

InnoDB支持行級鎖(row-level locking)和表級鎖,默認為行級鎖(偏向於寫)

InnoDB的鎖定模式實際上可以分為四種:共享鎖(S),排他鎖(X),意向共享鎖(IS)和意向排他鎖(IX),我們可以通過以下表格來總結上面這四種所的共存邏輯關系:

 

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

innodb鎖機制官網鏈接

5.1 行級鎖與表級鎖的使用區分

MyISAM 操作數據都是使用表級鎖,MyISAM總是一次性獲得所需的全部鎖,要么全部滿足,要么全部等待。所以不會產生死鎖,但是由於每操作一條記錄就要鎖定整個表,導致性能較低,並發不高。

InnoDB 與 MyISAM 的最大不同有兩點:一是 InnoDB 支持事務;二是 InnoDB 采用了行級鎖。也就是你需要修改哪行,就可以只鎖定哪行。

在Mysql中,行級鎖並不是直接鎖記錄,而是鎖索引。InnoDB 行鎖是通過給索引項加鎖實現的,而索引分為主鍵索引和非主鍵索引兩種

1、如果一條sql 語句操作了主鍵索引,Mysql 就會鎖定這條語句命中的主鍵索引(或稱聚簇索引)

2、如果一條語句操作了非主鍵索引(或稱輔助索引),MySQL會先鎖定該非主鍵索引,再鎖定相關的主鍵索引

3、如果沒有索引,InnoDB 會通過隱藏的聚簇索引來對記錄加鎖。也就是說:如果不通過索引條件檢索數據,那么InnoDB將對表中所有數據加鎖,實際效果跟表級鎖一樣

 

在實際應用中,要特別注意InnoDB行鎖的這一特性,不然的話,可能導致大量的鎖沖突,從而影響並發性能。

  • 1、在不通過索引條件查詢的時候,InnoDB 的效果就相當於表鎖
  • 2、當表有多個索引的時候,不同的事務可以使用不同的索引鎖定不同的行,另外,不論 是使用主鍵索引、唯一索引或普通索引,InnoDB 都會使用行鎖來對數據加鎖。
  • 3、由於 MySQL 的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以即便你的sql語句訪問的是不同的記錄行,但如果命中的是相同的被鎖住的索引鍵,也還是會出現鎖沖突的。
  • 4、即便在條件中使用了索引字段,但是否使用索引來檢索數據是由 MySQL 通過判斷不同 執行計划的代價來決定的,如果 MySQL 認為全表掃 效率更高,比如對一些很小的表,它 就不會使用索引,這種情況下 InnoDB 將鎖住所有行,相當於表鎖。因此,在分析鎖沖突時, 別忘了檢查 SQL 的執行計划,以確認是否真正使用了索引,如下案例

===============>案例<===============

准備表和數據,sql如下 

create table employee(
id int primary key auto_increment,
name varchar(20) not null,
age int(3) unsigned not null default 20
);

insert into employee(name) values
('egon'),
('alex'),
('wupeiqi'),
('yuanhao'),
('liwenzhou'),
('jingliyang'),
('jinxin'),
('成龍'),
('歪歪'),
('丫丫'),
('丁丁'),
('星星'),
('格格'),
('張野'),
('程咬金'),
('程咬銀'),
('程咬銅'),
('程咬鐵')
;
update employee set age = 16 where id=1;
update employee set age = 18 where id in (2,3,4);
View Code

步驟1、創建索引 

復制代碼
mysql> select * from employee;
+----+------------+-----+
| id | name       | age |
+----+------------+-----+
|  1 | jason       |  16 |
|  2 | alex       |  18 |
|  3 | wupeiqi    |  18 |
|  4 | yuanhao    |  18 |
|  5 | liwenzhou  |  20 |
|  6 | jingliyang |  20 |
|  7 | jinxin     |  20 |
|  8 | 成龍       |  20 |
|  9 | 歪歪       |  20 |
| 10 | 丫丫       |  20 |
| 11 | 丁丁       |  20 |
| 12 | 星星       |  20 |
| 13 | 格格       |  20 |
| 14 | 張野       |  20 |
| 15 | 程咬金     |  20 |
| 16 | 程咬銀     |  20 |
| 17 | 程咬銅     |  20 |
| 18 | 程咬鐵     |  20 |
+----+------------+-----+
18 rows in set (0.00 sec)
mysql
> explain select * from employee where age = 20; -- age字段沒有索引的情況下的查詢計划,條件為age = 20 +----+-------------+----------+------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+------+-------------+ | 1 | SIMPLE | employee | ALL | NULL | NULL | NULL | NULL | 18 | Using where | +----+-------------+----------+------+---------------+------+---------+------+------+-------------+ 1 row in set (0.00 sec) mysql> explain select * from employee where age = 18; -- age字段沒有索引的情況下的查詢計划,條件為age = 18 +----+-------------+----------+------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+------+-------------+ | 1 | SIMPLE | employee | ALL | NULL | NULL | NULL | NULL | 18 | Using where | +----+-------------+----------+------+---------------+------+---------+------+------+-------------+ 1 row in set (0.00 sec) mysql> create index xxx on employee(age); -- 為age字段添加索引 Query OK, 0 rows affected (0.02 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> explain select * from employee where age = 20; -- age字段沒有索引的情況下的查詢計划,條件為age = 20 此時 滿足age=20的行太多,即便是為age字段加了索引也是無法命中的,看下面的explain計划,key字段為NULL,證明雖然建立了索引,但壓根沒用上 +----+-------------+----------+------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+------+-------------+ | 1 | SIMPLE | employee | ALL | xxx | NULL | NULL | NULL | 18 | Using where | +----+-------------+----------+------+---------------+------+---------+------+------+-------------+ 1 row in set (0.00 sec) mysql> explain select * from employee where age = 18; -- -- age字段沒有索引的情況下的查詢計划,條件為age = 18 查看計划,key字段為xxx,命中了索引,因為age=18的行總共才3行,其實我們通常就應該給那些區分度高的字段加索引,否則加了也是白加,跟沒加一個鳥樣 +----+-------------+----------+------+---------------+------+---------+-------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+-------+------+-------+ | 1 | SIMPLE | employee | ref | xxx | xxx | 4 | const | 3 | NULL | +----+-------------+----------+------+---------------+------+---------+-------+------+-------+ 1 row in set (0.00 sec)
復制代碼

步驟2:驗證未命中索引則鎖表

  事務一 事務二
步驟1  

start transaction;

 

start transaction;

 

步驟2

 

-- 因為條件age=20無法命中索引,所以會鎖住整張表

select * from employee where age = 20 for update;

步驟3

-- 阻塞

select * from employee where age = 16 for update;

 

-- 阻塞

select * from employee where age = 18 for update;

 

-- 阻塞

select * from employee where age = 20 for update;

 
步驟3  

-- 提交一下事務,不要影響下一次實驗

commit;

 

-- 提交一下事務,不要影響下一次實驗

commit;

 

步驟3:驗證命中索引則鎖行

  事務一 事務二
步驟1  

start transaction;

 

start transaction;

 

步驟2

 

-- 因為條件age=18命中了索引,所以會鎖住行而不是表

select * from employee where age = 18 for update;

步驟3

-- 不阻塞

select * from employee where age = 16 for update;

 

-- 阻塞,因為事務二里鎖住了age=18的行

select * from employee where age = 18 for update;

 

-- 阻塞,???,不是說只鎖age=18的行嗎!!!

-- 請看下一小節:Next-Key Lock

select * from employee where age = 20 for update;

 
步驟3  

-- 提交一下事務,不要影響下一次實驗

commit;

 

-- 提交一下事務,不要影響下一次實驗

commit;

 

5.2 三種行鎖的算法

InnoDB有三種行鎖的算法,都屬於排他鎖:

  • 1、Record Lock:單個行記錄上的鎖。
  • 2、Gap Lock:間隙鎖,鎖定一個范圍,但不包括記錄本身。GAP鎖的目的,是為了防止同一事務的兩次當前讀,出現幻讀的情況。
  • 當我們用范圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;
    對於鍵值在條件范圍內但並不存在的記錄,叫做“間隙(GAP)”,InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖)。 # 例如
    例:假如employee表中只有101條記錄,其depid的值分別是 1,2,...,100,101,下面的SQL:
    mysql> select * from emp where depid > 100 for update;是一個范圍條件的檢索,並且命中了索引,InnoDB不僅會對符合條件的empid值為101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的“間隙”加鎖。
  • 3、Next-Key Lock:等於Record Lock結合Gap Lock,也就說Next-Key Lock既鎖定記錄本身也鎖定一個范圍特別需要注意的是,InnoDB存儲引擎還會對輔助索引下一個鍵值加上gap lock

對於行查詢,innodb采用的都是Next-Key Lock,主要目的是解決幻讀的問題,以滿足相關隔離級別以及恢復和復制的需要。

點擊查看詳解:記錄鎖,間隙鎖,next-key

 

案例:

復制代碼
# 准備數據

create table t1(

id int,

key idx_id(id)

)engine=innodb;

 

insert t1

values

(1),

(5),

(7),

(11);

 

mysql> explain select * from t1 where id=7 for update;  -- key字段為idx_id,命中索引,即會采用行鎖而不是表鎖

+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+

| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra       |

+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+

|  1 | SIMPLE      | t1    | ref  | idx_id        | idx_id | 5       | const |    1 | Using index |

+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+

1 row in set (0.00 sec)

復制代碼

實驗

  事務一 事務二
步驟1  

start transaction;

-- 開啟事務

start transaction;

步驟2  

-- 加排他鎖

select * from t1 where id=7 for update;

 

-- 須知

-- 1、上述語句命中了索引,所以加的是行鎖

-- 2、InnoDB對於行的查詢都是采用了Next-Key Lock的算法,鎖定的不是單個值,而是一個范圍(GAP)

表記錄的索引值為1,5,7,11,其記錄的GAP區間如下:

(-∞,1],(1,5],(5,7],(7,11],(11,+∞)

因為記錄行默認就是按照主鍵自增的,所以是一個左開右閉的區間

其中上述查詢條件id=7處於區間(5,7]中,所以Next-Key lock會鎖定該區間的記錄,但是還沒完

-- 3、InnoDB存儲引擎還會對輔助索引下一個鍵值加上gap lock

區間(5,7]的下一個Gap是(7,11],所以(7,11]也會被鎖定

綜上所述,最終確定5-11之間的值都會被鎖定

步驟3

-- 下述sql全都會阻塞在原地

insert t1 values(5);

insert t1 values(6);

insert t1 values(7);

insert t1 values(8);

insert t1 values(9);

insert t1 values(10);

 

-- 下述等sql均不會阻塞

insert t1 values(11); 

insert t1 values(1); 

insert t1 values(2);

insert t1 values(3);

insert t1 values(4);   

 
步驟4

-- 提交一下事務,不要影響下一次實驗

commit;

-- 提交一下事務,不要影響下一次實驗

commit;

 

插入超時失敗后,會怎么樣?

超時時間的參數:innodb_lock_wait_timeout ,默認是50秒。
超時是否回滾參數:innodb_rollback_on_timeout 默認是OFF。

復制代碼
section A:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> 
mysql> select * from t1 where id=7 for update;
section B:

mysql> begin;

Query OK, 0 rows affected (0.00 sec)

 

mysql> insert t1 values(2);

Query OK, 1 row affected (0.00 sec)

 

mysql> 

mysql> insert t1 values(7);

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction  -- 拋出超時異常

mysql> select * from t1;  -- 超時異常並不會被回滾

+------+

| id   |

+------+

|    1 |

|    2 |

|    5 |

|    7 |

|   11 |

+------+

5 rows in set (0.00 sec)

復制代碼

經過測試,不會回滾超時引發的異常,當參數innodb_rollback_on_timeout 設置成ON時,則可以回滾,會把插進去的12回滾掉。

默認情況下,InnoDB存儲引擎不會回滾超時引發的異常,死鎖問題帶來的超時異常除外。

既然InnoDB有三種算法,那Record Lock什么時候用?還是用上面的列子,把輔助索引改成唯一屬性的索引。

測試二:

復制代碼
>create table t(a int primary key)engine =innodb;
Query OK, 0 rows affected (0.19 sec)

>insert into t values(1),(3),(5),(8),(11);
Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0

>select * from t;
+----+
| a  |
+----+
|  1 |
|  3 |
|  5 |
|  8 |
| 11 |
+----+
5 rows in set (0.00 sec)

section A:
>start transaction;
Query OK, 0 rows affected (0.00 sec)

>select * from t where a = 8 for update;
+---+
| a |
+---+
| 8 |
+---+
1 row in set (0.00 sec)

section B:

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

>insert into t values(6);
Query OK, 1 row affected (0.00 sec)

>insert into t values(7);
Query OK, 1 row affected (0.00 sec)

>insert into t values(9);
Query OK, 1 row affected (0.00 sec)

>insert into t values(10);
Query OK, 1 row affected (0.00 sec)
復制代碼

問題:

為什么section B上面的插入語句可以正常,和測試一不一樣?

分析:

因為InnoDB對於行的查詢都是采用了Next-Key Lock的算法,鎖定的不是單個值,而是一個范圍,按照這個方法是會和第一次測試結果一樣。但是,當查詢的索引含有唯一屬性的時候,Next-Key Lock 會進行優化,將其降級為Record Lock,即僅鎖住索引本身,不是范圍。

注意:通過主鍵或則唯一索引來鎖定不存在的值,也會產生GAP鎖定。即: 

復制代碼
會話1:
>show create table t\G
*************************** 1. row ***************************
       Table: t
Create Table: CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

>start transaction;

>select * from t where id = 15 for update;
Empty set (0.00 sec)

會話2:
>insert into t(id,name) values(10,'k');
Query OK, 1 row affected (0.01 sec)

>insert into t(id,name) values(12,'k');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted
>insert into t(id,name) values(16,'kxx');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted
>insert into t(id,name) values(160,'kxx');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted 
復制代碼

如何讓測試一不阻塞?可以顯式的關閉Gap Lock:

1:把事務隔離級別改成:Read Committed,提交讀、不可重復讀。SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

2:修改參數:innodb_locks_unsafe_for_binlog 設置為1。

5.3 死鎖問題

MyISAM中是不會產生死鎖的,因為MyISAM總是一次性獲得所需的全部鎖,要么全部滿足,要么全部等待。而在InnoDB中,鎖是逐步獲得的,就造成了死鎖的可能。

 

  下面,來看看兩個死鎖的例子 (一個是兩個Session的兩條SQL產生死鎖;另一個是兩個Session的一條SQL,產生死鎖):

    

        

    

  上面的兩個死鎖用例。第一個非常好理解,也是最常見的死鎖,每個事務執行兩條SQL,分別持有了一把鎖,然后加另一把鎖,產生死鎖。

 

  第二個用例,只有多個事務同時運行的情況下才可能出現,但隱蔽性極強,雖然每個Session都只有一條語句,仍舊會產生死鎖。要分析這個死鎖,首先必須用到本文前面提到的MySQL加鎖的規則。針對Session 1,從name索引出發,讀到的[hdc, 1],[hdc, 6]均滿足條件,不僅會加name索引上的記錄X鎖,而且會加聚簇索引上的記錄X鎖,加鎖順序為先[1,hdc,100],后[6,hdc,10]。而Session 2,從pubtime索引出發,[10,6],[100,1]均滿足過濾條件,同樣也會加聚簇索引上的記錄X鎖,加鎖順序為[6,hdc,10],后[1,hdc,100]。發現沒有,跟Session 1的加鎖順序正好相反,如果兩個Session恰好都持有了第一把鎖,請求加第二把鎖,死鎖就發生了。  

create table emp(
id int not null unique auto_increment,
name varchar(20) not null,
sex enum('male','female') not null default 'male', #大部分是男的
age int(3) unsigned not null default 28,
hire_date date not null,
post varchar(50)
);


#插入記錄
#三個部門:教學,銷售,運營
insert into emp(name,sex,age,hire_date,post) values
('egon','male',78,'20170301','老男孩駐沙河辦事處外交大使'), #以下是教學部
('zxx','male',19,'20150302','teacher'),
('wupeiqi','male',81,'20130305','teacher'),
('yuanhao','male',73,'20140701','teacher'),
('liwenzhou','male',28,'20121101','teacher'),
('jingliyang','female',18,'20110211','teacher'),
('jinxin','male',18,'19000301','teacher'),
('成龍','male',48,'20101111','teacher'),

('歪歪','female',48,'20150311','sale'),#以下是銷售部門
('丫丫','female',38,'20101101','sale'),
('丁丁','female',18,'20110312','sale'),
('星星','female',18,'20160513','sale'),
('格格','female',28,'20170127','sale'),

('張野','male',28,'20160311','operation'), #以下是運營部門
('程咬金','male',18,'19970312','operation'),
('程咬銀','female',18,'20130311','operation'),
('程咬銅','male',18,'20150411','operation'),
('程咬鐵','female',18,'20140512','operation')
;


create index idx_name on emp(name);
create index idx_age on emp(age);

事務1與事務2同時運行,會出現死鎖
事務1
begin;
update emp set post="IT" where name="egon" or name="zxx";  -- 索引的有序性,'zxx'>'egon',所以在輔助索引中zxx對應的記錄在后
                                                           -- 即 ('egon',主鍵字段為1),('zxx',主鍵id為2),在鎖完輔助索引后鎖主鍵索引時,先鎖主鍵1對應的記錄再鎖2

事務2

begin;
select * from emp where age=78 or age = 19 for update;  -- 原理同上,但在鎖主鍵時,先鎖主鍵2對應的記錄,再鎖1,如果事務1與事務2是同時運行的,則會互相鎖住
示例代碼

 

 

  結論:

復制代碼
# 1、關於死鎖問題需要儲備的知識
在MySQL中,行級鎖並不是直接鎖記錄,而是鎖索引。索引分為主鍵索引和非主鍵索引兩種,
如果一條sql語句操作了主鍵索引,MySQL就會鎖定這條主鍵索引;
如果一條語句操作了非主鍵索引,MySQL會先鎖定該非主鍵索引,再鎖定相關的主鍵索引。
在UPDATE、DELETE操作時,MySQL不僅鎖定WHERE條件掃描過的所有索引記錄,而且會鎖定相鄰的鍵值,即所謂的next-key locking。 # 2、死鎖產生的本質原理
死鎖的發生與否,並不在於事務中有多少條SQL語句,死鎖的關鍵在於:兩個(或以上)的Session加鎖的順序不一致。
而使用本文上面提到的,分析MySQL每條SQL語句的加鎖規則,分析出每條語句的加鎖順序,然后檢查多個並發SQL間是否存在以相反的順序加鎖的情況,
就可以分析出各種潛在的死鎖情況,也可以分析出線上死鎖發生的原因。
復制代碼

發生死鎖后,InnoDB一般都可以檢測到,並使一個事務釋放鎖回退,另一個獲取鎖完成事務。

有多種方法可以避免死鎖,

(1)如果不同程序會並發存取多個表,盡量約定以相同的順序訪問表,可以大大降低死鎖機會。
(2)在同一個事務中,盡可能做到一次鎖定所需要的所有資源,減少死鎖產生概率;
(3)對於非常容易產生死鎖的業務部分,可以嘗試使用升級鎖定顆粒度,通過表級鎖定來減少死鎖產生的概率;
(4)在程序以批量方式處理數據的時候,如果事先對數據排序,保證每個線程按固定的順序來處理記錄,也可以大大降低出現死鎖的可能。

5.4 什么時候使用表鎖

絕大部分情況使用行鎖,但在個別特殊事務中,也可以考慮使用表鎖

1、事務需要更新大部分數據,表又較大
若使用默認的行鎖,不僅該事務執行效率低(因為需要對較多行加鎖,加鎖是需要耗時的); 而且可能造成其他事務長時間鎖等待和鎖沖突; 這種情況下可以考慮使用表鎖來提高該事務的執行速度
2、事務涉及多個表,較復雜,很可能引起死鎖,造成大量事務回滾
這種情況也可以考慮一次性鎖定事務涉及的表,從而避免死鎖、減少數據庫因事務回滾帶來的開銷當然,應用中這兩種事務不能太多,否則,就應該考慮使用MyISAM。

5.5 行鎖優化建議

通過檢查InnoDB_row_lock狀態變量來分析系統上的行鎖的爭奪情況,在着手根據狀態量來分析改善;

show status like 'innodb_row_lock%';//查看行鎖的狀態 
  • 盡可能讓所有數據檢索都通過索引來完成, 從而避免無索引行鎖升級為表鎖
  • 合理設計索引,盡量縮小鎖的范圍
  • 盡可能減少檢索條件,避免間隙鎖
  • 盡量控制事務大小,減少鎖定資源量和時間長度
  • 盡可能低級別事務隔離,詳見下一章節

六、樂觀鎖與悲觀鎖(使用方式)

數據庫管理系統(DBMS)中的並發控制的任務是確保在多個事務同時存取數據庫中同一數據時不破壞事務的隔離性和統一性以及數據庫的統一性。

樂觀並發控制(樂觀鎖)和悲觀並發控制(悲觀鎖)是並發控制主要采用的技術手段。

無論是悲觀鎖還是樂觀鎖,都是人們定義出來的概念,可以認為是一種思想。其實不僅僅是關系型數據庫系統中有樂觀鎖和悲觀鎖的概念,像memcache、hibernate、tair等都有類似的概念。

針對於不同的業務場景,應該選用不同的並發控制方式。所以,不要把樂觀並發控制和悲觀並發控制狹義的理解為DBMS中的概念,更不要把他們和數據中提供的鎖機制(行鎖、表鎖、排他鎖、共享鎖)混為一談。其實,在DBMS中,悲觀鎖正是利用數據庫本身提供的鎖機制來實現的。

下面來分別學習一下悲觀鎖和樂觀鎖。 

6.1、悲觀鎖

悲觀鎖介紹

當我們要對一個數據庫中的一條數據進行修改的時候,為了避免同時被其他人修改,最好的辦法就是直接對該數據進行加鎖以防止並發。

這種借助數據庫鎖機制在修改數據之前先鎖定,再修改的方式被稱之為悲觀並發控制(又名“悲觀鎖”,Pessimistic Concurrency Control,縮寫“PCC”)。

在關系數據庫管理系統里,悲觀並發控制(又名“悲觀鎖”,Pessimistic Concurrency Control,縮寫“PCC”)是一種並發控制的方法。它可以阻止一個事務以影響其他用戶的方式來修改數據。如果一個事務執行的操作都某行數據應用了鎖,那只有當這個事務把鎖釋放,其他事務才能夠執行與該鎖沖突的操作。
悲觀並發控制主要用於數據爭用激烈的環境,以及發生並發沖突時使用鎖保護數據的成本要低於回滾事務的成本的環境中。

悲觀鎖,正如其名,它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度(悲觀),因此,在整個數據處理過程中,將數據處於鎖定狀態。 悲觀鎖的實現,往往依靠數據庫提供的鎖機制 (也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據),現在互聯網高並發的架構中,受到fail-fast思路的影響,悲觀鎖已經非常少見了。

在數據庫中,悲觀鎖的流程如下:

在對任意記錄進行修改前,先嘗試為該記錄加上排他鎖(exclusive locking)。

如果加鎖失敗,說明該記錄正在被修改,那么當前查詢可能要等待或者拋出異常。 具體響應方式由開發者根據實際需要決定。

如果成功加鎖,那么就可以對記錄做修改,事務完成后就會解鎖了。

其間如果有其他對該記錄做修改或加排他鎖的操作,都會等待我們解鎖或直接拋出異常。

ps:行鎖、表鎖、讀鎖、寫鎖都是在操作之前先上排他鎖

在數據表中的實現

在MySQL中使用悲觀鎖,必須關閉MySQL的自動提交,set autocommit=0,因為MySQL默認使用自動提交autocommit模式,在執行完sql后會自動提交並釋放鎖

set autocommit=0;

 

舉例

假設商品表中有一個字段quantity表示當前該商品的庫存量。假設有一件Dulex套套,其id為100,quantity=8個;如果不使用鎖,那么操作方法

如下:

//step1: 查出商品剩余量
 select quantity from items where id=100;
//step2: 如果剩余量大於0,則根據商品信息生成訂單
 insert into orders(id,item_id) values(null,100);
 //step3: 修改商品的庫存
 update Items set quantity=quantity-1 where id=100;

這樣子的寫法,在小作坊真的很正常,No Problems,但是在高並發環境下可能出現問題。

其實在step1或者step2環節,已經有人下單並且減完庫存了,這個時候仍然去執行step3,就造成了超賣

但是使用悲觀鎖,就可以解決這個問題,在上面的場景中,商品信息從查詢出來到修改,中間有一個生成訂單的過程,使用悲觀鎖的原理就是,當我們在查詢出items信息后就把當前的數據鎖定,直到我們修改完畢后再解鎖。那么在這個過程中,因為數據被鎖定了,就不會出現有第三者來對其進行修改了。而這樣做的前提是需要將要執行的SQL語句放在同一個事物中,否則達不到鎖定數據行的目的。

如下
//step1: 查出商品狀態
select quantity from items where id=100 for update;
//step2: 根據商品信息生成訂單
insert into orders(id,item_id) values(null,100);
//step3: 修改商品的庫存
update Items set quantity=quantity-2 where id=100;
select...for update是MySQL提供的實現悲觀鎖的方式。此時在items表中,id為100的那條數據就被我們鎖定了,其它的要執行select quantity from items where id=100 for update的事務必須等本次事務提交之后才能執行。這樣我們可以保證當前的數據不會被其它事務修改。
總結

悲觀並發控制主要用於數據爭用激烈的環境,以及發生並發沖突時使用鎖保護數據的成本要低於回滾事務的成本的環境中。

優點:

    悲觀並發控制實際上是“先取鎖再訪問”的保守策略,為數據處理的安全提供了保證。
缺點:
  (a)在效率方面,處理加鎖的機制會讓數據庫產生額外的開銷,還有增加產生死鎖的機會;
  (b) 在只讀型事務處理中由於不會產生沖突,也沒必要使用鎖,這樣做只能增加系統負載;還有會降低了並行性,一個事務如果鎖定了某行數據,其他事務就必須等待該事務處理完才可以處理那行數

6.2、樂觀鎖

樂觀鎖介紹

在關系數據庫管理系統里,樂觀並發控制(又名“樂觀鎖”,Optimistic Concurrency Control,縮寫“OCC”)是一種並發控制的方法。它假設多用戶並發的事務在處理時不會彼此互相影響,各事務能夠在不產生鎖的情況下處理各自影響的那部分數據。在提交數據更新之前,每個事務會先檢查在該事務讀取數據后,有沒有其他事務又修改了該數據。如果其他事務有更新的話,正在提交的事務會進行回滾。樂觀事務控制最早是由孔祥重(H.T.Kung)教授提出。

樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖假設認為數據一般情況下不會造成沖突,所以在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測,如果發現沖突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。

相對於悲觀鎖,在對數據庫進行處理的時候,樂觀鎖並不會使用數據庫提供的鎖機制。一般的實現樂觀鎖的方式就是記錄數據版本。

數據版本,為數據增加的一個版本標識。當讀取數據時,將版本標識的值一同讀出,數據每更新一次,同時對版本標識進行更新。當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的版本標識進行比對,如果數據庫表當前版本號與第一次取出來的版本標識值相等,則予以更新,否則認為是過期數據。

在數據庫中,樂觀鎖的實現有兩種方式

  • 1、使用版本號實現

每一行數據多一個字段version,每次更新數據對應版本號+1,
原理:讀出數據,將版本號一同讀出,之后更新,版本號+1,提交數據版本號大於數據庫當前版本號,則予以更新,否則認為是過期數據,重新讀取數據

  • 2、使用時間戳實現

每一行數據多一個字段time
原理:讀出數據,將時間戳一同讀出,之后更新,提交數據時間戳等於數據庫當前時間戳,則予以更新,否則認為是過期數據,重新讀取數據 

樂觀鎖舉例

1、需求:

在使用mysql數據庫存儲數據的前提下,有一個搶任務系統,一個任務只能分配給n個人,如果有高並發請求,如何保證數據完整性?

 2、一般做法

在不考慮到數據是否完整的情況下,我們一般只會按照以下思維開發:
1、用戶請求搶任務接口
2、讀取數據庫剩余數量
3、如果大於0,剩余數量減1,更新數據庫剩余數量(update task set 4、4、count=count-1 where id=‘任務id’)
5、返回數據

 

3、出現的問題提以及樂觀鎖的應用

為什么上面的做法不對呢?我們舉個例子,假設用戶1和用戶2同時調用請求搶任務接口,並且數據庫只剩下一個任務可搶,任務剩余數量使用count字段保存;
用戶1和用戶2請求接口情況模擬,表格的每一行表示一個時間點

用戶1 用戶2
執行1 執行1
執行2 執行2
執行3(更新count為0)  
  執行3(更新count為-1)
執行4 執行4


通過以上的問題,我們不難知道,本來只有一個任務可搶的,現在被兩個用戶同時搶了,而且數據庫還出現了-1的情況,而這種情況再高並發的時候經常會遇到。

要解決高並發帶來的問題,就可以利用樂觀鎖的概念來解決。
將上面中的第3個步驟中是sql語句改為(update task set count=count-1 where id=1 and count=1)
當然,其中的count=1中的1是步驟2讀取出來的數據總數。
或者可以給表加一個版本號version字段,默認為1,每次執行更新的時候自增1,並在where語句后帶上讀取到的版本號,以免再讀取和更新數據之間,有第三者更新了數據庫。

用戶1 用戶2
執行1 執行1
執行2 執行2
執行3(更新count,dao層返回1,表示更新成功)  
  執行3(更新count,dao層返回0,表示更新失敗)
執行4 執行4


最后結果

  1. 用戶請求搶任務接口
  2. 讀取數據庫剩余數量
  3. 如果大於0,剩余數量減1,更新數據庫剩余數量(update task set count=count-1 where id=‘任務id’ and count=‘讀取到的剩余數量’)
  4. 返回數據

 

以上SQL其實還是有一定的問題的,就是一旦發上高並發的時候,就只有一個線程可以修改成功,那么就會存在大量的失敗。

對於像淘寶這樣的電商網站,高並發是常有的事,總讓用戶感知到失敗顯然是不合理的。所以,還是要想辦法減少樂觀鎖的粒度的。

有一條比較好的建議,可以減小樂觀鎖力度,最大程度的提升吞吐率,提高並發能力!如下:

//修改商品庫存 
update task set count=count-1 where id=‘任務id’ and count=‘讀取到的剩余數量’ and count-1 >= 0;
 

以上SQL語句中,通過count-1>0的方式進行樂觀鎖控制,商品個數count至少要有1件才可以。

以上update語句,在執行過程中,會在一次原子操作中自己查詢一遍count的值,並將其扣減掉1。

沒錯!你參加過的天貓、淘寶秒殺、聚划算,跑的就是上述這條SQL,通過挑選樂觀鎖,可以減小鎖力度,從而提升吞吐~

樂觀鎖需要靈活運用

現在互聯網高並發的架構中,受到fail-fast思路的影響,悲觀鎖已經非常少見了。

優點與不足

樂觀並發控制相信事務之間的數據競爭(data race)的概率是比較小的,因此盡可能直接做下去,直到提交的時候才去鎖定,所以不會產生任何鎖和死鎖。

如何選擇

在樂觀鎖與悲觀鎖的選擇上面,主要看下兩者的區別以及適用場景就可以了。

1、樂觀鎖並未真正加鎖,效率高。一旦鎖的粒度掌握不好,更新失敗的概率就會比較高,容易發生業務失敗。

2、悲觀鎖依賴數據庫鎖,效率低。更新失敗的概率比較低。

隨着互聯網三高架構(高並發、高性能、高可用)的提出,悲觀鎖已經越來越少的被使用到生產環境中了,尤其是並發量比較大的業務場景。 

 

 


免責聲明!

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



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