事務(wù)(Transaction)是并發(fā)控制的基本單位。所謂事務(wù),就是一個(gè)操作序列,要么全部執(zhí)行,要么都不執(zhí)行,它是一個(gè)不可分割的工作單位。比如下單,商品庫(kù)存減少和生成用戶訂單,這兩個(gè)操作要么都執(zhí)行要么都不執(zhí)行,所以應(yīng)該把它們視為一個(gè)事務(wù)。
事務(wù)的提出主要是為了解決并發(fā)情況下保持?jǐn)?shù)據(jù)一致性的問(wèn)題,它有以下幾個(gè)特性:
- 原子性:事務(wù)中的包含的操作被視為一個(gè)邏輯單元,要么全部成功,要么全部失敗。
- 一致性:事務(wù)只有合法的數(shù)據(jù)可以被寫入數(shù)據(jù)庫(kù),否則事務(wù)應(yīng)該將其回滾到最初狀態(tài)。
- 隔離性:事務(wù)允許多個(gè)用戶對(duì)同一個(gè)數(shù)據(jù)進(jìn)行并發(fā)訪問(wèn),而不破壞數(shù)據(jù)的正確性和完整性。同時(shí)事務(wù)的修改必須與其他事務(wù)的修改相互獨(dú)立。
- 持久性:事務(wù)完成之后,它對(duì)系統(tǒng)的影響是永久性的,即使出現(xiàn)致命的系統(tǒng)故障也將保持一致。
以上特性,原子性是基礎(chǔ),隔離性是手段,一致性和持久性是目標(biāo)。
既然隔離性是手段,而要求多個(gè)用戶之間的并發(fā)數(shù)據(jù)操作又很難保證互不干擾,那就必須要有一個(gè)共同遵守的規(guī)范。這個(gè)規(guī)范就是事務(wù)隔離級(jí)別。
1. 隔離級(jí)別
MySQL標(biāo)準(zhǔn)定義了4種隔離級(jí)別,低的隔離級(jí)別一般支持更高的并發(fā)處理,擁有更低的系統(tǒng)開銷。由低到高:Read Uncommitted < Read Committed < Repeatable Read < Serializable。
隔離級(jí)別的主要目的是處理數(shù)據(jù)一致性,它是保證一致性的重要手段和工具。假如有事務(wù)A和B并發(fā)執(zhí)行,在保證一致性的時(shí)候,就可能會(huì)遇到臟讀、不可重復(fù)讀和幻讀等問(wèn)題。
| 事務(wù)隔離級(jí)別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
|---|---|---|---|
| READ UNCOMMITTED | 允許 | 允許 | 允許 |
| READ COMMITTED | 禁止 | 允許 | 允許 |
| REPEATABLE READ | 禁止 | 禁止 | 允許 |
| SERIALIZABLE | 禁止 | 禁止 | 禁止 |
- 臟讀:讀到了臟數(shù)據(jù),事務(wù)A讀取事務(wù)B未提交的數(shù)據(jù),并又在這個(gè)基礎(chǔ)上做了其他操作(禁止)。
- 不可重復(fù)讀:事務(wù)A讀取了事務(wù)B已提交的修改數(shù)據(jù)(正常)。
- 幻讀:事務(wù)A讀取了事務(wù)B已提交的新增數(shù)據(jù)(正常)。
由上面可以看出,臟讀是必須禁止的,不可重復(fù)讀和幻讀雖然對(duì)當(dāng)前事務(wù)來(lái)說(shuō)不可理解,但對(duì)于最終結(jié)果來(lái)說(shuō)是正確的,沒(méi)有影響到數(shù)據(jù)的準(zhǔn)確性,可以不作考慮。所以說(shuō),數(shù)據(jù)庫(kù)起碼要做到不可臟讀。如果對(duì)數(shù)據(jù)一致性要求非常高,隔離界別就要達(dá)到SERIALIZABLE了,但一般不需要,因?yàn)樗鼤?huì)明顯降低數(shù)據(jù)庫(kù)的并發(fā)性能。MySQL默認(rèn)的隔離級(jí)別是REPEATABLE READ。
在MySQL中,事務(wù)有兩種模式:自動(dòng)提交(默認(rèn))和非自動(dòng)提交。
- 自動(dòng)提交:每一條語(yǔ)句執(zhí)行完把它做出的修改立刻自動(dòng)提交給數(shù)據(jù)庫(kù),并使之持久化。
- 非自動(dòng)提交:用start transaction掛起自動(dòng)提交模式,之后任何語(yǔ)句都將成為事務(wù)的一部分,直到發(fā)出commit或rollback語(yǔ)句來(lái)提交或撤銷。
總的來(lái)說(shuō)start transaction和DDL(數(shù)據(jù)定義語(yǔ)句)都會(huì)隱式地提交一個(gè)事務(wù),因此這些語(yǔ)句不能放在事務(wù)中執(zhí)行。DDL基本上就是除了增刪改查之外的語(yǔ)句。
2. 事務(wù)嵌套與傳播方式
事務(wù)嵌套就是在A函數(shù)中調(diào)用了B函數(shù),A函數(shù)使用了事務(wù),并且在事務(wù)中調(diào)用了B函數(shù),B函數(shù)也有事務(wù),這樣就出現(xiàn)了事務(wù)嵌套,PDO API事務(wù)函數(shù)基本是包含start transaction的,如果框架事務(wù)開始和提交不做特殊處理,使用原生的PDO API事務(wù)函數(shù),互相嵌套的時(shí)候就會(huì)造成隱式的提交,這樣事務(wù)肯定就是錯(cuò)誤的。那么碰到這種問(wèn)題怎么處理呢?通常我們需要在應(yīng)用層去處理,一般框架的ORM都會(huì)對(duì)此進(jìn)行處理。
這里介紹一種思路,在beginTransaction函數(shù)中,用一個(gè)變量level記錄當(dāng)前嵌套的級(jí)別,level=1時(shí)說(shuō)明沒(méi)有嵌套,直接start transaction就可以了,如果大于1,就是有嵌套了,創(chuàng)建一個(gè)savepoint(mysql提供的功能),savepoint就是事務(wù)記錄點(diǎn),當(dāng)需要回滾時(shí)可以只回滾到這個(gè)點(diǎn)。最后是commit函數(shù)的處理,如果level=1直接回滾,否則回滾到savepoint。
beginTransaction和commit是配對(duì)的處理方式。更簡(jiǎn)單粗暴的方式是把嵌套事務(wù)合并到外部事務(wù)中,不考慮嵌套。
嵌套事務(wù)涉及一個(gè)嵌套的規(guī)則,就是說(shuō)當(dāng)一個(gè)方法A調(diào)用另一個(gè)方法B時(shí),事務(wù)操作的方法,我們稱之為事務(wù)傳播規(guī)則,一共有7種規(guī)則,最常見的是PROPAGATION_REQUIRED這個(gè)事務(wù)傳播行為,即如果A沒(méi)有事務(wù),就新建一個(gè)事務(wù);如果有,就把B加入當(dāng)前事務(wù)。