面試官:使用 MySQL 時如果發(fā)現(xiàn)數(shù)據(jù)不一致了,可能是什么原因?
使用 MySQL 時有時候會遇到數(shù)據(jù)不一致的問題。那一般是什么原因會導(dǎo)致數(shù)據(jù)不一致呢?今天來聊一聊這個話題。
1.事務(wù)失效
1.1 單體事務(wù)
事務(wù)失效是造成數(shù)據(jù)不一致的一種常見情況。舉一個例子,客戶下訂單后,訂單表插入一條記錄,插入成功,賬戶表扣減金額,但扣減金額失敗。如下圖:
圖片
假如這兩張表在同一個庫,正常流程是回滾事務(wù),但因?yàn)槭聞?wù)失效,導(dǎo)致訂單表插入成功,賬戶表扣減金額失敗,最終數(shù)據(jù)不一致。
可能造成事務(wù)失效的因素有很多,比如下面集中情況:
- Java 代碼開發(fā)中事務(wù)管理多數(shù)都由 Spring 來管理,但有些情況下 Spring 管理事務(wù)會失效,比如事務(wù)所在方法的定義上有 private、final、static,或者事務(wù)方法是一個內(nèi)部方法,因?yàn)?Spring 無法創(chuàng)建代理;
- 事務(wù)所在方法被內(nèi)部,Spring 也無法代理;
- 異常被捕獲后沒有拋出,或者拋出的異常不是 Spring 事務(wù)管理的異常,造成事務(wù)回滾失敗(Spring 默認(rèn)回滾 RuntimeException)。
1.2 隔離級別
雖然事務(wù)沒有失效,但是因?yàn)槭聞?wù)隔離級別的問題,出現(xiàn)了臟讀、不可重復(fù)讀和幻讀等問題。比如讀取了其他未提交事務(wù)修改的數(shù)據(jù),如果那個事務(wù)最終回滾,讀取到的就是臟數(shù)據(jù),導(dǎo)致不一致。
要解決這個問題,就需要根據(jù)業(yè)務(wù)需求選擇合適的隔離級別。
1.3 分布式事務(wù)
還是用上面客戶下單的例子,如果系統(tǒng)是分布式架構(gòu),訂單服務(wù)和賬戶服務(wù)不在一個應(yīng)用中,那賬戶服務(wù)扣減金額失敗了,訂單服務(wù)無法回滾。
這種情況可以引入分布式事務(wù)框架,比如 Seata 或者 RocketMQ 的分布式事務(wù)。
2.數(shù)據(jù)同步
數(shù)據(jù)同步失敗或未完成,也可能會造成數(shù)據(jù)不一致。
2.1 數(shù)據(jù)未同步
如下圖是一個讀寫分離的主備架構(gòu),應(yīng)用寫主庫,然后從備庫中讀,如果數(shù)據(jù)未同步完成,就會造成問題。
圖片
對一些數(shù)據(jù)敏感的場景,可以選擇從主庫讀取數(shù)據(jù)。
2.2 數(shù)據(jù)同步失敗
再看下面雙主架構(gòu)的例子,假如我們有一張表,里面定義了自增主鍵 id,庫 M1 生成了主鍵 1、2、3,庫 M2 也生成了主鍵 1、2、3,這樣兩邊同步的時候因?yàn)橹麈I沖突,同步失敗。
圖片
要解決這個問題,可以把兩個主庫的起始 id 值設(shè)置為不一樣(比如 M1 id 起始值設(shè)置成 1,把步長設(shè)置成1,M2 起始值設(shè)置成 2,把步長設(shè)置成 2)。如下圖:
圖片
3.系統(tǒng)故障
3.1 主庫奔潰
在主備架構(gòu)中,如果主庫發(fā)生奔潰,InnoDB 利用重做日志進(jìn)行崩潰恢復(fù),這時要保證已提交事務(wù)的數(shù)據(jù)持久化以及未提交事務(wù)的數(shù)據(jù)回滾。這個過程有可能會造成數(shù)據(jù)不一致。
3.2 主備切換
如果發(fā)生主備切換,備庫還有部分?jǐn)?shù)據(jù)沒有同步到新主庫,這時應(yīng)用來讀取數(shù)據(jù)就會出現(xiàn)數(shù)據(jù)不一致。
4.數(shù)據(jù)備份
在數(shù)據(jù)備份和恢復(fù)過程中,可能沒有停止業(yè)務(wù),有部分業(yè)務(wù)還在寫入。備份完成后做數(shù)據(jù)恢復(fù)時,后寫入的業(yè)務(wù)數(shù)據(jù)就會丟失,造成數(shù)據(jù)不一致。
5.應(yīng)用邏輯問題
應(yīng)用程序的邏輯有問題,也可能會造成數(shù)據(jù)不一致。下面列舉兩個場景:
- 多個線程對同一條數(shù)據(jù)進(jìn)行操作,在應(yīng)用中沒有采用加鎖機(jī)制,導(dǎo)致出現(xiàn)數(shù)據(jù)不一致;
- 數(shù)據(jù)插入時因?yàn)檫壿媶栴}造成了主鍵沖突,插入失敗。