數據庫事務具有四個特徵,分別是原子性(Atomicity)、一致性(Consistency)、隔離性(Isoation)、持久性(Durability),簡稱爲事務的ACID特性。
事務的隔離性是指在併發環境中,併發的事務是相互隔離的。SQL標準中定義了四種數據庫事務隔離級別,級別從低到高分別爲:讀未提交(Read Uncommitted)、讀已提交(Read Committed)、可重複讀(Repeatable Read)、串行化(Serializable)。在事務的併發操作中會出現髒讀、不可重複讀、幻讀。在事務的併發操作中第二類更新丟失可以通過樂觀鎖和悲觀鎖解決。
--------------------------------------------------------------------------------
讀未提交(Read Uncommitted)
- 該隔離級別,所有事務都可以看到其他未提交事務的執行結果。通俗地講就是,在一個事務中可以讀取到另一個事務中新增或修改但未提交的數據。
- 該隔離級別可能導致的問題是髒讀。因爲另一個事務可能回滾,所以在第一個事務中讀取到的數據很可能是無效的髒數據,造成髒讀現象。
> set tx_isolation='READ-UNCOMMITTED';
事務A | 事務B | 備註 |
---|---|---|
> begin; > select name,blance from account where name='jack'; jack 1000 |
事務A開啓,查詢jack餘額1000 |
|
> begin; > select name,blance from account where name='jack'; jack 1000 > update account set blance=1200 where name='jack'; > select name,blance from account where name='jack'; jack 1200 |
事務B開啓,修改餘額爲1200, 但是沒有提交事務 |
|
> select name,blance from account where name='jack'; jack 1200 |
事務A中查詢jack餘額爲1200,讀 取到了事務B未提交的修改,即髒 讀,且和之前讀到的結果不同, 即不可重復讀 |
|
> rollback; > select name,blance from account where name='jack'; jack 1000 |
事務B回滾 | |
> select name,blance from account where name='jack'; jack 1000 > commit; |
事務A又讀取到了jack之前的餘額。 |
讀已提交(Read Committed)
- 這是大多數數據庫系統的默認隔離級別(但不是mysql默認的)
- 一個事務只能看見已經提交事務所做的修改。換句話說,一個事務從開始直到提交之前,所做的任何修改對其他事務都是不可見的。
- 該隔離級別可能導致的問題是不可重複讀。因爲兩次執行同樣的查詢,可能會得到不一樣的結果。
> set tx_isolation='READ-COMMITTED';
事務A | 事務B | 備註 |
---|---|---|
> begin; > select name,blance from account where name='jack'; jack 1000 |
事務A開啓,查詢jack餘額1000 | |
> begin; > select name,blance from account where name='jack'; jack 1000 > update account set blance=1200 where name='jack'; > select name,blance from account where name='jack'; jack 1200 |
事務B開啓,修改餘額爲1200, 但是沒有提交事務 |
|
> select name,blance from account where name='jack'; jack 1000 |
事務A中查詢jack餘額仍爲1000, 沒有讀取到事務B未提交的修改 |
|
> commit; > select name,blance from account where name='jack'; jack 1200 |
事務B提交 | |
> select name,blance from account where name='jack'; jack 1200 > commit; |
事務A讀取到了事務B提交的修改, 和之前讀到的結果不同,即不可重復讀 |
可重複讀(Repeatable Read)
- 這是MySQL的默認事務隔離級別
- 它確保同一事務的多個實例在併發讀取數據時,會看到同樣的數據行。通俗來講,可重複讀在一個事務裏讀取數據,怎麼讀都不會變,除非提交了該事務,再次進行讀取。
- 該隔離級別存在的問題是幻讀
set tx_isolation='REPEATABLE-READ';
事務A | 事務B | 備註 |
---|---|---|
> begin; > select name,blance from account where name='jack'; jack 1000 |
事務A開啓,查詢jack餘額1000 | |
> begin; > select name,blance from account where name='jack'; jack 1000 > update account set blance=1200 where name='jack'; > select name,blance from account where name='jack'; jack 1200 |
事務B開啓,修改餘額爲1200, 但是沒有提交事務 |
|
> select name,blance from account where name='jack'; jack 1000 |
事務A中查詢jack餘額仍爲1000, 沒有讀取到了事務B未提交的修改 |
|
> commit; > select name,blance from account where name='jack'; jack 1200 |
事務B提交 | |
> select name,blance from account where name='jack'; jack 1000 > commit; > select name,blance from account where name='jack'; jack 1200 |
事務A中查詢jack餘額仍爲1000, 沒有讀取到事務B已提交的修改 |
下面看下如何出現幻讀的
事務A | 事務B | 備註 |
---|---|---|
> begin; > select name,blance from account; jack 1000 |
事務A開啓,查詢到jack賬戶 | |
> begin; > select name,blance from account; jack 1000 > insert into account (name,blance) values ('tom',800); > select name,blance from account; jack 1000 tom 800 commit; |
事務B開啓,新增tom賬戶 | |
> select name,blance from account; jack 1000 > insert into account (name,blance) values ('tom',800); ERROR 1062 (23000): Duplicate entry 'tom' for key 'uk_name' rollback; |
事務A再次查詢時,只看到jack 賬戶,新增tom賬戶時報唯一鍵衝 突。明明沒有查詢到tom賬 戶,但是有新增衝突,這就是 幻讀。如果事務B新增或刪 除數據,事務A進行更新時, 不是預期的影響行數,也是幻讀 |
串行化(Serializable)
- 這是最高的隔離級別
- 它通過強制事務排序,使之不可能相互衝突,從而解決幻讀問題。通俗地講就是,假如兩個事務都操作到同一數據行,那麼這個數據行就會被鎖定,只允許先讀取/操作到數據行的事務優先操作,只有當事務提交了,數據行纔會解鎖,後一個事務才能成功操作這個數據行,否則只能一直等待
- 該隔離級別可能導致大量的超時現象和鎖競爭。
> set tx_isolation='SERIALIZABLE';
事務A | 事務B | 事務C | 備註 |
---|---|---|---|
> begin; > select * from account where ctime='2018-06-12 10:28:13'; 1 jack 1000 2018-06-12 10:28:13 2018-06-12 16:48:58 |
事務A開啓,並且查詢了jack賬戶,並拿到了該行的共享鎖 | ||
> begin; > select * from account where ctime='2018-06-12 10:28:13'; 1 jack 1000 2018-06-12 10:28:13 2018-06-12 16:48:58 > update account set blance=1200 where utime='2018-06-12 16:48:58'; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
事務B開啓,能夠正常查詢jack賬戶(可以拿到該行的共享鎖),但是不能修改(不可以拿到該行的排它鎖) | ||
> commit; | 事務A提交,釋放了jack賬戶的共享鎖 | ||
> update account set blance=1200 where utime='2018-06-12 16:48:58'; > select * from account where ctime='2018-06-12 10:28:13'; 1 jack 1200 2018-06-12 10:28:13 2018-06-12 16:48:58 |
事務B可以修改賬戶jack了,即拿到了該行的排它鎖 | ||
> begin; > select * from account where ctime='2018-06-12 10:28:13'; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
事務C開啓,不能查詢jack賬戶,因爲事務B拿了改行的排它鎖,導致事務C拿不到該行的共享鎖 | ||
> commit; | 事務B提交,釋放了jack賬戶的排它鎖 | ||
> select * from account where ctime='2018-06-12 10:28:13'; 1 jack 1200 2018-06-12 10:28:13 2018-06-12 17:06:02 commit; |
事務C可以查詢jack賬戶了,即拿到了該行的共享鎖 |
注:
- 這裏使用ctime、utime字段進行查詢和修改,是爲了展示使用非索引字段也有事務隔離效果
- 在該隔離級別中,select語句會自動獲取共享鎖,update/insert/delete會自動獲取排它鎖
- 一個事務中的一行記錄的共享鎖被拿走,其他事務只能獲取改行的共享鎖,不能獲取排它鎖;一個事務中的一行記錄的排它鎖被拿走,其他事務不能獲取該行的共享鎖和排它鎖
--------------------------------------------------------------------------------
從上面四種數據庫事務隔離級別介紹可以對應出解決的問題,如圖:
--------------------------------------------------------------------------------
第一類更新丟失
SQL標準中對此沒有定義,不會出現該錯誤。
事務A | 事務B |
---|---|
開啓事務 | |
查詢jack餘額爲1000 | |
開啓事務 | |
查詢jack餘額爲1000 | |
更新jack餘額爲1200 | |
提交事務 | |
更新jack餘額爲800 | |
回滾事務 | |
查詢jack餘額爲1000 (事務B的更新丟失) |
第二類更新丟失
事務A | 事務B |
---|---|
開啓事務 | |
查詢jack餘額爲1000 | |
開啓事務 | |
查詢jack餘額爲1000 | |
更新jack餘額爲1200 | |
提交事務 | |
更新jack餘額爲800 | |
事務提交 | |
查詢jack餘額爲800 (事務B的更新丟失) |
第二類更新丟失解決辦法
樂觀鎖
在更新語句中增加過濾條件,進行版本的判斷,可以是這條記錄的版本號、更新時間等。然後通過影響行數來判斷是否更新成功。
> begin;
> select name,blance,utime from account where name='jack';
jack 1000 2018-06-12 18:20:06
> update account set blance=1200 where name='jack' and utime='2018-06-12 18:20:06';
> commit;
悲觀鎖
悲觀鎖分爲共享鎖和排它鎖。
共享鎖又稱爲讀鎖,簡稱S鎖,顧名思義,共享鎖就是多個事務對於同一數據可以共享一把鎖,共享鎖是用來讀取數據的。另外,一個事務獲取了同一數據的共享鎖,其他事務就不能獲取該數據的排它鎖。
排它鎖又稱爲寫鎖,簡稱X鎖,顧名思義,排它鎖就是不能與其他所並存,如一個事務獲取了一個數據行的排它鎖,其他事務就不能再獲取該行的其它鎖,包括共享鎖和排它鎖。另外不存什麼事務隔離級別,update/insert/delete會自動獲取排它鎖
共享鎖獲取方式:select * from account where name='jack' lock in share mode;
排它鎖獲取方式:select * from account where name='jack' for update;
MySQL分爲表級鎖和行級鎖,共享鎖和排它鎖是行級鎖。表級鎖在此不做討論。
最佳實踐
通常,對於絕大多數的應用程序來說,可以優先考慮將數據庫系統的隔離級別設置爲讀已提交(Read Committed),這能夠在避免髒讀的同時保證較好的併發性能。儘管這種事務隔離級別會導致不可重複讀、幻讀和第二類丟失更新等併發問題,但較爲科學的做法是在可能出現這類問題的個別場合中,由應用程序主動採用悲觀鎖或樂觀鎖來進行事務控制。
另本文中示例的表結構如下:
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(16) NOT NULL DEFAULT '' COMMENT '用戶名',
`blance` int(11) NOT NULL DEFAULT '0' COMMENT '餘額',
`ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '寫入時間',
`utime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用戶餘額表';