個人網(wǎng)站建站系統(tǒng)百度搜索排名
文章目錄
- 數(shù)據(jù)庫事務(wù)
- 事務(wù)特性(ACID)
- 原子性(Atomicity)
- 一致性(Consistency)
- 隔離性(Isolation)
- 持久性(Durability)
- 隔離級別
- Read Uncommitted(讀未提交)
- Read Committed(讀已提交)
- Repeatable Read(可重復(fù)讀)
- Serializable(可串行化)
- Spring事務(wù)機制
- 實現(xiàn)方式
- 提交方式
- 事務(wù)隔離級別
- 事務(wù)傳播行為
- 事務(wù)回滾規(guī)則
- 事務(wù)常用配置
- 失效場景
數(shù)據(jù)庫事務(wù)
什么叫事務(wù)?
事務(wù)是一系列對系統(tǒng)中數(shù)據(jù)進行訪問與更新的操作組成的一個程序邏輯單元。即不可分割的許多基礎(chǔ)數(shù)據(jù)庫操作。
事務(wù)特性(ACID)
原子性(Atomicity)
原子性是指一個事務(wù)(事務(wù)是最小的執(zhí)行單位)是一個不可分割的工作單元,其中的操作要么都做,要么都不做。如果事務(wù)中一個sql語句執(zhí)行失敗,則已執(zhí)行的語句也必須回滾,數(shù)據(jù)庫退回到事務(wù)前的狀態(tài)。
實現(xiàn)原理
① 回滾日志(undo log)
InnoDB實現(xiàn)回滾靠的是undo log。當(dāng)事務(wù)對數(shù)據(jù)庫進行修改時,InnoDB會生成對應(yīng)的undo log。如果事務(wù)執(zhí)行失敗或調(diào)用了rollback,導(dǎo)致事務(wù)需要回滾,便可利用undo log中的信息將數(shù)據(jù)回滾到修改前。
- 對于每個
insert
,回滾時會執(zhí)行delete
- 對于每個
delete
,回滾時會執(zhí)行insert
- 對于每個
update
,回滾時會執(zhí)行一個相反的update
,把數(shù)據(jù)改回去
一致性(Consistency)
一致性是指事務(wù)執(zhí)行結(jié)束后,數(shù)據(jù)庫的完整性約束沒有被破壞,事務(wù)執(zhí)行的前后都是合法的數(shù)據(jù)狀態(tài)。
數(shù)據(jù)庫的完整性約束包括但不限于:實體完整性(如行的主鍵存在且唯一)、列完整性(如字段的類型、大小、長度要符合要求)、外鍵約束、用戶自定義完整性(如轉(zhuǎn)賬前后,兩個賬戶余額的和應(yīng)該不變)。假如A賬戶給B賬戶轉(zhuǎn)10塊錢,不管成功與否,A和B的總金額是不變的。
實現(xiàn)原理
一致性是事務(wù)追求的最終目標。前面提到的原子性、持久性和隔離性,都是為了保證數(shù)據(jù)庫狀態(tài)的一致性。此外,除了數(shù)據(jù)庫層面的保障,一致性的實現(xiàn)也需要應(yīng)用層面進行保障。實現(xiàn)一致性的措施包括:
① 保證原子性、持久性和隔離性。如果這些特性無法保證,事務(wù)的一致性也無法保證
② 數(shù)據(jù)庫本身提供保障。如不允許向整型列插入字符串值、字符串長度不能超過列的限制等
③ 應(yīng)用層面進行保障。如轉(zhuǎn)賬操作只扣除轉(zhuǎn)賬者余額,而未增加接收者余額
隔離性(Isolation)
**隔離性是指事務(wù)內(nèi)部的操作與其它事務(wù)是隔離的,并發(fā)執(zhí)行的各個事務(wù)之間不能互相干擾。**嚴格的隔離性,對應(yīng)了事務(wù)隔離級別中的Serializable (可串行化),但實際應(yīng)用中出于性能方面的考慮很少會使用可串行化。
實現(xiàn)原理
隔離性追求的是并發(fā)情形下事務(wù)之間互不干擾。主要分為兩個方面:
① 加鎖機制保證隔離性:(一個事務(wù))寫操作對(另一個事務(wù))寫操作的影響
事務(wù)在修改數(shù)據(jù)之前,需要先獲得相應(yīng)的鎖;獲得鎖之后,事務(wù)便可以修改數(shù)據(jù);該事務(wù)操作期間,這部分數(shù)據(jù)是鎖定的,其它事務(wù)如果需要修改數(shù)據(jù),需要等待當(dāng)前事務(wù)提交或回滾后釋放鎖。
② MVCC(多版本并發(fā)控制)保證隔離性:(一個事務(wù))寫操作對(另一個事務(wù))讀操作的影響
MVCC全稱Multi-Version Concurrency Control,即多版本的并發(fā)控制協(xié)議。最大優(yōu)點是讀不加鎖,因此讀寫不沖突,并發(fā)性能好。InnoDB的MVCC實現(xiàn)了多個版本的數(shù)據(jù)可共存,主要基于以下技術(shù)及數(shù)據(jù)結(jié)構(gòu):
- 隱藏列:在Innodb引擎中每行數(shù)據(jù)都會有兩個隱藏列(實際是三個列)
- 隱藏id(
id
,如果建表時沒有顯式指定,則會生成這個隱藏id作為主鍵,實際和mvcc沒有關(guān)系) - 創(chuàng)建版本號(
data_trx_id
,事務(wù)id):用來標識最近對本行記錄做修改的事務(wù) id - 回滾指針(
data_roll_pointer
,指向undo log的指針)
- 隱藏id(
- 基于undo log版本鏈:每條undo log也會指向更早版本的undo log,從而形成一條版本鏈
- ReadView:**通過隱藏列和版本鏈可以將數(shù)據(jù)恢復(fù)到指定版本,但具體要恢復(fù)到哪個版本,則需要根據(jù)ReadView來確定。**當(dāng)進行查詢操作時,事務(wù)會生成一個ReadView(是一個事務(wù)快照),準確來說是當(dāng)前時間點系統(tǒng)內(nèi)活躍的事務(wù)列表,也就是說系統(tǒng)內(nèi)所有未提交的事務(wù),都會記錄在這個Readview內(nèi),事務(wù)就根據(jù)它來判斷哪些數(shù)據(jù)是可見的,哪些是不可見的。在每一條 SQL 開始的時候被創(chuàng)建,有幾個重要屬性:
- trx_ids: 當(dāng)前系統(tǒng)活躍(未提交)事務(wù)版本號集合
- low_limit_id: 創(chuàng)建當(dāng)前 read view 時“當(dāng)前系統(tǒng)最大事務(wù)版本號+1”
- up_limit_id: 創(chuàng)建當(dāng)前read view 時“系統(tǒng)正處于活躍事務(wù)最小版本號”
- creator_trx_id: 創(chuàng)建當(dāng)前read view的事務(wù)版本號
MVCC查詢流程
現(xiàn)在開始查詢,一個 select 過來了,找到了一行數(shù)據(jù)。
- data_trx_id < up_limit_id:說明數(shù)據(jù)在當(dāng)前事務(wù)之前就存在了,顯示
- data_trx_id >= low_limit_id:說明該數(shù)據(jù)是在當(dāng)前read view 創(chuàng)建后才產(chǎn)生的,數(shù)據(jù)不顯示。不顯示怎么辦,根據(jù) data_roll_pointer 從 undo log 中找到歷史版本,找不到就空
- up_limit_id < data_trx_id < low_limit_id:就要看隔離級別了
MVCC應(yīng)用場景
在Mysql的InnoDB引擎中,只有已提交讀和可重復(fù)讀這兩種隔離級別的事務(wù)采用了MVCC機制:
- 已提交讀(READ COMMITTD):事務(wù)中的每次讀操作都會生成一個新的ReadView,也就是說如果這期間某個事務(wù)提交了,那么它就會從ReadView中移除。這樣確保事務(wù)每次讀操作都能讀到相對比較新的數(shù)據(jù)
- 可重復(fù)讀(REPEATABLE READ):事務(wù)只有在第一次進行讀操作時才會生成一個ReadView,后續(xù)的讀操作都會重復(fù)使用這個ReadView。也就是說如果在此期間有其他事務(wù)提交了,那么對于可重復(fù)讀來說也是不可見的,因為對它來說,事務(wù)活躍狀態(tài)在第一次進行讀操作時就已經(jīng)確定下來,后面不會修改了
持久性(Durability)
持久性是指事務(wù)一旦提交,它對數(shù)據(jù)庫的改變就應(yīng)該是永久性的,即使數(shù)據(jù)庫發(fā)生故障也不受影響。
實現(xiàn)原理
① redo log
當(dāng)數(shù)據(jù)修改時,除了修改Buffer Pool中的數(shù)據(jù),還會在redo log記錄這次操作;當(dāng)事務(wù)提交時,會調(diào)用fsync接口對redo log進行刷盤(刷臟頁)。如果MySQL宕機,重啟時可以讀取redo log中的數(shù)據(jù),對數(shù)據(jù)庫進行恢復(fù)。redo log采用的是WAL(Write-ahead logging,預(yù)寫式日志),所有修改先寫入日志,再更新到Buffer Pool,保證了數(shù)據(jù)不會因MySQL宕機而丟失,從而滿足了持久性要求。
為什么redo log寫入磁盤比直接將Buffer Pool中修改數(shù)據(jù)寫入磁盤(刷臟)快?
- 刷臟是隨機I/O,因為每次修改的數(shù)據(jù)位置隨機,但寫redo log是追加操作,屬于順序I/O
- 刷臟是以數(shù)據(jù)頁(Page)為單位的,MySQL默認頁大小是16KB,一個Page上一個小修改都要整頁寫入;而redo log中只包含真正需要寫入的部分,無效I/O大大減少
隔離級別
事務(wù)并發(fā)問題
在事務(wù)的并發(fā)操作中,不做隔離操作則可能會出現(xiàn) 臟讀、不可重復(fù)讀、幻讀 問題:
-
臟讀:指一個事務(wù)讀取到了另一個未提交事務(wù)修改過的數(shù)據(jù)
事務(wù)A中讀到了事務(wù)B中未提交的更新數(shù)據(jù)內(nèi)容,然后B回滾操作,那么A讀取到的數(shù)據(jù)是臟數(shù)據(jù)。
-
不可重復(fù)讀:同一個事務(wù)內(nèi),前后多次讀取,讀取到的數(shù)據(jù)內(nèi)容不一致
事務(wù)A讀到事務(wù)B已經(jīng)提交后的數(shù)據(jù),即事務(wù)A多次讀取同一數(shù)據(jù)時,返回結(jié)果不一致。
-
幻讀:指一個事務(wù)先根據(jù)某些搜索條件查詢出一些記錄,在該事務(wù)未提交時,另一個事務(wù)寫入了一些符合那些搜索條件的記錄(如insert、delete、update),再次查詢出的結(jié)果則出現(xiàn)不一致
事物A執(zhí)行select后,事物B增或刪了一條數(shù)據(jù),事務(wù)A再執(zhí)行同一條SQL后發(fā)現(xiàn)多或少了一條數(shù)據(jù)。
-
第一類丟失更新: A事務(wù)撤銷事務(wù)時,覆蓋了B事務(wù)提交的事務(wù)(現(xiàn)代關(guān)系型數(shù)據(jù)庫中已經(jīng)不會發(fā)生)
-
第二類丟失更新: A事務(wù)提交事務(wù)時,覆蓋了B事務(wù)提交的事務(wù)(是不可重復(fù)讀的特殊情況)
小結(jié):不可重復(fù)讀的和幻讀很容易混淆,不可重復(fù)讀側(cè)重于修改,幻讀側(cè)重于新增或刪除。解決不可重復(fù)讀的問題只需鎖住滿足條件的行,解決幻讀需要鎖表。查看 mysql
事務(wù)隔離級別:show variables like 'tx_iso%';
。
InnoDB存儲引擎下的四種隔離級別發(fā)生問題的可能性如下:
隔離級別 | 第一類丟失更新 | 第二類丟失更新 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
---|---|---|---|---|---|
Read Uncommitted(讀未提交) | 不可能 | 可能 | 可能 | 可能 | 可能 |
Read Committed(讀已提交) | 不可能 | 可能 | 不可能 | 可能 | 可能 |
Repeatable Read(可重復(fù)讀) | 不可能 | 不可能 | 不可能 | 不可能 | 可能 |
Serializable(串行化) | 不可能 | 不可能 | 不可能 | 不可能 | 不可能 |
Read Uncommitted(讀未提交)
只限制了兩個數(shù)據(jù)不能同時修改,但即使事務(wù)未提交也會讀取到其它事務(wù)未提交的內(nèi)容(可能會被回滾)。會有臟讀、重復(fù)讀、幻讀的問題,讀取未提交的數(shù)據(jù),也被稱之為臟讀(Dirty Read)。
特點:最低級別,任何情況都無法保證
數(shù)據(jù)庫鎖情況
- 讀取數(shù)據(jù):未加鎖,每次都讀到最新數(shù)據(jù),性能最好
- 寫入數(shù)據(jù):只對數(shù)據(jù)增加行級共享鎖,寫完釋放
Read Committed(讀已提交)
當(dāng)前事務(wù)只能讀取到其它事務(wù)已提交的數(shù)據(jù)。因同一事務(wù)的其它實例在該實例處理期間可能會有新的commit,所以同一select可能返回不同結(jié)果,這就是所謂的不可重復(fù)讀(Nonrepeatable Read)。該隔離級別解決了臟讀問題,但還是會存在重復(fù)讀、幻讀問題。
特點:避免臟讀
臟讀解決方案:基于樂觀鎖理論的MVCC(多版本并發(fā)控)實現(xiàn)
數(shù)據(jù)庫鎖情況
- 讀取數(shù)據(jù):加行級共享鎖(讀到時才加鎖),讀完后立即釋放
- 寫入數(shù)據(jù):在更新時的瞬間對其加行級排它鎖,直到事務(wù)結(jié)束才釋放
Repeatable Read(可重復(fù)讀)
限制了讀取數(shù)據(jù)時不可以進行修改,所以解決了不能重復(fù)讀的問題。但是讀取范圍數(shù)據(jù)的時候,是可以插入或刪除數(shù)據(jù),所以還會存在**幻讀(Phantom Read)**問題。
幻讀 是戶讀取某一范圍的數(shù)據(jù)行時,另一個事務(wù)又在該范圍內(nèi)插入了新行,當(dāng)該用戶再讀取該范圍的數(shù)據(jù)行時,會發(fā)現(xiàn)有新的“幻影” 行。
特點:避免臟讀、不可重復(fù)讀。MySQL默認事務(wù)隔離級別
不可重復(fù)讀解決方案:基于樂觀鎖理論的MVCC(多版本并發(fā)控)實現(xiàn)
數(shù)據(jù)庫鎖情況
- 讀取數(shù)據(jù):開始讀取的瞬間對其增加行級共享鎖,直到事務(wù)結(jié)束才釋放
- 寫入數(shù)據(jù):開始更新的瞬間對其增加行級排他鎖,直到事務(wù)結(jié)束才釋放
Serializable(可串行化)
所有事務(wù)都是進行串行化順序執(zhí)行的。可以避免臟讀、不可重復(fù)讀與幻讀所有并發(fā)問題。但該事務(wù)隔離級別下,事務(wù)執(zhí)行很耗性能。
特點:避免臟讀、不可重復(fù)讀、幻讀
數(shù)據(jù)庫鎖情況
- 讀取數(shù)據(jù):先對其加表級共享鎖 ,直到事務(wù)結(jié)束才釋放
- 寫入數(shù)據(jù):先對其加表級排他鎖 ,直到事務(wù)結(jié)束才釋放
Spring事務(wù)機制
實現(xiàn)方式
在Spring中事務(wù)有兩種實現(xiàn)方式:
- 編程式事務(wù)管理: 編程式事務(wù)管理使用
TransactionTemplate
或直接使用底層的PlatformTransactionManager
- 聲明式事務(wù)管理: 建立在
AOP
之上的。其本質(zhì)是對方法前后進行攔截,然后在目標方法開始之前創(chuàng)建或者加入一個事務(wù),在執(zhí)行完目標方法之后根據(jù)執(zhí)行情況提交或者回滾事務(wù)。聲明式事務(wù)管理不需要入侵代碼,通過@Transactional
就可以進行事務(wù)操作,更快捷而且簡單
提交方式
默認情況下,數(shù)據(jù)庫處于自動提交模式。每一條語句處于一個單獨的事務(wù)中,在這條語句執(zhí)行完畢時,如果執(zhí)行成功則隱式的提交事務(wù),如果執(zhí)行失敗則隱式的回滾事務(wù)。
對于正常的事務(wù)管理,是一組相關(guān)的操作處于一個事務(wù)之中,因此必須關(guān)閉數(shù)據(jù)庫的自動提交模式。不過,這個我們不用擔(dān)心,Spring會將底層連接的自動提交特性設(shè)置為false。也就是在使用Spring進行事物管理的時候,Spring會將是否自動提交設(shè)置為false,等價于JDBC中的 connection.setAutoCommit(false);
,在執(zhí)行完之后在進行提交,connection.commit();
。
事務(wù)隔離級別
隔離級別是指若干個并發(fā)的事務(wù)之間的隔離程度。
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void addGoods(){......
}
枚舉類Isolation中定義了五種隔離級別:
DEFAULT
:默認值。表示使用底層數(shù)據(jù)庫的默認隔離級別。對大部分數(shù)據(jù)庫,通常這值就是READ_COMMITTEDREAD_UNCOMMITTED
:該隔離級別表示一個事務(wù)可以讀取另一個事務(wù)修改但還沒有提交的數(shù)據(jù)。該級別不能防止臟讀,不可重復(fù)讀和幻讀,因此很少使用該隔離級別READ_COMMITTED
:該隔離級別表示一個事務(wù)只能讀取另一個事務(wù)已經(jīng)提交的數(shù)據(jù)。該級別可以防止臟讀,這也是大多數(shù)情況下的推薦值REPEATABLE_READ
:該隔離級別表示一個事務(wù)在整個過程中可以多次重復(fù)執(zhí)行某個查詢,并且每次返回的記錄都相同。該級別可以防止臟讀和不可重復(fù)讀SERIALIZABLE
:所有的事務(wù)依次逐個執(zhí)行,這樣事務(wù)之間就完全不可能產(chǎn)生干擾,也就是說,該級別可以防止臟讀、不可重復(fù)讀以及幻讀。但是這將嚴重影響程序的性能。通常情況下也不會用到該級別
事務(wù)傳播行為
事務(wù)的傳播性一般用在事務(wù)嵌套的場景,如一個事務(wù)方法里面調(diào)用了另外一個事務(wù)方法,那兩個方法是各自作為獨立的方法提交還是內(nèi)層事務(wù)合并到外層事務(wù)一起提交,這就需要事務(wù)傳播機制配置來確定怎么樣執(zhí)行。
@Transactional(propagation=Propagation.REQUIRED)
public void addGoods(){......
}
枚舉類Propagation中定義了七種事務(wù)傳播機制如下:
REQUIRED
(required,要求,Spring默認):當(dāng)前存在事務(wù),則加入該事務(wù);當(dāng)前沒有事務(wù),則創(chuàng)建一個新的事務(wù)REQUIRES_NEW
(requires_new,要求新的):創(chuàng)建一個新事務(wù),如果存在當(dāng)前事務(wù),則掛起該事務(wù)SUPPORTS
(supports,支持):如果當(dāng)前存在事務(wù),則加入當(dāng)前事務(wù);如果當(dāng)前沒有事務(wù),就以非事務(wù)方法執(zhí)行NOT_SUPPORTED
(not_supported,不支持):始終以非事務(wù)方式執(zhí)行,如果當(dāng)前存在事務(wù),則掛起當(dāng)前事務(wù)NEVER
(never,都不):不使用事務(wù),如果當(dāng)前事務(wù)存在,則拋出異常MANDATORY
(mandatory,強制):如果當(dāng)前存在事務(wù),則加入當(dāng)前事務(wù);如果當(dāng)前事務(wù)不存在,則拋出異常NESTED
(nested,嵌套)如果當(dāng)前事務(wù)存在,則在嵌套事務(wù)中執(zhí)行,否則REQUIRED的操作一樣(開啟一個事務(wù))
事務(wù)回滾規(guī)則
指示Spring事務(wù)管理器回滾一個事務(wù)的推薦方法是在當(dāng)前事務(wù)的上下文內(nèi)拋出異常。Spring事務(wù)管理器會捕捉任何未處理的異常,然后依據(jù)規(guī)則決定是否回滾拋出異常的事務(wù)。
默認配置下,Spring只有在拋出的異常為運行時unchecked異常時才回滾該事務(wù),也就是拋出的異常為RuntimeException的子類(Errors也會導(dǎo)致事務(wù)回滾),而拋出checked異常則不會導(dǎo)致事務(wù)回滾。
可以明確的配置在拋出那些異常時回滾事務(wù),包括checked異常。也可以明確定義那些異常拋出時不回滾事務(wù)。
事務(wù)常用配置
-
readOnly
該屬性用于設(shè)置當(dāng)前事務(wù)是否為只讀事務(wù),設(shè)置為true表示只讀,false則表示可讀寫,默認值為false。例如:@Transactional(readOnly=true)
-
rollbackFor
該屬性用于設(shè)置需要進行回滾的異常類數(shù)組,當(dāng)方法中拋出指定異常數(shù)組中的異常時,則進行事務(wù)回滾。例如:指定單一異常類:@Transactional(rollbackFor=RuntimeException.class)指定多個異常類:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
-
rollbackForClassName
該屬性用于設(shè)置需要進行回滾的異常類名稱數(shù)組,當(dāng)方法中拋出指定異常名稱數(shù)組中的異常時,則進行事務(wù)回滾。例如:指定單一異常類名稱@Transactional(rollbackForClassName=”RuntimeException”)指定多個異常類名稱:@Transactional(rollbackForClassName={“RuntimeException”,”Exception”})
-
noRollbackFor
該屬性用于設(shè)置不需要進行回滾的異常類數(shù)組,當(dāng)方法中拋出指定異常數(shù)組中的異常時,不進行事務(wù)回滾。例如:指定單一異常類:@Transactional(noRollbackFor=RuntimeException.class)指定多個異常類:@Transactional(noRollbackFor={RuntimeException.class, Exception.class})
-
noRollbackForClassName
該屬性用于設(shè)置不需要進行回滾的異常類名稱數(shù)組,當(dāng)方法中拋出指定異常名稱數(shù)組中的異常時,不進行事務(wù)回滾。例如:指定單一異常類名稱:@Transactional(noRollbackForClassName=”RuntimeException”)指定多個異常類名稱:@Transactional(noRollbackForClassName={“RuntimeException”,”Exception”})
-
propagation
該屬性用于設(shè)置事務(wù)的傳播行為。例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)
-
isolation
該屬性用于設(shè)置底層數(shù)據(jù)庫的事務(wù)隔離級別,事務(wù)隔離級別用于處理多事務(wù)并發(fā)的情況,通常使用數(shù)據(jù)庫的默認隔離級別即可,基本不需要進行設(shè)置
-
timeout
該屬性用于設(shè)置事務(wù)的超時秒數(shù),默認值為-1表示永不超時
失效場景
- @Transactional 應(yīng)用在非 public 修飾的方法上
- 數(shù)據(jù)庫引擎要不支持事務(wù)
- 由于propagation 設(shè)置錯誤,導(dǎo)致注解失效
- rollbackFor 設(shè)置錯誤,@Transactional 注解失效
- 方法之間的互相調(diào)用也會導(dǎo)致@Transactional失效
- 異常被你的 catch“吃了”導(dǎo)致@Transactional失效