Mysql鎖機制解析

MySql支持可插拔的存儲引擎,不同存儲引擎使用的鎖機制不盡相同。MySql常用的存儲引擎爲InnoDB、MyISAM、MEMORY等一般我們在需要數據庫事物支持的互聯場景下主要使用 InnoDB(MySQL 5.5.5以上版本的默認存儲引擎),以下基於InnoDB的鎖機制進行分析
 

事務的隔離級別

隔離級別定義的是併發事物之間的可⻅性和影響程度,爲什麼要有事物隔離級別?

在理想的情況下,事務之間是完全隔離的,這樣就可以避免出現髒讀,不可重複讀,幻讀等問題,且 事物隔離級別越⾼,在併發下會產⽣的問題就越少,但同時付出的性能消耗也將越⼤。因此很多時候必須在併發性和性能之間做⼀個權衡,針對這四種隔離級別,應該根據具體的業務來取捨,如果某個系統的業務⾥根本就不會出現重複讀RR的場景,完全可以將數據庫的隔離級別設置爲 RC,這樣可以最⼤程度的提⾼數據庫的併發性

事務的ACID特性:

  • 原子性(Atomicity)
  • 一致性(Consistency)
  • 隔離性(Isolation)
  • 持久性(Durability)

事務的隔離級別:

  • 讀未提交(READ UNCOMMITTED)
  • 讀已提交(READ COMMITTED)
  • 可重複讀(REPEATABLE READ)
  • 串行化(SERIALIZABLE)

事務併發引起的問題:

  • 髒讀(DIRTY READ)
  • 不可重複讀(UNREPEATABLE READ)
  • 幻讀(PHANTOM READ)

其中:

讀未提交:.可以讀取未提交的記錄,會出現髒讀,幻讀, 不可重複讀,所有併發問題都可能遇到

讀已提交:事務中只能看到已提交的修改,不會出現髒讀,但是會出現幻讀,不可重複讀(⼤多數數據庫的默認隔離級別都是 RC,如Oracle、Mysql非InnoDb,InnoDb 默認是 RR)

可重複讀:解決了不可重複讀問題,但是仍然存在幻讀問題.(MySQL通過MVCC+GAP間隙鎖解決了幻讀)

序列化:“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖衝突的時 ,後訪問的事務必須等前⼀個事務執⾏完成,沒有併發問題.

SQL 規範中定義的四種隔離級別,分別是爲了解決事務併發時可能遇到的問題,⾄於如何解決,實現⽅式是什麼,規範中並沒有嚴格定義。鎖作爲最簡單最顯⽽易⻅的實現⽅式被應⽤在很多數據庫中。除了鎖,實現併發問題的⽅式還時間戳多版本控制等。這些也可以稱爲⽆鎖的併發控制

 

基於鎖的隔離級別併發控制(Lock-Based Concurrent Control,簡寫 LBCC)

通過對讀寫操作加不同的鎖,以及對釋放鎖的時機進⾏不同的控制,就可以實現四種隔離級別。傳統的鎖有兩種:讀操作通常加共享鎖(Share locks,S鎖,⼜叫讀鎖),操作加排它鎖(Exclusive locks,X鎖,⼜叫寫鎖);加了共享鎖的記錄,其他事務也可以讀,但不能寫;加了排它鎖的記錄,其他事務既不能讀,也不能寫。另外,對於鎖的粒度⼜分爲⾏鎖和表鎖,⾏鎖只鎖⼀⾏或某⼏⾏記錄,對其它⾏的操作不受影響,表鎖會鎖住整張表,所有對這個表的操作都受影響。

寫鎖/讀鎖互斥關係

X

S

X

S

通過對鎖的類型(讀鎖還是寫鎖),鎖的粒度(⾏鎖還是表鎖),持有鎖的時間(臨時鎖:語句執⾏完後就釋放鎖和持續鎖:事物結束才釋放鎖)合理的進⾏組合,就可以實現四種不同的隔離級別。(理論模型,認識不同的鎖和互斥關係,可以實現不同的事物隔離級別)

  • 讀未提交:通過對寫操作加 “持續X鎖”,對讀操作不加鎖實現;(寫操作加鎖是爲了防⽌出現回滾覆蓋,也叫做第⼀類更新丟失,數據庫任何隔離級別下都不允許出現),事務讀不阻塞其他事務讀和寫,事務寫阻塞其他事務寫但不阻塞讀;
  • 讀已提交:通過對寫操作加 持續X鎖”,對讀操作加 臨時S鎖” 實現; 不會出現髒讀;事務讀不會阻塞其他事務讀和寫,事務寫會阻塞其他事務讀和寫;
  • 可重複讀通過對寫操作加 “持續X鎖”,對讀操作加 “持續S鎖” 實現;事務讀會阻塞其他事務寫但不阻塞讀,事務寫會阻塞其他事務讀和寫;
  • 序列化:爲了解決幻讀問題,⾏級鎖做不到,需使⽤表級鎖。

其中:

第一類更新丟失(回滾丟失,Lost update:如A事務撤銷時,把已經提交的B事務的更新數據覆蓋了。這種錯誤可能造成很嚴重的問題,通過下面的賬戶取款轉賬就可以看出來:

第二類丟失更新(覆蓋丟失/兩次更新問題,Second lost update) :A事務覆蓋B事務已經提交的數據,造成B事務所做操作丟失  

 

基於MVCC的隔離級別併發控制(Multi-Version Concurrent Control,MVCC)

LBCC最⼤的問題是它只實現了併發的讀讀,對於併發的讀寫還是衝突的,寫時不能讀,讀時不能寫。當讀寫操作都很頻繁時,數據庫的併發性將⼤⼤降低,針對這種場景,MVCC 技術應運而生,MVCC 的全稱叫做 Multi-Version Concurrent Control(版本併發控制),InnoDb  會爲每⼀⾏記錄增加⼏個隱含的“輔助字段”(ROWID、事物ID、回滾指針)。事務在更新⼀條記錄時會將其拷⻉⼀份⽣成這條記錄的⼀個原始拷⻉,寫操作同樣還是會對原記錄加鎖,但是讀操作會讀取未加鎖的新記錄,即通過維持一個數據的多個版本,使得讀寫操作沒有衝突,保證了讀寫並⾏。要注意的是,生成的新版本其實就是 undo log,它也是實現事務回滾的關鍵技術。

MVCC的實現原理
在數據庫中的實現,爲每行記錄添加 3個隱式字段,字段名:DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID

  • DB_TRX_ID:6byte,最近修改(修改/插入)事務ID:記錄創建這條記錄/最後一次修改該記錄的事務ID
  • DB_ROLL_PTR:7byte,回滾指針,指向這條記錄的上一個版本(存儲於rollback segment裏)
  • DB_ROW_ID:6byte,隱含的自增ID(隱藏主鍵),如果數據表沒有主鍵,InnoDB會自動以DB_ROW_ID產生一個聚簇索引
  • 實際還有一個刪除flag隱藏字段, 既記錄被更新或刪除並不代表真的刪除,而是刪除flag變了

從上圖中可以看出,每次修改都會新增一個undo log,各個undo log之間通過指針連接,不同事務或者相同事務的對同一記錄的修改,會導致該記錄的undo log成爲一條記錄版本線性表,既鏈表,undo log的鏈首就是最新的舊記錄,鏈尾就是最早的舊記錄

快照讀和當前讀

mysql通過MVCC來實現RR和RC隔離級別下的讀寫並⾏,它們讀取的都是快照數據,並不會被寫操作阻塞,所以這種讀操作稱爲 快照讀(Snapshot Read)。除了快照讀 , MySQL 還提供了另⼀種讀取⽅式:當前讀(Current Read),有時候⼜叫做 加鎖讀(Locking Read) 或者 阻塞讀(Blocking Read),這種讀操作讀的不再是數據的快照版本,⽽是數據的最新版本,並會對數據加鎖(當前讀在 RR 和 RC 兩種隔離級別下的實現也是不⼀樣的:RC 只加記錄鎖,RR 除了加記錄鎖,還會加間隙鎖,用於解決幻讀問題

Read View(讀視圖)
Read View就是事務進行快照讀操作的時候生產的讀視圖(Read View),在該事務執行的快照讀的那一刻,會生成數據庫系統當前的一個快照,記錄並維護系統當前活躍事務的ID(當每個事務開啓時,都會被分配一個ID, 這個ID是遞增的,所以最新的事務,ID值越大),所以我們知道 Read View主要是用來做可見性判斷的, 即當我們某個事務執行快照讀的時候,對該記錄創建一個Read View讀視圖,把它比作條件用來判斷當前事務能夠看到哪個版本的數據,既可能是當前最新的數據,也有可能是該行記錄的undo log裏面的某個版本的數據。

Read View遵循一個可見性算法,主要是將要被修改的數據的最新記錄中的DB_TRX_ID(即當前事務ID)取出來,與系統當前其他活躍事務的ID去對比(由Read View維護),如果DB_TRX_ID跟Read View的屬性做了某些比較,不符合可見性,那就通過DB_ROLL_PTR回滾指針去取出Undo Log中的DB_TRX_ID再比較,即遍歷鏈表的DB_TRX_ID(從鏈首到鏈尾,即從最近的一次修改查起),直到找到滿足特定條件的DB_TRX_ID, 那麼這個DB_TRX_ID所在的舊記錄就是當前事務能看見的最新老版本

根據上圖可以看出,存在一個數值列表read_view_list,用來維護Read View生成時刻系統正活躍的事務ID,其中:tmin是記錄read_view_list列表中事務ID最小的ID,tmax記錄的是read_view_list列表中事務ID最大的ID,可見性算法主要如下:

  1. 首先比較DB_TRX_ID < tmin, 如果小於,則當前事務能看到DB_TRX_ID 所在的記錄,
  2. 如果大於等於進入下一個判斷,接下來判斷 DB_TRX_ID 大於 tmax , 如果大於則代表DB_TRX_ID 所在的記錄在Read View生成後纔出現的,那對當前事務肯定不可見
  3. 如果小於則進入下一個判斷,判斷DB_TRX_ID 是否在活躍事務之中,trx_list.contains(DB_TRX_ID),如果在,則代表Read View生成時刻,當前這個事務還在活躍,還沒有Commit,當前事務修改的數據是看不見的;如果不在,則說明,當前這個事務在Read View生成之前就已經Commit了,當前事務修改的數據是是能看見的

undo日誌
undo log主要分爲兩種:

insert undo log:代表事務在insert新記錄時產生的undo log, 只在事務回滾時需要,並且在事務提交後可以被立即丟棄;

update undo log:事務在進行update或delete時產生的undo log; 不僅在事務回滾時需要,在快照讀時也需要;所以不能隨便刪除,只有在快速讀或事務回滾不涉及該日誌時,對應的日誌纔會被purge線程統一清除
purge線程:爲了實現InnoDB的MVCC機制,更新或者刪除操作都只是設置一下老記錄的deleted_bit,並不真正將過時的記錄刪除。爲了節省磁盤空間,InnoDB有專門的purge線程來清理deleted_bit爲true的記錄。爲了不影響MVCC的正常工作,purge線程自己也維護了一個read view(read view相當於系統中最老活躍事務的read view);如果某個記錄的deleted_bit爲true,並且DB_TRX_ID相對於purge線程的read view可見,那麼這條記錄一定是可以被安全清除的。從這裏的回滾日誌也側⾯說明長事物的性能影響,長事物不僅會佔用更多的鎖資源,也會產⽣⼤量的回滾日誌。

在 read uncommit 隔離級別下,每次都是讀取最新版本的數據⾏,所以不能⽤ MVCC 的多版本,⽽ serializable 隔離級別每次讀取操作都會爲記錄加上讀鎖,也和 MVCC 不兼容,所以只有 RC 和 RR 這兩個隔離級別纔有 MVCC,那麼RC,RR級別下的InnoDB快照讀有什麼不同?
正是Read View生成時機的不同,從而造成RC,RR級別下快照讀的結果的不同,在RR級別下的某個事務的對某條記錄的第一次快照讀會創建一個快照及Read View, 將當前系統活躍的其他事務記錄起來,此後在調用快照讀的時候,還是使用的是同一個Read View,所以只要當前事務在其他事務提交更新之前使用過快照讀,那麼之後的快照讀使用的都是同一個Read View,所以對之後的修改不可見;
即RR級別下,快照讀生成Read View時,Read View會記錄此時所有其他活動事務的快照,這些事務的修改對於當前事務都是不可見的。而早於Read View創建的事務所做的修改均是可見
而在RC級別下的,事務中,每次快照讀都會新生成一個快照和Read View, 這就是我們在RC級別下的事務中可以看到別的事務提交的更新的原因
總之在RC隔離級別下,是每個快照讀都會生成並獲取最新的Read View;而在RR隔離級別下,則是同一個事務中的第一個快照讀纔會創建Read View, 之後的快照讀獲取的都是同一個Read View。

 

常見鎖類型

鎖的基礎分類:鎖的類型(讀鎖還是寫鎖),鎖的粒度(⾏鎖還是表鎖),持有鎖的時間(臨時鎖還是持續鎖)。在 MySQL 中鎖的種類有很多,但是最基本的還是表鎖和行鎖:

表鎖指的是對⼀整張表加鎖,⼀般是 DDL 處理時使⽤,也可以⾃⼰在 SQL 中指定。

表鎖由 MySQL 服務器實現,行鎖由存儲引擎實現 。InnoDB支持行鎖,而 MyISAM 存儲引擎只能使用表鎖

表鎖:開銷小,加鎖快,不會出現死鎖,鎖的粒度大,發生鎖衝突的概率高,併發度低

行鎖指的是鎖定某⼀行數據或某幾行,或行和行之間的間隙。行鎖的加鎖方法比較複雜,但是由於只鎖住有限的數據,對於其它數據不加限制,所以併發能力強,通常都是用行鎖來處理併發事務。

行鎖:開銷大,加鎖慢,會出現死鎖,鎖定粒度小,發生鎖衝突的概率低,併發度高 

表鎖

關於表鎖,我們要了解它的加鎖和解鎖原則。要注意的是它使⽤的是⼀次封鎖技術,也就是說,我們會在會話開始的地方使用 lock 命令將後面所有要用到的表加上鎖,在鎖釋放之前,我們只能訪問這些加鎖的表,不能訪問其他的表,最後通過unlock tables 釋放所有表鎖。

mysql root @localhost :study> lock table vote_record read,t_item write; Query OK, 0 rows affected
Time: 0.006s
mysql root @localhost :study> select id,user_id,vote_id from vote_record limit 1;
+----+----------------------+---------+
| id | user_id	| vot e_id |
+----+----------------------+---------+
| 1   | a56351fd5b013bd0bf 3f | 985	|
+----+----------------------+---------+
1 row in set Time: 0.015s
mysql root @localhost :study> update vote_record set vote_id=905 where id=1; (1099, "Table 'vote_record' was locked with a READ lock and can't be updated") mysql root @localhost :study> unlock tables;

MySQL 表鎖的加鎖規則如下

InnoDB如何加表鎖:

在用 LOCK TABLES對InnoDB表加鎖時要注意,要將AUTOCOMMIT設爲0,否則MySQL不會給表加鎖;COMMIT或ROLLBACK並不能釋放用LOCK TABLES加的表級鎖,必須用UNLOCK TABLES釋放表鎖。UNLOCK TABLES會隱含地提交事務

SET AUTOCOMMIT=0;
LOCK TABLES t1 WRITE, t2 READ, ...;
[do something with tables t1 and t2 here];
COMMIT;
UNLOCK TABLES;

 對於讀鎖:lock tableName read

  • 持有讀鎖的會話可以讀表,但不能寫表;
  • 允許多個會話同時持有讀鎖;
  •  其他會話就算沒有給表加讀鎖,也是可以讀表的,但是不能寫表;
  • 其他會話申請該表寫鎖時會阻塞,直到鎖釋放。

對於寫鎖:lock tableName write

  • 持有寫鎖的會話既可以讀表,也可以寫表;
  • 只有持有寫鎖的會話纔可以訪問該表,其他會話訪問該表會被阻塞,直到鎖釋放;
  • 其他會話⽆論申請該表的讀鎖或寫鎖,都會阻塞,直到鎖釋放。

表鎖的釋放規則如下:

  • 使⽤ UNLOCK TABLES 語句可以顯示釋放表鎖
  • 如果會話在持有表鎖的情況下執⾏ LOCK TABLES 語句,將會釋放該會話之前持有的鎖
  • 如果會話在持有表鎖的情況下執⾏ START TRANSACTION 或 BEGIN 開啓⼀個事務,將會釋放該會話之前持有的鎖
  • 如果會話連接斷開,將會釋放該會話所有的鎖

MySQL 行鎖的加鎖規則如下

  • SELECT ... LOCK IN SHARE MODE:加 S 鎖
  • SELECT ... FOR UPDATE:加 X 鎖
  • INSERT / UPDATE / DELETE:加 X 鎖
  • 常⻅的增刪改(INSERT、DELETE、UPDATE)語句會⾃動對操作的數據⾏加X鎖,
  • 對於普通SELECT語句,InnoDB不會加任何鎖

查詢的時候也可以明確指定鎖的類型,其中:SELECT ... LOCK IN SHARE MODE 語句加的是讀鎖,SELECT ... FOR UPDATE 語句加的是寫鎖。在 MySQL 中,行鎖是加在索引上的,MySQL 有兩種索引類型:主鍵索引(Primary Index)和⾮主鍵索引(Secondary Index,⼜稱爲⼆級索引、輔助索引,細分⼜可以分爲唯⼀索引、普通索引)

InnoDB行鎖是通過給索引上的索引項加鎖來實現的。所以,只有通過索引條件檢索數據,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖。注意事項如下:

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

測試用表和數據

Table	| student
Create Table | CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`stu_no` varchar(20) COLLATE utf8_bin NOT NULL COMMENT '學號',
`name` varchar(20) COLLATE utf8_bin NOT NULL COMMENT '姓名',
`age` int(11) NOT NULL COMMENT '年齡',
`score` int(11) NOT NULL DEFAULT '0' COMMENT '學分',
PRIMARY KEY (`id`),
UNIQUE KEY `student_stu_no_uindex` (`stu_no`),
KEY `student_name_index` (`name`),
KEY `student_age_index` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin

 mysql root@localhost:study> select * from student;
+-----+--------+------+-----+-----+-----+
| id | stu_no   | name  | age   | score |
+-----+--------+------+-----+-----+-----+
| 1	 | S0001	| Bob	| 25	| 34	|
| 3	 | S0002	| Tom	| 23	| 50	|
| 5	 | S0003	| Eric  | 26	| 27	|
| 10 | S0004	| Rain  | 21	| 48	|
| 20 | S0005	| Tom	| 23	| 8	    |
| 30 | S0006	| Kobe  | 19	| 28	|
| 40 | S0007	| Rose  | 25	| 52	|
| 41 | S0008	| Jim	| 26	| 19	|
| 42 | S0009	| Zoom  | 22	| 90	|
| 50 | S0010	| Tom	| 24	| 81	|
+----+--------+------+-----+------+-----+

1、主鍵加鎖

update student set score=35 where id=1

InnoDb存儲引擎會在id=1這個主鍵索引上加一把X鎖

2、普通索引加鎖

update student set score =35 where name ='Rose'

InnoDb存儲引擎會在name='Rose'這個索引上加一把X鎖,同時會通過name='Rose'這個二級索引定位到id=40這個主鍵索引,並在id=40這個主鍵索引上加一把X鎖

3、多條記錄加鎖

update student set score=28 where name ='Tom'

當UPDATE語句被髮給Mysql之後,Mysql Server會根據Where條件讀取第一條滿足條件的記錄,然後InnoDB引擎將第一條記錄返回並加鎖(Current read),待Mysql Server收到這條加鎖的記錄之後們會在發起一個UPDATE請求,更新這條記錄,一條操作記錄完成在讀取下一條記錄,直到讀完所有滿足條件的記錄,Mysql在操作多條記錄時InnoDB與Mysql Sever的交互式一條一條進行的,加鎖也是一條一條進行的,先對一條滿足條件的記錄加鎖,返回給Mysql,做DML操作後,繼續下一條,直至讀取完畢

 

可以根據多個事務的讀取進行驗證

行鎖的種類

行鎖根據場景的不同又可以進一步細分,Mysql定義了四種類型的行鎖,如下:

  • LOCK_ORDINARY:也稱爲Next-key Lock,鎖一條記錄及其之前的間隙,這是RR隔離級別用的最多的鎖(RR下才有)
  • LOCK_GAP:間隙鎖,鎖兩個記錄之間的GAP,防止記錄插入(RR下才有)
  • LOCK_REC_NOT_GAP:只鎖記錄
  • LOCK_INSERT_INTENSION:插入意向GAP鎖,插入記錄時使用,是LOCK_GAP的一種特例

鎖模式分類

  • LOCK_IS:讀意向鎖
  • LOCK_IX:寫意向鎖
  • LOCK_S:讀鎖
  • LOCK_X:寫鎖

將鎖分爲讀鎖和寫鎖主要是爲了提高鎖的併發,如果不區分讀寫,那麼數據庫將沒辦法併發讀,併發性大大降低,而IS(讀意向鎖)、IX(寫意向鎖)只會應用到表鎖上,方便表鎖和行鎖之間的衝突檢查

讀寫鎖

讀鎖,又稱共享鎖(Shared locks,簡稱S鎖),加了讀鎖的記錄所有事物都可以讀取,但是不能修改,並且可同時有多個事務對記錄加讀鎖,寫鎖,又稱排他鎖(Exclusive locks,簡稱X鎖),或者獨佔鎖,對記錄加了排他鎖後,只有擁有該鎖的事務可以讀取和修改,其他事物不允許讀取或者修改,並且同一時間只能有一個事務加寫鎖(這裏說的讀都是當前讀,快照讀無需加鎖,記錄上無論有沒有鎖,都可以快照讀)

讀寫意向鎖

表鎖鎖定了整張表,而行鎖是鎖定表中的某條記錄,他們鎖定的範圍存在交集,因此表鎖和行鎖是存在衝突的,如某個表中有10000行記錄,其中有一題條記錄加了X鎖,如果這個時候需要對錶添加表鎖,爲了判斷是否可以加這個表鎖,系統需要便利表中的10000行記錄,看看是否有某些記錄被加了行鎖,如果有則不允許加表鎖,顯然這種方式的效率極低,所以引入意向鎖

意向鎖是表級鎖,也可分爲讀意向鎖和寫意向鎖,當事務試圖讀或者寫一條記錄時,會現在表上加上意向鎖,然後再要操作的記錄上添加讀鎖或者寫鎖,這樣判斷表是否有記錄行鎖就非常簡單,只需要看錶中是否存在意向鎖即可,意向鎖之間是不會產生衝突的,他只會阻塞表級讀鎖或者表級寫鎖,另外意向鎖也不會和行鎖衝突,行鎖只會與行鎖產生衝突

 

 

查看行級鎖的爭用情況

執行SQL:mysql> show status like 'InnoDB_row_lock%';

mysql> show status like 'InnoDB_row_lock%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| InnoDB_row_lock_current_waits | 0     |
| InnoDB_row_lock_time          | 0     |
| InnoDB_row_lock_time_avg      | 0     |
| InnoDB_row_lock_time_max      | 0     |
| InnoDB_row_lock_waits         | 0     |
+-------------------------------+-------+

如果發現鎖爭用比較嚴重,還可以通過設置InnoDB Monitors 來進一步觀察發生鎖衝突的表、數據行等,並分析鎖爭用的原因。如:

設置監視器:mysql> create table InnoDB_monitor(a INT) engine=InnoDB;

查看:mysql> show engine InnoDB status;

停止查看:mysql> drop table InnoDB_monitor;

 

間隙鎖(Next-Key鎖)

間隙鎖定義:

nnodb的鎖定規則是通過在指向數據記錄的第一個索引鍵之前和最後一個索引鍵之後的空域空間上標記鎖定信息而實現的。 Innodb的這種鎖定實現方式被稱爲“ NEXT-KEY locking” (間隙鎖),因爲Query執行過程中通過範圍查找的話,它會鎖定整個範圍內所有的索引鍵值,即使這個鍵值並不存在。

例:假如emp表中只有101條記錄,其empid的值分別是 1,2,…,100,101,下面的SQL:

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


是一個範圍條件的檢索,InnoDB不僅會對符合條件的empid值爲101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的“間隙”加鎖。

間隙鎖的缺點:

  • 間隙鎖有一個比較致命的弱點,就是當鎖定一個範圍鍵值之後,即使某些不存在的鍵值也會被無辜的鎖定,而造成在鎖定的時候無法插入鎖定鍵值範圍內的任何數據。在某些場景下這可能會對性能造成很大的危害
  • 當Query無法利用索引的時候, Innodb會放棄使用行級別鎖定而改用表級別的鎖定,造成併發性能的降低;
  • 當Quuery使用的索引並不包含所有過濾條件的時候,數據檢索使用到的索引鍵所指向的數據可能有部分並不屬於該Query的結果集的行列,但是也會被鎖定,因爲間隙鎖鎖定的是一個範圍,而不是具體的索引鍵;
  • 當Query在使用索引定位數據的時候,如果使用的索引鍵一樣但訪問的數據行不同的時候(索引只是過濾條件的一部分),一樣會被鎖定

間隙鎖的作用:

  • 防止幻讀,以滿足相關隔離級別的要求。
  • 爲了數據恢復和複製的需要。

注意

  • 在實際應用開發中,尤其是併發插入比較多的應用,我們要儘量優化業務邏輯,儘量使用相等條件來訪問更新數據,避免使用範圍條件。
  • InnoDB除了通過範圍條件加鎖時使用間隙鎖外,如果使用相等條件請求給一個不存在的記錄加鎖,InnoDB也會使用間隙鎖。

 

死鎖

什麼是死鎖:你等我釋放鎖,我等你釋放鎖就會形成死鎖。

如何發現死鎖: 在InnoDB的事務管理和鎖定機制中,有專門檢測死鎖的機制,會在系統中產生死鎖之後的很短時間內就檢測到該死鎖的存在

解決辦法:

  • 回滾較小的那個事務
  • 在REPEATABLE-READ隔離級別下,如果兩個線程同時對相同條件記錄用SELECT…FOR UPDATE加排他鎖,在沒有符合該條件記錄情況下,兩個線程都會加鎖成功。程序發現記錄尚不存在,就試圖插入一條新記錄,如果兩個線程都這麼做,就會出現死鎖。這種情況下,將隔離級別改成READ COMMITTED,就可避免問題。

判斷事務大小:事務各自插入、更新或者刪除的數據量

注意:

當產生死鎖的場景中涉及到不止InnoDB存儲引擎的時候,InnoDB是沒辦法檢測到該死鎖的,這時候就只能通過鎖定超時限制參數InnoDB_lock_wait_timeout來解決。


優化行級鎖定

InnoDB存儲引擎由於實現了行級鎖定,雖然在鎖定機制的實現方面所帶來的性能損耗可能比表級鎖定會要更高一些,但是在整體併發處理能力方面要遠遠優於MyISAM的表級鎖定的。當系統併發量較高的時候,InnoDB的整體性能和MyISAM相比就會有比較明顯的優勢了。但是,InnoDB的行級鎖定同樣也有其脆弱的一面,當我們使用不當的時候,可能會讓InnoDB的整體性能表現不僅不能比MyISAM高,甚至可能會更差。

(1)要想合理利用InnoDB的行級鎖定,做到揚長避短,我們必須做好以下工作: 

  • 儘可能讓所有的數據檢索都通過索引來完成,從而避免InnoDB因爲無法通過索引鍵加鎖而升級爲表級鎖定; 
  • 合理設計索引,讓InnoDB在索引鍵上面加鎖的時候儘可能準確,儘可能的縮小鎖定範圍,避免造成不必要的鎖定而影響其他Query的執行; 
  • 儘可能減少基於範圍的數據檢索過濾條件,避免因爲間隙鎖帶來的負面影響而鎖定了不該鎖定的記錄; 
  • 儘量控制事務的大小,減少鎖定的資源量和鎖定時間長度; 
  • 在業務環境允許的情況下,儘量使用較低級別的事務隔離,以減少MySQL因爲實現事務隔離級別所帶來的附加成本。

(2)由於InnoDB的行級鎖定和事務性,所以肯定會產生死鎖,下面是一些比較常用的減少死鎖產生概率的小建議: 

  • 類似業務模塊中,儘可能按照相同的訪問順序來訪問,防止產生死鎖; 
  • 在同一個事務中,儘可能做到一次鎖定所需要的所有資源,減少死鎖產生概率; 
  • 對於非常容易產生死鎖的業務部分,可以嘗試使用升級鎖定顆粒度,通過表級鎖定來減少死鎖產生的概率。

 

六、表級鎖

表級鎖的類型:

表級鎖的兩種類型:表共享讀鎖(Table Read Lock)和表獨佔寫鎖(Table Write Lock)。

表級鎖模式的兼容性:

對MyISAM表的讀操作,不會阻塞其他用戶對同一表的讀請求,但會阻塞對同一表的寫操作;
對MyISAM表的寫操作,則會阻塞其他用戶對同一表的讀和寫操作;
MyISAM表的讀操作與寫操作之間,以及寫操作之間是串行的。當一個線程獲得對一個表的寫鎖後,只有持有鎖的線程可以對錶進行更新操作。其他線程的讀、寫操作都會等待,直到鎖被釋放爲止。

 

表級鎖的加鎖:

  • 在執行查詢語句(select)前,會自動給涉及的所有表加讀鎖
  • 在執行更新操作(update、delete、insert等)前,會自動給涉及的表加寫鎖。這個過程並不需要用戶干預,因此不需要直接用lock table命令給MyISAM表顯式加鎖。

當然可以顯示的加鎖,如下:

顯示加寫鎖:

// 當一個線程獲得對一個表的寫鎖後,只有持有鎖的線程可以對錶進行更新操作。
// 其他線程的讀、寫操作都會等待,直到鎖被釋放爲止。
// test表將會被鎖住,另一個線程執行select * from test where id = 3;將會一直等待,直到test表解鎖
LOCK TABLE test WRITE; 

顯示加讀鎖

// test表將會被鎖住,另一個線程執行select * from test where id = 3;不會等待
// 執行UPDATE test set name='peter' WHERE id = 4;將會一直等侍,直到test表解鎖
LOCK table test READ;

顯示釋放鎖:

UNLOCK TABLES;

需要注意的是,在同一個SQL session裏,如果已經獲取了一個表的鎖定,則對沒有鎖的表不能進行任何操作,否則會報錯。

// 鎖定test表
LOCK table test WRITE;

// 操作鎖定表沒問題
SELECT * from test where id = 4;

// 操作沒有鎖的表會報錯
SELECT * from bas_farm where id =1356

報錯:[Err] 1100 - Table 'bas_farm' was not locked with LOCK TABLES。這是因爲MyISAM希望一次獲得sql語句所需要的全部鎖。這也正是myisam表不會出現死鎖的原因。

當然,你也不必擔心,MyISAM引擎的默認方式是會給同一個session裏的所有表都加上鎖的,不會麻煩你自己顯示操作的。


查看錶級鎖爭用情況

執行SQL:mysql> show status like ‘table%’;

mysql> show status like 'table%';
+----------------------------+-----------+
| Variable_name              | Value     |
+----------------------------+-----------+
| Table_locks_immediate      | 20708     |
| Table_locks_waited         | 0         |
+----------------------------+-----------+

Table_locks_immediate:產生表級鎖定的次數; 
Table_locks_waited:出現表級鎖定爭用而發生等待的次數; 
如果Table_locks_waited狀態值比較高,那麼說明系統中表級鎖定爭用現象比較嚴重,就需要進一步分析爲什麼會有較多的鎖定資源爭用了。

 

優化表級鎖定

優化表級鎖時的最大問題是:提高併發度

1. 通過減少查詢時間縮短鎖定時間

  • 縮短鎖定時間的總體原則是:讓Query執行時間儘可能的短。
  • 儘量減少大的、複雜的Query,將複雜Query分拆成幾個小的Query分步執行;
  • 儘可能的建立足夠高效的索引,讓數據檢索更迅速;
  • 儘量讓MyISAM存儲引擎的表只存放必要的信息,控制字段類型;
  • 利用合適的機會優化MyISAM表數據文件。

2. 設置可併發插入:concurrent_insert=2

MyISAM的表鎖雖是讀寫互相阻塞的,但依然能夠實現並行操作。MyISAM存儲引擎有一個控制是否打開Concurrent Insert(併發插入)功能的參數選項:concurrent_insert,取值範圍爲0,1,2。

  • concurrent_insert=0,不允許併發插入。
  • concurrent_insert=1,如果MyISAM表中沒有空洞(即表的中間沒有被刪除的行),MyISAM允許在一個線程讀表的同時,另一個線程從表尾插入記錄。這是MySQL的默認設置;
  • concurrent_insert=2,無論MyISAM表中有沒有空洞,都允許在表尾併發插入記錄;

所以,我們可通過設置concurrent_insert=2,同時定期在系統空閒時段執行optimize table tableName語句來整理空間碎片,收回因刪除記錄而沒有真正釋放的空間,從而提高併發。optimize參考:mysql中OPTIMIZE TABLE的作用及使用

3. 合理設置讀寫優先級

MyISAM存儲引擎默認是寫優先級大於讀優先級。即使是寫請求後到,寫鎖也會插到讀鎖請求之前。

但是,有時像修改文章點擊數 操作是不那麼重要的,我們希望的是讀更快,此時我們可以這樣:

UPDATE  LOW_PRIORITY  article SET click_num=134 WHERE id = 823

LOW_PRIORITY使得系統認爲update操作優化級比讀操作低,如果同時出現讀操作和上面的更新操作,則優先執行讀操作。

MySQL提供了幾個語句調節符,允許你修改它的調度策略:

  • LOW_PRIORITY關鍵字應用於:DELETE、INSERT、LOAD DATA、REPLACE和UPDATE。
  • HIGH_PRIORITY關鍵字應用於:SELECT、INSERT語句。
  • DELAYED(延遲)關鍵字應用於:INSERT、REPLACE語句。

如果你希望所有支持LOW_PRIORITY選項的語句都默認地按照低優先級來處理,那麼可能使用low-priority-updates選項來啓動服務器。然後可通過使用insert HIGH_PRIORITY table.....來把個別我們希望的INSERT語句提高到正常的寫入優先級。

 

七、意向鎖

爲什麼沒有意向鎖的話,表鎖和行鎖不能共存?

舉個粟子(此時假設行鎖和表鎖能共存): 事務A鎖住表中的一行(寫鎖)。事務B鎖住整個表(寫鎖)。

但你就會發現一個很明顯的問題,事務A既然鎖住了某一行,其他事務就不可能修改這一行。這與”事務B鎖住整個表就能修改表中的任意一行“形成了衝突。所以,沒有意向鎖的時候,行鎖與表鎖共存就會存在問題!

 

意向鎖是如何讓表鎖和行鎖共存的?

有了意向鎖之後,前面例子中的事務A在申請行鎖(寫鎖)之前,數據庫會自動先給事務A申請表的意向排他鎖。當事務B去申請表的寫鎖時就會失敗,因爲表上有意向排他鎖之後事務B申請表的寫鎖時會被阻塞。

所以,意向鎖的作用就是:

當一個事務在需要獲取資源的鎖定時,如果該資源已經被排他鎖佔用,則數據庫會自動給該事務申請一個該表的意向鎖。如果自己需要一個共享鎖定,就申請一個意向共享鎖。如果需要的是某行(或者某些行)的排他鎖定,則申請一個意向排他鎖

注:意向共享鎖可以同時並存多個,但是意向排他鎖同時只能有一個存在。

 

意向鎖是表鎖還是行鎖?

首先可以肯定的是,意向鎖是表級別鎖。意向鎖是表鎖是有原因的。

當我們需要給一個加表鎖的時候,我們需要根據意向鎖去判斷表中有沒有數據行被鎖定,以確定是否能加成功。如果意向鎖是行鎖,那麼我們就得遍歷表中所有數據行來判斷。如果意向鎖是表鎖,則我們直接判斷一次就知道表中是否有數據行被鎖定了。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章