前段時間,公司內部遇到了一個問題,就是咱們建立的同一批任務,別分配給了不一樣的實例去執行,致使線上的結果出現問題。面試
另外一個組的leader說沒有開啓事務,設置下事務就能夠。sql
數據庫一出現一致性問題,就說開啓事務,我就有點疑惑,數據庫的事務究竟是怎麼保證一致性的。數據庫
在看下面的內容,咱們能夠先思考幾個問題。數組
數據庫的隔離級別都有什麼?數據庫的MVVC視圖是怎麼實現的?session
數據庫的隔離級別是爲了解決什麼問題的?併發
看完上面三個問題,本身能回答上來幾個呢?不急。咱們繼續往下看cors
數據庫的事務咱們簡單來講就是用來保證數據的正確性,它只有兩個操做:事務要麼成功,要麼失敗並進行回滾。spa
爲何這麼作呢?這是由於通常咱們進行事務操做,都會進行一組操做。好比你常見的金融轉帳。日誌
在這個轉帳事務裏面包含2個操做:code
如今思考下,若是咱們沒有添加事務,那麼會出現什麼樣的狀況呢?
因此了咱們只能在這種操做中使用事務,來保證執行的成功與失敗,失敗了要進行回滾,保證扣錢的操做也不執行。
事務具備四個特性,這四個特性簡稱爲ACID
今天主要說的事務就是隔離。看看事務是怎麼保證數據之間的隔離
不一樣的事務隔離級別對應的不一樣的數據執行效率,隔離的越嚴格,那麼執行的效率就約低下,下面的四個隔離級別是原來越嚴格。
Mysql中默認的事務隔離級別是可重複讀,使用下面這個命令進行查看當前的事務級別,
show variables like 'transaction_isolation'; # 下面的語句進行修改事務的級別。 SET session TRANSACTION ISOLATION LEVEL Serializable;(參數能夠爲:Read uncommitted,Read committed,Repeatable,Serializable)
在程序中,咱們不少時候都是默認的自動提交,也就是一個sql操做就是一條事務,但有時候須要的是多個SQL進行組合,咱們就要顯式的開啓事務。
顯示開啓的語句是用 begin或者 start transaction.一樣在事務結束的時候使用commit進行提交,失敗使用rollbakc進行回滾。
固然若是不想讓SQL進行自動提交,咱們就將自動提交進行關閉set autocommit=0
,這樣事務就不會自動提交,須要咱們手動的執行commit.
關閉自動提交事務後,就須要咱們來本身提交事務,這時候每一個語句執行都是這樣的。
begin sql語句 commit
若是咱們在程序編寫中,原本一個sql解決的操做,結果忘記進行事務的提交,到下下下一個SQL才進行commit,這樣就會出現長事務。
而長事務每每會形成大量的堵塞與鎖超時的現象,事務中若是有讀寫(讀讀不衝突,讀寫衝突,寫寫衝突)操做,那麼會將數據進行鎖住,其餘事務也要進行等待。
因此在程序中,咱們應該儘可能避免使用大事務,一樣也避免咱們寫程序的時候出現偶然的大事務(失誤😁)。
解決辦法是咱們將自動提交打開,當須要使用事務的時候纔會顯示的開啓事務。
在MySQL中想定位一個長事務問題仍是很方便的。
首先咱們先找到正在執行的長事務是什麼。
select t.*,to_seconds(now())-to_seconds(t.trx_started) idle_time from INFORMATION_SCHEMA.INNODB_TRX t G
該語句會展現出事務的執行的開始時間,咱們能夠很簡單的算出,當前事務執行了多久,其中上面的idle_time就是執行的事務時間
假設咱們如今設定的超過30s執行的事務都是長事務,可使用下面語句進行過濾30s以上的長事務。
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>30
經過information_schema.innodb_trx 咱們就能定位到長事務。
從而決定長事務是否要被殺死,仍是繼續等待。若是出現死鎖的狀況,處理方式也是相似,查找到死鎖的語句,而後進行殺死某個語句的執行(有點暴力)。
注意:本次數據隔離是創建在可重複讀的場景下
在可重複讀的場景下,咱們瞭解每次啓動事務的時候,會在當前啓動一個視圖,而這個視圖是整個數據庫的視圖快照。
嘿嘿,是否是想數據庫那麼大,爲啥咱們沒有感受到建立快照時間的消耗呢?
這是由於數據庫建立的視圖快照利用了全部數據都有多個版本的特性,來實現快速建立視圖快照的能力。
那數據多個版本是怎麼回事呢?
先別急,咱們準備下數據。
如今建立一個表,而且插入三條數據。
create table scores ( id int not null primary key, score float null ); INSERT INTO scores (id, score) VALUES (1, 3.5); INSERT INTO scores (id, score) VALUES (2, 3.65); INSERT INTO scores (id, Score) VALUES (3, 4);
在開始使用前咱們要了解兩個小知識點。begin/start transaction 與 start transaction with consistent snapshot。
begin select source from scores; //視圖是在這裏開始建立 而不是在begin那裏建立 commit
瞭解上面兩個建立事務的區別後,咱們來看下視圖是怎麼建立出來多個數據版本的. 如下SQL在兩個窗口打開。
事務A | 事務B | 結果
start transaction with consistent snapshot | 開啓事務,並建立視圖 |
--| start transaction with consistent snapshot |開啓事務,並建立視圖
select score from scors where id =2 | -- | 事務A中的值爲3.65
-- | update scores set scores = 10 where id =2 | 事務B修改成10
--| select score from scores where id =2 | 事務B顯示爲10
select score from scores where id =2 | --| 事務A顯示爲3.65
select score from scores where id =2 for update | --| 會被鎖住,等待事務B釋放鎖(間隙鎖)
-- |commit | 提交事務B
select score from scores where id =2 for update | --| 這個語句能夠看到變成了10(利用了當前讀)
select score from scores where id =2 | --| 不加 for update 那麼結果仍是3.65
commit|---|---| 提交A的結果
上述流程就是兩個不一樣的請求過來,對數據庫同一個表的不一樣操做。
當事務A執行start transaction with consistent snapshot以後,A的視圖就開始被建立了,這時候是看不到事務B對其中的修改,就算事務Bcommit以後,只要事務A不結束,它看到的結果就是它啓動時刻的值。
這就與不重複提交,執行過程當中看到的結果與啓動的時候看到的結果是一致的這句話對應上了。
前面說了,快照是事務的啓動的時候是基於整個數據庫的,而整個數據庫是很大,那MYSQL是怎麼讓咱們無感並快速建立一個快照呢。
快照多版本你能夠認爲是由如下兩部分構成。
當一行記錄存在多個數據版本的時候,那麼就有多個row trx_id 。舉個例子
版本 | 值|事務ID| 對應的語句操做
v1 | score =3 | 89| --
v2 | score =5| 90| update scores set score = 5 where id =3; select score from scores where id =3;|
v3 | score = 6 | 91 | update scores set score = 6 where id =3;|
v1->v2->v3 這裏面涉及了三個版本的迭代。中間是經過undo log 日誌來保存更新的記錄的。
注意啓動快照以後,可重複讀隔離狀況下,獲取到v1的值,不是說MYSQL直接存儲的該值,而是利用如今這條記錄的最新版本與undo log日誌計算出來的,好比經過v3 ->v2—>v1 計算出v1中score值。
上面簡單說了下版本的計算規則,可是在MYSQL中,版本並非那麼簡單的計算的,咱們如今來看下到底怎麼計算的,
這個兩點咱們在注意一下:
舉個簡單的例子。
a. 注意一點:獲取事務ID與建立數組不是一個原子操做,因此存在事務id爲8,而後又存在當前MYSQL中存在活躍事務ID爲9 10的事務。
b. 事務ID低於低水位那麼對於當前事務確定是可見的,事務ID高於高水位的事務ID值,則對當前事務不可見.
c. 事務ID 位於低水位與高水位之間分爲兩種狀況。
總的上面來講就是你在我建立的時候事務結果已經提交,那麼是可見的,以後提交那麼就是不可見的。
上面簡單說了下老版本視圖中的數據是經過最新的版本與undo log 計算出來的,那到底怎麼就算的呢?
事務A | 事務B | 結果
start transaction with consistent snapshot 事務 id 89| 開啓事務,並建立視圖 |
--| start transaction with consistent snapshot 事務id 92 |開啓事務,並建立視圖
select score from scors where id =2 | -- | 事務A中的值爲3.65
-- | update scores set scores = 10 where id =2 | 事務B修改成10
--| select score from scores where id =2 | 事務B顯示爲10
select score from scores where id =2 | --| 事務A顯示爲3.65
commit|---|---| 提交A的結果
仍是看這個事務操做。
下面是數據變更的流程。
select score from scors where id =2
將其看到的結果認爲是v1版本數據好比其如今row trx_id(注意:row trx_id是數據行被更新後事務id纔會賦值給row trx id上)是86,而且保存好。你看通過簡單的幾步,咱們就拿到了想要讀取的事務數據,因此不論事務A何時查詢,它拿到的結果都是跟它讀取的數據是一致的。
你看有了MVCC(多版本併發控制)計算別的事務更改了值也不會影響到當前事務讀取結果的過程。
咱們常常說不要寫一個長事務,經過上面的讀取流程能夠看到,長事務存在時間長的話,數據版本就會有不少,那麼undo log日誌就須要保存很久,這些回滾日誌會佔用大量的內存存儲空間。
當沒有事務須要讀取該日誌與版本數據的時候,這個日誌才能夠刪除,從而釋放內存空間。
事務A | 事務B | 結果
start transaction with consistent snapshot 事務 id 89| 開啓事務,並建立視圖 |
--| start transaction with consistent snapshot 事務id 92 |開啓事務,並建立視圖
select score from scors where id =2 | -- | 事務A中的值爲3.65
-- | update scores set scores = 10 where id =2 | 事務B修改成10
--| select score from scores where id =2 | 事務B顯示爲10
select score from scores where id =2 | --| 事務A顯示爲3.65
select score from scores where id =2 for update | --| 會被鎖住,等待事務B釋放鎖(間隙鎖)
-- |commit | 提交事務B
select score from scores where id =2 for update | --| 這個語句能夠看到變成了10(利用了當前讀)
select score from scores where id =2 | --| 不加 for update 那麼結果仍是3.65
commit|---|---| 提交A的結果
上面說了讀取的過程,其實在事務中,咱們還有更新流程,更新流程比較簡單,更新過程咱們須要保證數據的一致性,不能說別人修改了,咱們還看不到,那樣就會形成數據的不一致。
爲了保證看到最新的數據,會對更新行的操做加鎖(行鎖),加鎖以後,其餘事務對行進行更新操做,必須等待其餘事務commit以後才能獲取到最新的值,這個過程被稱爲當前讀。
想要讀取過程當中得到最新的值可使用 上面的語句select score from scores where id =2 for update ,就能夠看到當前最新值。
本小節主要梳理了事務的隔離級別,事務的MVCC多版本併發控制實現原理。
事務在面試中是比較多的一個點,這樣的題目能夠多種變換,咱們剛開始題目提到的三個問題已經能夠解答了。
你來嘗試回答下?
下期會說下數據庫中的幻讀,幻讀也是面試中常常遇到的問題哦。