MVCC機制
死鎖
事務失效的常見原因
https://blog.csdn.net/iteye_12828/article/details/81934492
1.關系型數據庫遵循ACID原則:
事務在英文中是transaction,和現實世界中的交易很類似,它有如下四個特性:
#1、A (Atomicity) 原子性
原子性很容易理解,也就是說事務里的所有操作要么全部做完,要么都不做,
事務成功的條件是事務里的所有操作都成功,只要有一個操作失敗,整個事務就失敗,需要回滾。
#比如銀行轉賬,從A賬戶轉100元至B賬戶,分為兩個步驟:
1)從A賬戶取100元;
2)存入100元至B賬戶。
這兩步要么一起完成,要么一起不完成,如果只完成第一步,第二步失敗,錢會莫名其妙少了100元。
#2、C (Consistency) 一致性
一致性也比較容易理解,也就是說數據庫要一直處于一致的狀態,
事務的運行不會改變數據庫原本的一致性約束。
#例如現有完整性約束a+b=10,
如果一個事務改變了a,那么必須得改變b,使得事務結束后依然滿足a+b=10,否則事務失敗。
#3、I (Isolation) 獨立性
所謂的獨立性是指并發的事務之間不會互相影響,
如果一個事務要訪問的數據正在被另外一個事務修改,
只要另外一個事務未提交,它所訪問的數據就不受未提交事務的影響。
#比如現在有個交易是從A賬戶轉100元至B賬戶,
在這個交易還未完成的情況下,如果此時B查詢自己的賬戶,是看不到新增加的100元的。
#4、D (Durability) 持久性
持久性是指一旦事務提交后,
它所做的修改將會永久的保存在數據庫上,即使出現宕機也不會丟失。
1.1 并發事務處理帶來的問題
相對于串行處理來說,并發事務處理能大大增加數據庫資源的利用率,
提高數據庫系統的事務吞吐量,從而可以支持更多的用戶。
但并發事務處理也會帶來一些問題,主要包括以下幾種情況:
更新丟失(Lost Update)
當兩個或多個事務選擇同一行,然后基于最初選定的值更新該行時,由于每個事務都不知道其他事務的存在,
就會發生丟失更新問題, 最后的更新覆蓋了由其他事務所做的更新。
例如,兩個編輯人員制作了同一文檔的電子副本。
每個編輯人員獨立地更改其副本,然后保存更改后的副本,這樣就覆蓋了原始文檔。
最后保存其更改副本的編輯人員覆蓋另一個編輯人員所做的更改。
如果在一個編輯人員完成并提交事務之前,另一個編輯人員不能訪問同一文件,則可避免此問題。
#第一類更新丟失:
張三的工資為5000,
事務A中獲取工資為5000,事務B獲取工資為5000,
事務A與B各匯入100,并提交數據庫,工資變為5200,
隨后
事務A發生異常,回滾了,恢復張三的工資為5000,這樣就導致事務B的更新丟失了。
#第二類更新丟失(不可重復讀的特例):
在事務A中,讀取到張三的存款為5000,操作沒有完成,事務還沒提交。
與此同時,
事務B,存儲1000,把張三的存款改為6000,并提交了事務。
隨后,
在事務A中,存儲500,把張三的存款改為5500,并提交了事務,
這樣事務A的更新覆蓋了事務B的更新。
臟讀(Dirty Reads)
一個事務正在對一條記錄做修改,在這個事務完成并提交前,這條記錄的數據就處于不一致狀態;
這時,另一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取了這些“臟”數據,
并據此做進一步的處理,就會產生未提交的數據依賴關系。
這種現象被形象地叫做"臟讀"。
張三的工資為5000,事務A中把他的工資改為8000,但事務A尚未提交。
與此同時,
事務B正在讀取張三的工資,讀取到張三的工資為8000。
隨后,
事務A發生異常,而回滾了事務。張三的工資又回滾為5000。
最后,
事務B讀取到的張三工資為8000的數據即為臟數據,事務B做了一次臟讀。
不可重復讀(Non-Repeatable Reads)
一個事務在讀取某些數據后的某個時間,再次讀取以前讀過的數據,
卻發現其讀出的數據已經發生了改變、或某些記錄已經被刪除了!
這種現象就叫做“不可重復讀”。
一段數據被連接1讀取后,被連接2更新或刪除了,連接1再次讀取發現不一致了。
在事務A中,讀取到張三的工資為5000,操作沒有完成,事務還沒提交。
與此同時,
事務B把張三的工資改為8000,并提交了事務。
隨后,
在事務A中,再次讀取張三的工資,此時工資變為8000。
在一個事務中前后兩次讀取的結果并不致,導致了不可重復讀。
幻讀(Phantom Reads)
一個事務按相同的查詢條件重新讀取以前檢索過的數據,
卻發現其他事務插入了滿足其查詢條件的新數據,這種現象就稱為“幻讀”。
一段數據被連接1查詢后,連接2向這個范圍又插入了新數據,連接1再次讀取發現不一致了。
目前工資為5000的員工有10人,事務A讀取所有工資為5000的人數為10人。
此時,
事務B插入一條工資也為5000的記錄。
這時候,事務A再次讀取工資為5000的員工,記錄為11人。此時產生了幻讀。
不可重復讀和幻讀兩者有些相似。
但不可重復讀重點在于update和delete,而幻讀的重點在于insert。
1.2 事務隔離級別-->并發事務的問題
更新丟失:
不能單靠數據庫事務控制器來解決,需要應用程序對要更新的數據加必要的鎖(樂觀鎖或悲觀鎖)來解決,
因此,防止更新丟失應該是應用的責任。
“臟讀”、“不可重復讀”和“幻讀”:
其實都是數據庫讀一致性問題,必須由數據庫提供一定的事務隔離機制來解決。
數據庫實現事務隔離的方式,基本上可分為以下兩種。
1)一種是在讀取數據前,對其加鎖,阻止其他事務對數據進行修改。
2)另一種是不用加任何鎖,通過一定機制生成一個數據請求時間點的一致性數據快照(Snapshot),
并用這個快照來提供一定級別(語句級或事務級)的一 致性讀取。
從用戶的角度來看,好像是數據庫可以提供同一數據的多個版本,
因此,這種技術叫做數據多版本并發控制(MultiVersion Concurrency Control,簡稱MVCC或MCC),
也經常稱為"多版本數據庫"。
數據庫的事務隔離越嚴格,并發副作用越小,但付出的代價也就越大,
因為事務隔離實質上就是使事務在一定程度上 “串行化”進行,這顯然與“并發”是矛盾的。
同時,不同的應用對讀一致性和事務隔離程度的要求也是不同的,
比如許多應用對“不可重復讀”和“幻讀”并不敏感,可能更關心數據并發訪問的能力。
為了解決“隔離”與“并發”的矛盾,ISO/ANSI SQL92定義了4個事務隔離級別
隔離級別 | 讀數據一致性 | 臟讀 | 不可重復讀 | 幻讀 |
---|---|---|---|---|
未提交讀(Read uncommitted) | 最低級別,只能保證不讀取物理上損壞的數據 | 是 | 是 | 是 |
已提交度(Read committed) | 語句級 | 否 | 是 | 是 |
可重復讀(Repeatable read) | 事務級 | 否 | 否 | 是 |
可序列化(Serializable) | 最高級別,事務級 | 否 | 否 | 否 |
"Oracle"
只提供Read committed和Serializable兩個標準隔離級別,
另外還提供自己定義的Read only隔離級別;
"oracle默認的事務處理級別:
是read_committed"
"SQL Server"
除支持上述4個隔離級別外,還支持一個叫做“快照”的隔離級別,
但嚴格來說它是一個用MVCC實現的Serializable隔離級別;
"MySQL"
支持全部4個隔離級別,但在具體實現時,
有一些特點,比如在一些隔離級別下是采用MVCC一致性讀,但某些情況下又不是.
"mysql默認的事務處理級別:
是'REPEATABLE-READ',也就是可重復讀,
但仍可能導致更新丟失, 該問題可由應用程序解決
"
1.2.1 前置背景
假設有如下表結構的數據.
id | first_name | last_name | gender | age | address | phone |
---|---|---|---|---|---|---|
1 | 張 | 三 | 男 | 20 | nj | 12345678901 |
2 | 李 | 四 | 男 | 30 | bj | 12345678902 |
3 | 王 | 五 | 男 | 40 | tj | 12345678903 |
1.2.2 事務的可見性
>> 事務一定能看到自己的修改
>> 事務可能看得到已提交的數據
>> 事務可能看得到未提交的數據
連接1 | 連接2 |
---|---|
INSERT INTO stu VALUES('趙', '六', '女', 50, 'gz', '12345678904'); | |
START TRANSACTION; | |
START TRANSACTION; | SELECT * FROM stu WHERE phone='12345678904'; (能看得到剛才插入的'趙六') |
INSERT INTO stu VALUES('于', '七', '女', 60, 'sc', '12345678905'); | DELETE FROM stu WHERE first_name='趙' AND last_name='六'; |
SELECT * FROM stu WHERE phone>='12345678904'; ('趙六'已經被刪掉了, 同時'于七'也讀不到) |
|
COMMIT; | COMMIT; |
1.2.3 事務的可見性-Serilizable
事務需要對讀到的數據進行加鎖
(這種級別影響性能, 慎用)
連接1 | 連接2 |
---|---|
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; START TRANSACTION; |
|
START TRANSACTION; | SELECT * FROM stu; (讀到三條記錄) |
INSERT INTO stu VALUES('趙', '六', '女', 50, 'gz', '12345678904'); (等待, 語句無法執行) |
|
SELECT * FROM stu; (還是讀到三條記錄, 因為'趙六'還沒插入) |
|
COMMIT; | |
連接2COMMIT之后, INSERT語句執行成功; COMMIT; |
|
SELECT * FROM stu; (讀到四條記錄, 包含'趙六') |
1.2.4 事務的可見性-Repetable Read
事務看到的始終是本事務第一次讀時候能看到的內容
1.2.4.1 事務的可見性-Repetable Read(1)
連接1 | 連接2 |
---|---|
START TRANSACTION; | |
START TRANSACTION; | SELECT * FROM stu; (讀到三條記錄) |
INSERT INTO stu VALUES('趙', '六', '女', 50, 'gz', '12345678904'); |
|
COMMIT; | SELECT * FROM stu; (還是讀到三條記錄, 因為連接2還沒commit) |
SELECT * FROM stu; (還是讀到三條記錄, 因為連接2還沒commit) |
|
COMMIT; | |
SELECT * FROM stu; (讀到四條記錄, 包含'趙六') |
1.2.4.2 事務的可見性-Repetable Read(2)
連接1 | 連接2 |
---|---|
START TRANSACTION; | |
START TRANSACTION; | SELECT * FROM stu; (讀到三條記錄) |
DELETE FROM stu WHERE phone='12345678902' | |
UPDATE stu SET phone='86-12345678901' WHERE phone='12345678901' | SELECT * FROM stu; (還是讀到三條記錄, 因為連接2還沒commit) |
COMMIT; | SELECT * FROM stu; (還是讀到三條記錄, 因為連接2還沒commit) |
SELECT * FROM stu; (還是讀到三條記錄, 因為連接2還沒commit) |
|
COMMIT; | |
SELECT * FROM stu; (讀到兩條記錄, 其中一條被刪除) |
1.2.5 事務的可見性-Read Committed
事務看到的始終是每個讀開始時刻已提交的數據
1.2.5.1 事務的可見性-Read Committed (1)
連接1 | 連接2 |
---|---|
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION; |
|
START TRANSACTION; | SELECT * FROM stu; |
INSERT INTO stu VALUES('趙', '六', '女', 50, 'gz', '12345678904'); (等待, 語句無法執行) |
|
COMMIT; | |
SELECT * FROM stu; (讀到四條記錄, 包含連接1插入的數據) |
|
COMMIT; |
1.2.5.2 事務的可見性-Read Committed (2)
連接1 | 連接2 |
---|---|
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION; |
|
START TRANSACTION; | SELECT * FROM stu; (讀到三條記錄) |
DELETE FROM stu WHERE phone='12345678902' | |
UPDATE stu SET phone='86-12345678901' WHERE phone='12345678901' | |
SELECT * FROM stu; (還是讀到三條記錄, 因為連接1還沒commit) |
|
COMMIT; | |
SELECT * FROM stu; (讀到2條記錄, 不包含連接1刪除的數據) |
|
COMMIT; |
1.2.5.3 事務的可見性-Read Committed (3)
連接1 | 連接2 |
---|---|
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION; |
|
START TRANSACTION; | SELECT * FROM stu; (讀到三條記錄) |
DELETE FROM stu WHERE phone='12345678902' | |
UPDATE stu SET phone='86-12345678901' WHERE phone='12345678901' | |
SELECT * FROM stu; (還是讀到三條記錄, 因為連接1還沒commit) |
|
ROLLBACK; | |
SELECT * FROM stu; (還是讀到三條記錄, 因為連接1rollback了) |
|
COMMIT; |
1.2.6 事務的可見性-Read Uncommitted
事務看得到當前最新的未提交數據
連接1 | 連接2 |
---|---|
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; |
|
START TRANSACTION; | SELECT * FROM stu; (讀到三條記錄) |
UPDATE stu SET phone='86-12345678901' WHERE phone='12345678901' | |
SELECT * FROM stu; (讀到連接1的1條更新) |
|
UPDATE stu SET phone='86-12345678902' WHERE phone='12345678902' | |
SELECT * FROM stu; (讀到連接1的2條更新) |
|
COMMIT; | COMMIT; |
1.3 事務的優化思路
1.3.1 避免大事務和長事務
#1.避免大事務
>> 主要指事務包含的語句很多,或者語句執行耗時很長
>> 將大事務轉換為若干小事務提交,提高可靠性
>> 優化大事務邏輯 , 如刪除全表數據改為 TRUNCATE TABLE
>> 注意DDL執行的耗時,以及它對資源、復制等其它問題的影響
#2.避免長事務
>> 主要指事務目前不繁忙,但是一直沒有提交
>> 長事務占用連接資源
>> 長事務可能占用系統資源,如磁盤空間等
>> 長事務可能導致過期數據一直無法回收
1.3.2 優化小事務
>> 頻繁的單語句DML事務不利于性能
>> 考慮將可以合并的DML放在一個事務提交
>> 多條語句合并為一條語句(比如批量更新或批量插入)
1.3.3 事務隔離級別的選擇
確認隔離級別對并發DML的影響.
>> 最常用的隔離級別是REPETABLE READ和READ COMMITED;
>> 另外兩種隔離級別慎用.
1.3.4 其他優化
#1.確定是不是能用只讀事務
>> START TRANSACTION READ ONLY;
>> 可以提高性能
#2.索引對加鎖的影響
>> 如果表上沒有索引, 一旦涉及到范圍加鎖, 可能整張表都被鎖住
>> 如果表上有唯一索引, 唯一索引的加鎖粒度更小
>> 如果使用二級索引掃描進行更新, 二級索引和聚簇索引記錄都要加鎖
1.4 事務的生命周期
#1.事務一般有三種開啟方式
>> BEGIN/START TRANSACTION;
>> AUTOCOMMIT=0;
>> AUTOCOMMIT=1; 一條語句即一個事務
#2.事務的結束一般有四種方式
>> COMMIT: 所有的修改都會生效
>> ROLLBACK: 所有的修改都會回滾
>> 當前連接斷開, 事務會回滾
>> 執行某些特定的DDL語句, 原有事務會被隱式提交, 之后才執行DDL
2.mysql中的鎖問題
鎖包括latch和lock. 本文指代的是lock.
lock的對象是事務, 用來鎖定數據庫中的對象(表, 頁, 行), 一般lock住的對象僅在事務commit/rollback/連接斷開后釋放, 且具有死鎖檢測機制.
2.1 InnoDB引擎支持行鎖及表鎖
數據庫系統使用鎖機制來支持事務的并發控制和隔離性.
在mysql 的 InnoDB引擎支持行鎖,分布式存儲引擎NDBCluster也是
與Oracle不同,mysql的行鎖是通過索引加載的,即是行鎖是加在索引相應的行上的,
要是對應的SQL語句沒有走索引,則會全表掃描,行鎖則無法實現,取而代之的是表鎖。
2.1.1 表鎖/意向鎖
不會出現死鎖,發生鎖沖突幾率高,并發低。
顯式的表鎖
#有兩種類型
>> READ 持有者只能讀加鎖的表, 不同會話可以共同持有表鎖
>> WRITE 只有持有者可以讀寫加鎖的表, 其他會話都不能訪問加鎖的表
#語法
LOCK TABLES t1 READ [, t2 READ [, t3 WRITE]] ...;
UNLOCK TABLES;
#特點
>> 所有當前會話要訪問表需要在同一個LOCK TABLES語句里面加鎖
>> 加鎖語句會隱式的提交當前未完成的事務
>> 加鎖語句會隱式的釋放當前已持有的表鎖
#缺點
加鎖粒度太大, 不利于并發, 謹慎使用
隱式的表鎖
隱式的表鎖一般對用戶不可見, 用戶不可操作, 但能感知到, 主要用于數據庫內部并發同步保持正確性.
有以下兩種情況
連接1 | 連接2 |
---|---|
ALTER TABLE stu ADD COLUM hobby VARCHAR(128) DEFAULT 'None', ALGORITHM=COPY; | |
--executing... | SELECT * FROM stu; (讀到三條記錄, 不會看到hobby字段, 沒有阻擋) |
--executing... | INSERT INTO stu VALUES('趙', '六', '女', 50, 'gz', '12345678904'); (等待, 語句無法執行) |
--ALTER TABLE結束 | -- 緊隨ALTER TABLE結束之后, 上面的插入語句也返回執行失敗, 列的個數不匹配 |
連接1 | 連接2 |
---|---|
ALTER TABLE stu ADD COLUM hobby VARCHAR(128) DEFAULT 'None', ALGORITHM=INPLACE; 注意這里的參數是 'INPLACE' |
|
--executing... | SELECT * FROM stu; (讀到三條記錄, 不會看到hobby字段, 沒有阻擋) |
--executing... | INSERT INTO stu VALUES('趙', '六', '女', 50, 'gz', '12345678904'); (等待, 語句無法執行) |
--ALTER TABLE結束 | SELECT * FROM stu; (讀到四條記錄, hobby字段都為'None') |
2.1.2 行鎖
行鎖主要存在于InnoDB存儲引擎層.
會出現死鎖,發生鎖沖突幾率低,并發高。
#行鎖的主要類型:
>> 記錄鎖
>> 間隙鎖
>> 插入意向鎖
>> ...
#行鎖模式
>> 共享鎖
>> 互斥鎖
#加鎖語句
>> DML語句
>> SELECT 語句, 帶加鎖提示
>> ...
2.1.2.1 行鎖的類型
行鎖分 共享鎖 和 排它鎖。
而在鎖定機制的實現過程中為了讓行級鎖定和表級鎖定共存,
InnoDB也同樣使用了意向鎖(表級鎖定)的概念,也就有了"意向共享鎖"和"意向排他鎖"這兩種
"共享鎖/讀鎖"
當一個事務對某幾行上讀鎖時,允許其他事務對這幾行進行讀操作,
但不允許其進行寫操作,也不允許其他事務給這幾行上排它鎖,但允許上讀鎖。
"上共享鎖的寫法: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;"
2.1.2.2 行鎖的實現
注意幾點:
1.行鎖必須有索引才能實現,否則會自動鎖全表,那么就不是行鎖了。
"update或delete語句也是一樣的, 如果where后的條件沒有走索引, 也是會鎖全表的, 但也與事務的隔離級別有關"
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在事務中都會自動默認加上排它鎖
對于普通SELECT語句,InnoDB不會加任何鎖;
https://blog.csdn.net/weixin_39004901/article/details/105719828 (一個不走索引的更新語句,到底會不會鎖全表)
2.1.3 意向鎖/表鎖與行鎖共存
"當一個事務需要給自己需要的某個資源加鎖的時候"
"行鎖的作用是"
如果遇到一個共享鎖正鎖定著自己需要的資源的時候,自己可以再加一個共享鎖,不過不能加排他鎖。
如果遇到自己需要鎖定的資源已經被一個排他鎖占有之后,則只能等待該鎖定釋放資源之后自己才能獲取鎖定資源并添加自己的鎖定。
"而意向鎖的作用是"
當一個事務在需要獲取資源鎖定的時候,如果遇到自己需要的資源已經被排他鎖占用的時候,該事務可以需要鎖定行的表上面添加一個合適的意向鎖。
如果自己需要一個共享鎖,那么就在表上面添加一個意向共享鎖。
如果自己需要的是某行(或者某些行)上面添加一個排他鎖的話,則先在表上面添加一個意向排他鎖。
意向共享鎖可以同時并存多個,但是意向排他鎖同時只能有一個存在。
所以,可以說InnoDB的鎖定模式實際上可以分為四種:
共享鎖(S)
排他鎖(X)
意向共享鎖(IS)
意向排他鎖(IX)
"意向鎖是InnoDB自動加的,不需用戶干預。"
當前鎖模式/是否兼容/請求鎖模式 | X(排它鎖) | IX(意向排它鎖) | S(共享鎖) | IS(共享鎖) |
---|---|---|---|---|
X(排它鎖) | 沖突 | 沖突 | 沖突 | 沖突 |
IX(意向排它鎖) | 沖突 | 兼容 | 沖突 | 兼容 |
S(共享鎖) | 沖突 | 沖突 | 兼容 | 兼容 |
IS(共享鎖) | 沖突 | 兼容 | 兼容 | 兼容 |
2.1.4 MyISAM引擎支持表鎖
MyISAM:
在執行查詢語句(select)前, 會自動給涉及的所有表加讀鎖,
在執行增刪改操作前, 會自動給涉及的表加寫鎖。
MySQL的表級鎖有兩種模式:
表共享讀鎖
表獨占寫鎖
結論:讀鎖會阻塞寫,寫鎖會阻塞讀和寫
"對MyISAM表的讀操作"
不會阻塞其它進程對同一表的讀請求,但會阻塞對同一表的寫請求。
只有當讀鎖釋放后,才會執行其它進程的寫操作。
"對MyISAM表的寫操作"
會阻塞其它進程對同一表的讀和寫操作,只有當寫鎖釋放后,才會執行其它進程的讀寫操作。
MyISAM不適合做寫為主表的引擎,
因為寫鎖后,其它線程不能做任何操作,
大量的更新會使查詢很難得到鎖,從而造成永遠阻塞
2.1.5 一致性讀和加鎖讀
InnoDB實現了兩種不同的讀數據機制
#1.一致性不加鎖讀
>> 不加鎖, 基于多版本機制(MVCC)
>> 讀寫可并行
>> 讀取的是指定時間點的快照內容, 不一定是最新內容(read commited級別下讀取的是被鎖定行的最新快照數據; repeatable read級別下讀取的是事務開始時的行數據版本)
#2.加鎖讀
>> 讀取的是最新數據
>> 基于鎖管理機制, 按要求加鎖, 鎖沖突需要等待
>> 可能產生死鎖
>> SELECT ... LOCK IN SHARE MODE;
>> SELECT ... FOR UPDATE;
>> 在使用上述倆Select語句時, 必須是在一個事務中, 請務必加上BEGIN/START TRANSACTION/SET AUTOCOMMIT=0等
2.2 鎖沖突
例如說事務A將某幾行上鎖后,事務B又對其上鎖,鎖不能共存否則會出現鎖沖突。
(但是共享鎖可以共存,共享鎖和排它鎖不能共存,排它鎖和排他鎖也不可以)
2.2.1 鎖沖突1
INSERT 和 DELETE可能會沖突, 例如先INSERT再DELETE場景
連接1 | 連接2 |
---|---|
START TRANSACTION; | START TRANSACTION; |
INSERT INTO stu VALUES('趙', '六', '女', 50, 'gz', '12345678904'); |
|
SELECT * FROM stu; (讀到三條記錄, 因為還沒提交) |
|
DELETE FROM stu WHERE phone>'12345678902' --等待, 無法立即執行, 返回 |
|
COMMIT/ROLLBACK; | |
--1.如果連接1是COMMIT, 則phone='12345678903'和'12345678904'兩條記錄都會被刪掉 --2.如果連接1是ROLLBACK, 則只會刪掉phone='12345678903' --3.如果連接1一直不提交, 則報超時錯誤 |
|
COMMIT; |
2.2.2 鎖沖突2
INSERT 和 DELETE可能會沖突, 例如先DELETE再INSERT場景
連接1 | 連接2 |
---|---|
START TRANSACTION; | START TRANSACTION; |
DELETE FROM stu WHERE phone='12345678902' | |
SELECT * FROM stu; (讀到三條記錄, 因為還沒提交) |
|
INSERT INTO stu VALUES('趙', '六', '女', 50, 'gz', '12345678902'); --等待, 無法立即執行, 返回 |
|
COMMIT/ROLLBACK; | |
--1.如果連接1是COMMIT, 則插入成功 --2.如果連接1是ROLLBACK, INSERT會報唯一主鍵沖突錯誤 --3.如果連接1一直不提交, 則報超時錯誤 |
|
COMMIT; |
2.2.3 鎖沖突3
INSERT 和 INSERT 可能會沖突
連接1 | 連接2 |
---|---|
START TRANSACTION; | START TRANSACTION; |
INSERT INTO stu VALUES('趙', '六', '女', 50, 'gz', '12345678904'); | |
SELECT * FROM stu; (讀到三條記錄, 因為還沒提交) |
|
INSERT INTO stu VALUES('趙', '六', '女', 50, 'gz', '12345678904'); --等待, 無法立即執行, 返回 |
|
COMMIT/ROLLBACK; | |
--1.如果連接1是COMMIT, 則INSERT會報唯一主鍵沖突錯誤 --2.如果連接1是ROLLBACK, 則插入成功 --3.如果連接1一直不提交, 則報超時錯誤 |
|
COMMIT; |
2.2.4 鎖沖突4
INSERT 和 SELECT可能會沖突, 例如先讀后插入的場景
連接1 | 連接2 |
---|---|
START TRANSACTION; | |
SELECT * FROM stu LOCK IN SHARE MODE; | |
INSERT INTO stu VALUES('趙', '六', '女', 50, 'gz', '12345678904'); --等待, 無法立即執行, 返回 |
|
COMMIT; | |
--1.如果連接2很快COMMIT, 則INSERT成功 --2.如果連接1一直不提交, 則報超時錯誤 |
2.2.5 鎖沖突5
INSERT 和 SELECT可能會沖突, 例如先插入后讀的場景
連接1 | 連接2 |
---|---|
START TRANSACTION; | SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; START TRANSACTION; |
INSERT INTO stu VALUES('趙', '六', '女', 50, 'gz', '12345678904'); | |
SELECT * FROM stu; --鎖等待, 無法立即執行, 返回 |
|
COMMIT; | |
--1.如果連接1很快COMMIT, 則能查到4條數據 --2.如果連接1一直不提交, 則報超時錯誤 |
2.3 死鎖
例如說兩個事務,事務A鎖住了1~5行,同時事務B鎖住了6~10行,
此時事務A請求鎖住6~10行,就會阻塞直到事務B釋放6~10行的鎖,
而隨后事務B又請求鎖住1~5行,事務B也阻塞直到事務A釋放1~5行的鎖。
"死鎖發生時,會產生Deadlock錯誤。"
"鎖是對表操作的,所以自然鎖住全表的表鎖就不會出現死鎖。"
2.3.1 避免死鎖
死鎖導致事務回滾,降低系統效率,浪費系統資源,影響業務體驗
#1.最主要的原則是避免死鎖條件的滿足
>> 事務盡量小,比如只更新一條記錄,但不代表不會死鎖
>> 事務盡量短,縮短或者避免沖突時間窗
>> 事務更新多張表時,用同一個順序更新不同的表
>> 事務更新一張表內的多行時,用同一個順序更新不同的行
#2.另一個角度是減少事務的加鎖
>> 避免事務(長時間)鎖一個范圍
>> 如果一致性讀可以滿足要求,盡量少用加鎖讀
>> 需要加鎖讀的時候,盡童使用READ COMMITTED隔離級別有利于減少死鎖的產生
>> 使用索引掃描,減少事務加鎖的數量
#3.如何監測和處理死鎖
>> 應用程序做好重新啟動事務的準備,應對死鎖場景
>> SHOW ENGINE INNODB STATUS; / 錯誤日志
>> 根據死鎖信息,調整應用程序邏輯
2.3.2 死鎖檢測
#1.mysql官網對死鎖的說明
A deadlock is a situation where different transactions are unable to proceed because each holds a lock that the other needs. Because both
transactions are waiting for a resource to become available, neither ever release the locks it holds.
#2.死鎖發生條件
>> 多個事務并發
>> 每個事務持有部分資源(行鎖),需要申請更多的資源(行鎖)
>> 一旦申請存在相互依賴,資源等待構成環,即形成死鎖
#3.死鎖檢測
>> MySQL/InnoDB內部默認會進行死鎖檢測,避免事務長時間等待
>> 一旦檢測到死鎖,選擇一個事務進行回滾,其它事務可以繼續
#4.禁用死鎖檢測
>> 某些場景下,可以提高性能
>> 通過 innodb_ lock_ wait_ timeout 來控制死鎖超時時間
2.3.3 死鎖檢測
UPDATE語句導致的死鎖檢測和處理
連接1 | 連接2 |
---|---|
START TRANSACTION; | START TRANSACTION; |
UPDATE stu SET phone='86-12345678901' WHERE phone='12345678901' --更新成功 |
|
UPDATE stu SET phone='86-12345678903' WHERE phone='12345678903' --更新成功 |
|
SELECT * FROM stu WHERE phone='12345678903' FOR UPDATE; --等待, 因為該上被連接2更新了 |
|
SELECT * FROM stu WHERE phone='12345678901' FOR UPDATE; --檢測出死鎖, 當前事務被回滾 |
|
COMMIT; | COMMIT; --無效, 當前事務已被回滾 |
SELECT * FROM stu; 只能看到連接1更新的那條記錄和另外兩條未曾改變的記錄 |
https://blog.csdn.net/lzy_lizhiyang/article/details/52678446?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-14.not_use_machine_learn_pai&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-14.not_use_machine_learn_pai (死鎖1: 不恰當的update語句使用主鍵和索引導致mysql死鎖)
https://blog.csdn.net/qiumuxia0921/article/details/50574879?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1.not_use_machine_learn_pai&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1.not_use_machine_learn_pai (死鎖2:
兩個事物 update同一張表出現的死鎖問題)
https://www.cnblogs.com/s-b-b/p/8334593.html (非聚簇索引)
2.4 樂觀鎖與悲觀鎖 (可解決數據更新丟失問題)
樂觀鎖
使用數據版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。
何謂數據版本?
即為數據增加一個版本標識,一般是通過為數據庫表增加一個數字類型的 “version” 字段來實現。
當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。
當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,
如果數據庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期數據
1.設計tb_test表有id, money, version三個字段
2.更新時:
1) 先讀tb_test表的數據, 得到 id=id1, version=v1
select id, money, version from tb_test
2) 每次更新task表中的value字段時, 為了防止發生沖突, 需要這樣操作
update tb_test set money=newMoney,version=v1+1 where id=id1 and version=v1
成功, 則成功, 失敗則表明失敗.
悲觀鎖
使用命令設置MySQL為非autocommit模式:
set autocommit=0;
設置完autocommit后,我們就可以執行我們的正常業務了。
需要注意的是,在事務中,
只有SELECT ... FOR UPDATE 或LOCK IN SHARE MODE同一筆數據時會等待其它事務結束后才執行,
一般SELECT ... 則不受此影響。
拿上面的實例來說,
當執行select name from tb_test where id=1 for update;后,
在另外的事務中如果再次執行select status from t_goods where id=1 for update;
則第二個事務會一直等待第一個事務的提交,此時第二個查詢處于阻塞的狀態,
但是如果我是在第二個事務中執行select status from t_goods where id=1;
則能正常查詢出數據,不會受第一個事務的影響。
關于事務的處理的應用層/業務代碼控制, 可參看<<spring系列>>文章
2.5 MVCC
MVCC(Multiversion concurrency control )是一種多版本并發控制機制。
2.5.1 MVCC是為了解決什么問題
并發訪問(讀或寫)數據庫時,對正在事務內處理的數據做多版本的管理。以達到用來避免寫操作的堵塞,從而引發讀操作的并發問題。
鎖機制可以控制并發操作,但是其系統開銷較大,而MVCC可以在大多數情況下代替行級鎖,使用MVCC,能降低其系統開銷。
2.5.2 MVCC實現
MVCC是通過保存數據在某個時間點的快照來實現的。
不同存儲引擎的MVCC實現是不同的,典型的有樂觀并發控制和悲觀并發控制。
當我們創建表完成后,mysql會自動為每個表添加 數據版本號(最后更新數據的事務id)db_trx_id 刪除版本號 db_roll_pt (數據刪除的事務id) 事務id由mysql數據庫自動生成,且遞增。
1.當修改或刪除記錄時,會插入一條新記錄并指向前一個版本的記錄,并標記該記錄的操作類型和事務id。
2當開啟事務并查詢表時,會為該事務保存一個快照read-view,保存了當前還未提交的事務id的列表和最大的事務id。
3.如果是可重復讀,那么該事務再次讀取數據時會根據保存的read-view,去undo日志查找對其可見的版本列并返回。
4.如果查找到的最新記錄的事務id在read-view的事務id列表中,那么根據指針查找上一版本的記錄,直到找到對其可見的記錄。
2.n 總結一下
2.n.1 InnoDB行鎖
基于索引實現,無索引或未命中索引,將鎖全表
2.n.2 不可重復讀和幻讀的區別
不可重復讀偏重于在連接1的事務開啟期間,指定區段內的數據被其他連接更新或刪除,連接1再次讀取發現不一致。
幻讀偏重于在連接1的事務開啟期間,指定區段內的數據被其他連接插入了新數據庫,連接1再次讀取發現不一致。
2.n.3 防止更新丟失的方案
1.樂觀鎖:
在表中添加一個version字段,每次更新前先查出來,更新時,version也作為where后面的條件,比較是否是剛才查出來的version。
2.悲觀鎖:
每次更新前,select *...where... for update ,先加鎖,然后再更新,注意盡量鎖定指定record(即where后盡量是唯一索引)
2.n.4 SQL是否觸發排他鎖或共享鎖(RR級別下)
1.select... from...
無鎖
2.select...from...in share mode
命中索引處加next-key lock,聚簇索引處加排他鎖
3.select...from...for update
命中索引處加next-key lock,聚簇索引處加排他鎖
4.update/delete...from...
命中索引處自動添加next-key lock,聚簇索引處加排他鎖
5.insert into...
排他鎖/插入意向鎖
2.n.5 next-key lock
1.record lock
只鎖定指定一行
2.gap lock
鎖定記錄前的一段范圍(不包含記錄)
3.next-key lock
是record lock和gap lock的結合,鎖定記錄及記錄前的一段范圍
#注意
1.next-key lock只在默認級別(即RR級別)下有效,RC級別下只有record lock
2.如果sql語句中where條件未命中索引,則鎖全表
3.如果sql語句where條件命中非唯一索引,則產生next-key lock
4.如果sql語句where條件命中唯一索引(非聯合主鍵),則產生record lock.
2.n.6 快照讀和當前讀
#快照讀
單純的select操作,不包括select ... lock in share mode, select ... for update。
Read Committed隔離級別:每次select都生成一個快照讀。
Read Repeatable隔離級別:開啟事務后第一個select語句才是快照讀的地方,而不是一開啟事務就快照讀。
快照讀的實現方式:undolog和多版本并發控制MVCC
#當前讀
select...lock in share mode (共享讀鎖)
select...for update
update , delete , insert
當前讀, 讀取的是最新版本, 并且對讀取的記錄加鎖, 阻塞其他事務同時改動相同記錄,避免出現安全問題。
當前讀的實現方式:next-key鎖(行記錄鎖+Gap間隙鎖)
3.事務失效的常見原因
3.1 MySQL使用了 MyISAM 存儲引擎
MySQL 的 MyISAM 引擎是不支持事務操作的,InnoDB 才是支持事務的引擎,一般要支持事務都會使用 InnoDB。
從 MySQL 5.5.5 開始的默認存儲引擎是:InnoDB,之前默認的都是:MyISAM。
3.2 @Transactional所在的類未被Spring管理
// @Service
public class StuServiceImpl {
@Transactional
public void add(Stu stu) {
// ...
}
}
3.3 數據源沒有配置事務管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
3.4 方法不是 public的
@Service
public class StuServiceImpl {
@Transactional
void add(Stu stu) {
// ...
}
}
3.5 自身調用問題
3.5.1 自身非事務方法調用自身事務方法不生效
@Service
public class StuServiceImpl {
public void add(Stu stu) {
addStu(stu);
}
@Transactional
public void addStu(Stu stu) {
// ...
}
}
3.5.2 自身事務方法調用自身事務方法(但后者開啟了新事物)不生效
@Service
public class StuServiceImpl {
@Transactional
public void add(Stu stu) {
addStu(stu);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addStu(Stu stu) {
// ...
}
}
3.6 不支持事務
// Propagation.NOT_SUPPORTED: 表示不以事務運行,當前若存在事務則掛起
@Service
public class StuServiceImpl {
@Transactional
public void add(Stu stu) {
addStu(stu);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void addStu(Stu stu) {
// ...
}
}
3.7 異常被捕獲了
@Service
public class StuServiceImpl {
@Transactional
public void add(Stu stu) {
try {
// ...
}catch (Throwable e){
// ...
}
}
}
3.8 異常類型錯誤或格式配置錯誤
@Service
public class StuServiceImpl {
// 沒配置 rollbackFor 時, 默認是 RuntimeException & Error, 當是其他異常時, 事務默認不回滾
@Transactional
public void add(Stu stu) {
try {
// ...
}catch (Throwable e){
throw new Exception("error");
}
}
}
參考資料
https://mp.weixin.qq.com/s/6EpeHAF5UmFzEuaQPWjdTw
https://www.cnblogs.com/immer/p/10930020.html (數據更新丟失方案)