在日常開發中,尤其是業務開發,少不了利用 Java 對數據庫進行基本的增刪改查等數據操作,這也是 Java 工程師的必備技能之一。做好數據操作,不僅僅需要對 Java 語言相關框架的掌握,更需要對各種數據庫自身體系結構的理解。今天這一講,作為補充 Java 面試考察知識點的完整性,關于數據庫的應用和細節還需要在實踐中深入學習。
今天我要問你的問題是,談談 MySQL 支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
## 典型回答
所謂隔離級別([Isolation Level](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Isolation_levels)),就是在數據庫事務中,為保證并發數據讀寫的正確性而提出的定義,它并不是 MySQL 專有的概念,而是源于[ANSI](https://en.wikipedia.org/wiki/American_National_Standards_Institute)/[ISO](https://en.wikipedia.org/wiki/International_Organization_for_Standardization)制定的[SQL-92](https://en.wikipedia.org/wiki/SQL-92)標準。
每種關系型數據庫都提供了各自特色的隔離級別實現,雖然在通常的[定義](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Isolation_levels)中是以鎖為實現單元,但實際的實現千差萬別。以最常見的 MySQL InnoDB 引擎為例,它是基于[MVCC](https://dev.mysql.com/doc/refman/8.0/en/innodb-multi-versioning.html)(Multi-Versioning Concurrency Control)和鎖的復合實現,按照隔離程度從低到高,MySQL 事務隔離級別分為四個不同層次:
* 讀未提交(Read uncommitted),就是一個事務能夠看到其他事務尚未提交的修改,這是最低的隔離水平,允許[臟讀](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Dirty_reads)出現。
* 讀已提交(Read committed),事務能夠看到的數據都是其他事務已經提交的修改,也就是保證不會看到任何中間性狀態,當然臟讀也不會出現。讀已提交仍然是比較低級別的隔離,并不保證再次讀取時能夠獲取同樣的數據,也就是允許其他事務并發修改數據,允許不可重復讀和幻象讀(Phantom Read)出現。
* 可重復讀(Repeatable reads),保證同一個事務中多次讀取的數據是一致的,這是 MySQL InnoDB 引擎的默認隔離級別,但是和一些其他數據庫實現不同的是,可以簡單認為 MySQL 在可重復讀級別不會出現幻象讀。
* 串行化(Serializable),并發事務之間是串行化的,通常意味著讀取需要獲取共享讀鎖,更新需要獲取排他寫鎖,如果 SQL 使用 WHERE 語句,還會獲取區間鎖(MySQL 以 GAP 鎖形式實現,可重復讀級別中默認也會使用),這是最高的隔離級別。
至于悲觀鎖和樂觀鎖,也并不是 MySQL 或者數據庫中獨有的概念,而是并發編程的基本概念。主要區別在于,操作共享數據時,“悲觀鎖”即認為數據出現沖突的可能性更大,而“樂觀鎖”則是認為大部分情況不會出現沖突,進而決定是否采取排他性措施。
反映到 MySQL 數據庫應用開發中,悲觀鎖一般就是利用類似 SELECT … FOR UPDATE 這樣的語句,對數據加鎖,避免其他事務意外修改數據。樂觀鎖則與 Java 并發包中的 AtomicFieldUpdater 類似,也是利用 CAS 機制,并不會對數據加鎖,而是通過對比數據的時間戳或者版本號,來實現樂觀鎖需要的版本判斷。
我認為前面提到的 MVCC,其本質就可以看作是種樂觀鎖機制,而排他性的讀寫鎖、雙階段鎖等則是悲觀鎖的實現。
有關它們的應用場景,你可以構建一下簡化的火車余票查詢和購票系統。同時查詢的人可能很多,雖然具體座位票只能是賣給一個人,但余票可能很多,而且也并不能預測哪個查詢者會購票,這個時候就更適合用樂觀鎖。
## 考點分析
今天的問題來源于實際面試,這兩部分問題反映了面試官試圖考察面試者在日常應用開發中,是否學習或者思考過數據庫內部的機制,是否了解并發相關的基礎概念和實踐。
我從普通數據庫應用開發者的角度,提供了一個相對簡化的答案,面試官很有可能進一步從實例的角度展開,例如設計一個典型場景重現臟讀、幻象讀,或者從數據庫設計的角度,可以用哪些手段避免類似情況。我建議你在準備面試時,可以在典型的數據庫上試驗一下,驗證自己的觀點。
其他可以考察的點也有很多,在準備這個問題時你也可以對比 Java 語言的并發機制,進行深入理解,例如,隨著隔離級別從低到高,競爭性(Contention)逐漸增強,隨之而來的代價同樣是性能和擴展性的下降。
數據庫衍生出很多不同的職責方向:
* 數據庫管理員(DBA),這是一個單獨的專業領域。
* 數據庫應用工程師,很多業務開發者就是這種定位,綜合利用數據庫和其他編程語言等技能,開發業務應用。
* 數據庫工程師,更加側重于開發數據庫、數據庫中間件等基礎軟件。
后面兩者與 Java 開發更加相關,但是需要的知識和技能是不同的,所以面試的考察角度也有區別,今天我會分析下對相關知識學習和準備面試的看法。
另外,在數據庫相關領域,Java 工程師最常接觸到的就是 O/R Mapping 框架或者類似的數據庫交互類庫,我會選取最廣泛使用的框架進行對比和分析。
## 知識擴展
首先,我來談談對數據庫相關領域學習的看法,從最廣泛的應用開發者角度,至少需要掌握:
* 數據庫設計基礎,包括數據庫設計中的幾個基本范式,各種數據庫的基礎概念,例如表、視圖、索引、外鍵、序列號生成器等,清楚如何將現實中業務實體和其依賴關系映射到數據庫結構中,掌握典型實體數據應該使用什么樣的數據庫數據類型等。
* 每種數據庫的設計和實現多少會存在差異,所以至少要精通你使用過的數據庫的設計要點。我今天開篇談到的 MySQL 事務隔離級別,就區別于其他數據庫,進一步了解 MVCC、Locking 等機制對于處理進階問題非常有幫助;還需要了解,不同索引類型的使用,甚至是底層數據結構和算法等。
* 常見的 SQL 語句,掌握基礎的 SQL 調優技巧,至少要了解基本思路是怎樣的,例如 SQL 怎樣寫才能更好利用索引、知道如何分析[SQL 執行計劃](https://dev.mysql.com/doc/workbench/en/wb-performance-explain.html)等。
* 更進一步,至少需要了解針對高并發等特定場景中的解決方案,例如讀寫分離、分庫分表,或者如何利用緩存機制等,目前的數據存儲也遠不止傳統的關系型數據庫了。

上面的示意圖簡單總結了我對數據庫領域的理解,希望可以給你進行準備時提供個借鑒。當然在準備面試時并不是一味找一堆書悶頭苦讀,我還是建議從實際工作中使用的數據庫出發,側重于結合實踐,完善和深化自己的知識體系。
接下來我們還是回到 Java 本身,目前最為通用的 Java 和數據庫交互技術就是 JDBC,最常見的開源框架基本都是構建在 JDBC 之上,包括我們熟悉的[JPA](https://www.tutorialspoint.com/jpa/jpa_introduction.htm)/[Hibernate](https://en.wikipedia.org/wiki/Hibernate_(framework))、[MyBatis](http://www.mybatis.org/mybatis-3/)、Spring JDBC Template 等,各自都有獨特的設計特點。
Hibernate 是最負盛名的 O/R Mapping 框架之一,它也是一個 JPA Provider。顧名思義,它是以對象為中心的,其強項更體現在數據庫到 Java 對象的映射,可以很方便地在 Java 對象層面體現外鍵約束等相對復雜的關系,提供了強大的持久化功能。內部大量使用了[Lazy-load](https://en.wikipedia.org/wiki/Lazy_loading)等技術提高效率。并且,為了屏蔽數據庫的差異,降低維護開銷,Hibernate 提供了類 SQL 的 HQL,可以自動生成某種數據庫特定的 SQL 語句。
Hibernate 應用非常廣泛,但是過度強調持久化和隔離數據庫底層細節,也導致了很多弊端,例如 HQL 需要額外的學習,未必比深入學習 SQL 語言更高效;減弱程序員對 SQL 的直接控制,還可能導致其他代價,本來一句 SQL 的事情,可能被 Hibernate 生成幾條,隱藏的內部細節也阻礙了進一步的優化。
而 MyBatis 雖然仍然提供了一些映射的功能,但更加以 SQL 為中心,開發者可以側重于 SQL 和存儲過程,非常簡單、直接。如果我們的應用需要大量高性能的或者復雜的 SELECT 語句等,“半自動”的 MyBatis 就會比 Hibernate 更加實用。
而 Spring JDBC Template 也是更加接近于 SQL 層面,Spring 本身也可以集成 Hibernate 等 O/R Mapping 框架。
關于這些具體開源框架的學習,我的建議是:
* 從整體上把握主流框架的架構和設計理念,掌握主要流程,例如 SQL 解析生成、SQL 執行到結果映射等處理過程到底發生了什么。
* 掌握映射等部分的細節定義和原理,根據我在準備專欄時整理的面試題目,發現很多題目都是偏向于映射定義的細節。
* 另外,對比不同框架的設計和實現,既有利于你加深理解,也是面試考察的熱點方向之一。
今天我從數據庫應用開發者的角度,分析了 MySQL 數據庫的部分內部機制,并且補充了我對數據庫相關面試準備和知識學習的建議,最后對主流 O/R Mapping 等框架進行了簡單的對比。
## 一課一練
關于今天我們討論的題目你做到心中有數了嗎? 今天的思考題是,從架構設計的角度,可以將 MyBatis 分為哪幾層?每層都有哪些主要模塊?
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?