轉載:http://book.51cto.com/art/200803/68127.htm
20.3.4 InnoDB行鎖實現方式
InnoDB行鎖是通過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不同,后者是通過在數據塊中對相應數據行加鎖來實現的。InnoDB這種行鎖實現特點意味着:只有通過索引條件檢索數據,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖!
在實際應用中,要特別注意InnoDB行鎖的這一特性,不然的話,可能導致大量的鎖沖突,從而影響並發性能。下面通過一些實際例子來加以說明。
(1)在不通過索引條件查詢的時候,InnoDB確實使用的是表鎖,而不是行鎖。
在如表20-9所示的例子中,開始tab_no_index表沒有索引:
mysql> create table tab_no_index(id int,name varchar(10)) engine=innodb; Query OK, 0 rows affected (0.15 sec) mysql> insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4'); Query OK, 4 rows affected (0.00 sec) Records: 4 Duplicates: 0 Warnings: 0 |
表20-9 InnoDB存儲引擎的表在不使用索引時使用表鎖例子
session_1 |
session_2 |
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select * from tab_no_index where id = 1 ; +------+------+ | id | name | +------+------+ | 1 | 1 | +------+------+ 1 row in set (0.00 sec) |
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select * from tab_no_index where id = 2 ; +------+------+ | id | name | +------+------+ | 2 | 2 | +------+------+ 1 row in set (0.00 sec) |
mysql> select * from tab_no_index where id = 1 for update; +------+------+ | id | name | +------+------+ | 1 | 1 | +------+------+ 1 row in set (0.00 sec) |
|
|
mysql> select * from tab_no_index where id = 2 for update; 等待 |
在如表20-9所示的例子中,看起來session_1只給一行加了排他鎖,但session_2在請求其他行的排他鎖時,卻出現了鎖等待!原因就是在沒有索引的情況下,InnoDB只能使用表鎖。當我們給其增加一個索引后,InnoDB就只鎖定了符合條件的行,如表20-10所示。
創建tab_with_index表,id字段有普通索引:
mysql> create table tab_with_index(id int,name varchar(10)) engine=innodb; Query OK, 0 rows affected (0.15 sec) mysql> alter table tab_with_index add index id(id); Query OK, 4 rows affected (0.24 sec) Records: 4 Duplicates: 0 Warnings: 0 |
表20-10 InnoDB存儲引擎的表在使用索引時使用行鎖例子
session_1 |
session_2 |
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select * from tab_with_index where id = 1 ; +------+------+ | id | name | +------+------+ | 1 | 1 | +------+------+ 1 row in set (0.00 sec) |
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select * from tab_with_index where id = 2 ; +------+------+ | id | name | +------+------+ | 2 | 2 | +------+------+ 1 row in set (0.00 sec) |
mysql> select * from tab_with_index where id = 1 for update; +------+------+ | id | name | +------+------+ | 1 | 1 | +------+------+ 1 row in set (0.00 sec) |
|
|
mysql> select * from tab_with_index where id = 2 for update; +------+------+ | id | name | +------+------+ | 2 | 2 | +------+------+ 1 row in set (0.00 sec) |
(2)由於MySQL的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不同行的記錄,但是如果是使用相同的索引鍵,是會出現鎖沖突的。應用設計的時候要注意這一點。
在如表20-11所示的例子中,表tab_with_index的id字段有索引,name字段沒有索引:
mysql> alter table tab_with_index drop index name; Query OK, 4 rows affected (0.22 sec) Records: 4 Duplicates: 0 Warnings: 0 mysql> insert into tab_with_index values(1,'4'); Query OK, 1 row affected (0.00 sec) mysql> select * from tab_with_index where id = 1; +------+------+ | id | name | +------+------+ | 1 | 1 | | 1 | 4 | +------+------+ 2 rows in set (0.00 sec) |
表20-11 InnoDB存儲引擎使用相同索引鍵的阻塞例子
session_1 |
session_2 |
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) |
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) |
mysql> select * from tab_with_index where id = 1 and name = '1' for update; +------+------+ | id | name | +------+------+ | 1 | 1 | +------+------+ 1 row in set (0.00 sec) |
|
|
雖然session_2訪問的是和session_1不同的記錄,但是因為使用了相同的索引,所以需要等待鎖: mysql> select * from tab_with_index where id = 1 and name = '4' for update; 等待 |
(3)當表有多個索引的時候,不同的事務可以使用不同的索引鎖定不同的行,另外,不論是使用主鍵索引、唯一索引或普通索引,InnoDB都會使用行鎖來對數據加鎖。
在如表20-12所示的例子中,表tab_with_index的id字段有主鍵索引,name字段有普通索引:
mysql> alter table tab_with_index add index name(name); Query OK, 5 rows affected (0.23 sec) Records: 5 Duplicates: 0 Warnings: 0 |
表20-12 InnoDB存儲引擎的表使用不同索引的阻塞例子
· session_1 |
· session_2 |
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) |
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) |
mysql> select * from tab_with_index where id = 1 for update; +------+------+ | id | name | +------+------+ | 1 | 1 | | 1 | 4 | +------+------+ 2 rows in set (0.00 sec) |
|
|
Session_2使用name的索引訪問記錄,因為記錄沒有被索引,所以可以獲得鎖: mysql> select * from tab_with_index where name = '2' for update; +------+------+ | id | name | +------+------+ | 2 | 2 | +------+------+ 1 row in set (0.00 sec) |
|
由於訪問的記錄已經被session_1鎖定,所以等待獲得鎖。: mysql> select * from tab_with_index where name = '4' for update; |
(4)即便在條件中使用了索引字段,但是否使用索引來檢索數據是由MySQL通過判斷不同執行計划的代價來決定的,
如果MySQL認為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下InnoDB將使用表鎖,而不是行鎖。因此,在分析鎖沖突時,別忘了檢查SQL的執行計划,以確認是否真正使用了索引。關於MySQL在什么情況下不使用索引的詳細討論,參見本章“索引問題”一節的介紹。
在下面的例子中,檢索值的數據類型與索引字段不同,雖然MySQL能夠進行數據類型轉換,但卻不會使用索引,從而導致InnoDB使用表鎖。通過用explain檢查兩條SQL的執行計划,我們可以清楚地看到了這一點。
例子中tab_with_index表的name字段有索引,但是name字段是varchar類型的,如果where條件中不是和varchar類型進行比較,則會對name進行類型轉換,而執行的全表掃描。
mysql> alter table tab_no_index add index name(name); Query OK, 4 rows affected (8.06 sec) Records: 4 Duplicates: 0 Warnings: 0
mysql> explain select * from tab_with_index where name = 1 \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: tab_with_index type: ALL possible_keys: name key: NULL key_len: NULL ref: NULL rows: 4 Extra: Using where 1 row in set (0.00 sec) mysql> explain select * from tab_with_index where name = '1' \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: tab_with_index type: ref possible_keys: name key: name key_len: 23 ref: const rows: 1 Extra: Using where 1 row in set (0.00 sec)
(5)數據庫事務隔離級別
隔離級別 | 臟讀(Dirty Read) | 不可重復讀(NonRepeatable Read) | 幻讀(Phantom Read) |
---|---|---|---|
未提交讀(Read uncommitted) | 可能 | 可能 | 可能 |
已提交讀(Read committed) | 不可能 | 可能 | 可能 |
可重復讀(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable) | 不可能 | 不可能 | 不可能 |
- 未提交讀(Read Uncommitted):允許臟讀,也就是可能讀取到其他會話中未提交事務修改的數據
-
提交讀(Read Committed):只能讀取到已經提交的數據。Oracle等多數數據庫默認都是該級別 (不重復讀)
-
可重復讀(Repeated Read):可重復讀。在同一個事務內的查詢都是事務開始時刻一致的,InnoDB(mysql)默認級別。在SQL標准中,該隔離級別消除了不可重復讀,但是還存在幻象讀
-
串行讀(Serializable):完全串行化的讀,每次讀都需要獲得表級共享鎖,讀寫相互都會阻塞
spring隔離級別
隔離級別是指若干個並發的事務之間的隔離程度,與我們開發時候主要相關的場景包括:臟讀取、重復讀、幻讀。
我們可以看 org.springframework.transaction.annotation.Isolation
枚舉類中定義了五個表示隔離級別的值:
public enum Isolation { DEFAULT(-1), READ_UNCOMMITTED(1), READ_COMMITTED(2), REPEATABLE_READ(4), SERIALIZABLE(8); }
DEFAULT
:這是默認值,表示使用底層數據庫的默認隔離級別。對大部分數據庫而言,通常這值就是:READ_COMMITTED
。READ_UNCOMMITTED
:該隔離級別表示一個事務可以讀取另一個事務修改但還沒有提交的數據。該級別不能防止臟讀和不可重復讀,因此很少使用該隔離級別。READ_COMMITTED
:該隔離級別表示一個事務只能讀取另一個事務已經提交的數據。該級別可以防止臟讀,這也是大多數情況下的推薦值。REPEATABLE_READ
:該隔離級別表示一個事務在整個過程中可以多次重復執行某個查詢,並且每次返回的記錄都相同。即使在多次查詢之間有新增的數據滿足該查詢,這些新增的記錄也會被忽略。該級別可以防止臟讀和不可重復讀。SERIALIZABLE
:所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止臟讀、不可重復讀以及幻讀。但是這將嚴重影響程序的性能。通常情況下也不會用到該級別。
指定方法:通過使用 isolation
屬性設置,例如:
@Transactional(isolation = Isolation.DEFAULT)
spring傳播行為
所謂事務的傳播行為是指,如果在開始當前事務之前,一個事務上下文已經存在,此時有若干選項可以指定一個事務性方法的執行行為。
我們可以看 org.springframework.transaction.annotation.Propagation
枚舉類中定義了6個表示傳播行為的枚舉值:
public enum Propagation { REQUIRED(0), SUPPORTS(1), MANDATORY(2), REQUIRES_NEW(3), NOT_SUPPORTED(4), NEVER(5), NESTED(6); }
REQUIRED
:如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務(默認)。SUPPORTS
:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。MANDATORY
:如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。REQUIRES_NEW
:創建一個新的事務,如果當前存在事務,則把當前事務掛起。NOT_SUPPORTED
:以非事務方式運行,如果當前存在事務,則把當前事務掛起。NEVER
:以非事務方式運行,如果當前存在事務,則拋出異常。NESTED
:如果當前存在事務,則創建一個事務作為當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價於REQUIRED
。
指定方法:通過使用 propagation
屬性設置,例如:
@Transactional(propagation = Propagation.REQUIRED)
設置數據庫事務鎖時間
# 查詢全局等待事務鎖超時時間 SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout'; # 設置全局等待事務鎖超時時間 SET GLOBAL innodb_lock_wait_timeout=100; # 查詢當前會話等待事務鎖超時時間 SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'; 1205 - Lock wait timeout exceeded; try restarting transaction
select * from table where ? lock in share mode; select * from table where ? for update; insert into table values (…); update table set ? where ?; delete from table where ?;
所有以上的語句,都屬於當前讀,讀取記錄的最新版本。並且,讀取之后,還需要保證其他並發事務不能修改當前記錄,對讀取記錄加鎖。
其中,除了第一條語句,對讀取記錄加S鎖 (共享鎖)外,其他的操作,都加的是X鎖 (排它鎖)。