<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # 為什么需要事務呢? 在[數據庫(二),數據庫起源](http://www.cnblogs.com/dy2903/p/8365376.html)里面我們提到了事務。 數據庫除了對查詢等操作進行了抽象,另外一個重要的功能就是**事務**了。為什么需要事務呢?因為我們在操作數據的時候,可能遇到多個線程同時操作數據的問題,也可能遇到突然數據庫故障了的問題,這些都可能造成數據的**不一致**。所以事務要保證的就是**一致性**。 保證一致性的第一重意思是**鎖**,這是為了應對多個**連接**同時連到數據庫的時候。因為我們可能為每個連接分配一個線程,而這些線程有可能同時操作同一塊數據,這樣將會發生**不一致**。所以我們只好在寫的時候加上**鎖**,也就是強行保證只有一個線程可以訪問到這塊數據。 另外我們還會遇到數據庫崩潰的問題,所以我們要求一個事務一定是**原子**的,也就是 **要么全部發生, 要么根本不發生。**比如Bob給Smith轉100塊,要么Bob有100塊,要么Smith有100塊,不存在中間狀態。 對于單機事務而言,需要保證 - 原子性 - 一致性 - 隔離性 - 持久性 也就是所謂的ACID,下面我們依次介紹他們是怎么實現的。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-b5f95a3d7ad7bb23.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 原子性 ## Undo日志 所謂**原子性**指的是要么同時成功,要么同時失敗。比如Bob賬戶里面有100塊,而Smith賬戶里面有0元,現在我們希望Bob轉100塊給Smith。 所謂**原子性**就是要么Bob成功轉給了Smith100塊,此時Bob有0元、Smith有100塊。要么失敗了,Bob仍然有100塊,Smith為0元。**不會存在Bob把錢轉出去了,而Smith卻沒有拿到錢的情況。** 現在我們來想想要實現這個事務,應該怎么做 - 鎖定Bob賬戶 - 鎖定Smith賬戶 - 查看Bob是否有100塊錢,如果有,則從賬號里面減少100塊 - 給Smith賬戶里面增加100塊 - 依次解鎖Bob和Smith ![image.png](http://upload-images.jianshu.io/upload_images/1323506-a8fb745ab639343a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 但是執行事務不會永遠是一帆風順的,可能出現**意外**,比如Bob或者Smith賬戶不存在怎么辦?沒關系,我們可以**回滾**到上一個狀態。 但是數據庫不可能把每個狀態都記錄下來,這就需要我們在轉賬之前把**之前的狀態**記錄下來。 比如我們看剛剛那個轉賬操作的**中間狀態** 1. Bob:100,Smith:0 2. Bob:0,Smith :0 (此時正在轉賬) 3. Bob : 0 , Smith :100(轉賬成功) 我們可以在插入兩個undo段,他們記錄在日志中。 1. Bob:100,Smith:0 2. Bob:0,Smith :0 (此時正在轉賬) - 上一個狀態為:Bob:100,Smith:0 3. Bob : 0 , Smith :100(轉賬成功) - 上一個狀態為: Bob:0,Smith :0 這樣如果要回滾,只需要回溯日志即可實現。這 **另外還有一種可能就是事務并沒有進行完,系統就崩潰了怎么辦?**那系統重啟之后就得做**恢復**操作啊。那怎么恢復了,同樣也是通過**日志**。我們可以在進行真正的操作之前,需要把要做的事寫下來, 我們會在事務開始之前寫下: > Bob原有100元,Smith原有0元 如果事務執行到一半就斷電,那么重啟之后我們就可以按照**日志來恢復**,然后仍然是** Bob有100元,Smith有0元**。即使恢復100次,仍然是這個結果,這就叫**冪等性**,所以恢復過程中也斷電了,我們仍然可以按照日志來進行恢復。 現在還有個一問題沒有解決,那就是怎么知道**一個事務沒有完成呢?** 同樣我們可以通過記錄日志的方式來完成。比如我們在記錄的時候,不但把余額記上,還把**事務開始了和結束**這兩個動作打上標記。 比如 > [開始事務T1] [事務T1:Bob原有100] [事務T2:Smith原有0] [提交事務T1] 這樣,如果在日志中看到了**提交事務T1**或者**回滾事務T1**,我們就知道這個事務已經結束了。如果只看到**開始事務T1**,那就得恢復。比如下面這個就得恢復 > [開始事務T1] [事務T1:Bob原有100] [事務T2:Smith原有0] 而且,在**恢復之后**,需要在日志文件中加上一行**回滾事務T1**,這樣下次恢復就不用再考慮T1這個事務呢,因為現在早已經回到上一個狀態去了呢。 ## Undo日志寫入文件的時機 上面的討論其實我們都故意忽略了一個問題,那就是Undo日志也需要加載到內存中才能讀寫,但是如果日志還沒寫好就斷電了怎么辦? **其實我們只要掌握好把日志寫入文件的時機就OK了。** 最容易想到的就是在一開始就把日志寫入文件,就好比寫作文前把草稿打好,后面只管按著草稿謄抄一遍就可以了。 然而,現實是,一開始的時候,我們都不知道程序要操作哪個字段,怎么記錄日志呢,當然也不能寫入文件呢。所以肯定是一邊在內存中操作Undo日志,一邊找時機寫入磁盤中。 比如上面的轉賬操作,我們其實可以這樣來修改和寫日志。 |操作|數據緩沖區|日志緩沖區| |:--|:--|:--| |開始事務T1||[開始事務T1]| |Bob = Bob - 100 |Bob新余額為0|[事務T1,Bob原有余額為100]| |把日志寫入文件||注意,日志寫入文件后,緩沖區會清空| |把Bob余額寫入文件||| |Smith = Smith + 100||[事務T1,Smith原有余額0]| |把日志寫入文件||注意,日志寫入文件后,緩沖區會清空| |把Smith余額寫入文件|Smith新余額為100|| |提交事務T1||[提交事務T1]| |把日志寫入文件||注意,日志寫入文件后,緩沖區會清空| 總結一下就是, - 當余額發生改變的時候,記錄之前的余額 - 在余額要寫入硬盤之前,需要把日志先寫入文件,然后日志緩沖區會清空。 - 提交事務的日志一定是在所有余額都寫入硬盤之后才寫入 也就是說事務過程中,余額發生改變,在余額正式寫入了硬盤以后,相當于木已成舟,所以我們也需要把日志寫入硬盤。 當所有余額都穩穩當當的落到磁盤上了,我們自然也應該把日志落到磁盤上 那么我們可以攻防演練一下。 如果Bob的余額寫到了硬盤,但是Smith還沒修改。此時日志中落盤的只有Bob原有的余額也就是: > [開始事務T1] [事務T1:Bob原有100] 恢復的時候,發現事務沒有結束,所以還會把Bob的余額給恢復了。 同理,如果Bob和Smith的余額都落盤了,但是沒有提交事務,此時日志是 > [開始事務T1] [事務T1:Bob原有100] [事務T2:Smith原有0] 依然可以恢復兩個賬戶的余額。 即使兩個賬戶的最新余額都落盤了,也提交了事務,但是只要在日志寫入磁盤之前崩潰,則Undo日志還是 > [開始事務T1] [事務T1:Bob原有100] [事務T2:Smith原有0] 同樣會把余額恢復成原樣。 ## 原子性做不到的地方 現在可算是把原子性說完了,但是只有原子性是不夠的,為什么呢?因為它無法保證多個線程訪問數據時的一致性。 比如在第2步的時候,另一個事務把把smith賬戶加到了300塊錢, 1. Bob:100,Smith:0 2. Bob:0,Smith :0 ------------->Bob:0,Smith :300(另一個事務干的) - 上一個狀態為:Bob:100,Smith:0 3. Bob : 0 , Smith :100(轉賬成功) - 上一個狀態為: Bob:0,Smith :0 如果有另一個事務在進行到步驟2的時候把smith賬戶加到了300塊錢,此時如果回滾,會把smith改為0,那加上的300塊就丟失了。 那么我們還需要一致性。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-b978099f8a682b23.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 一致性 上一章我們提到了如果在事務中間,有另一個事務突然插手對數據進行修改,則如果出現回退,將會出現數據不一致的問題。 那怎么解決這個問題呢?如果我們一個事務對數據操作完了以后,另一個事務再進入,這樣就不會發生爭搶和數據不一致了。所以核心就在于**加鎖**。 比如 - Lock Bob , Smith 1. Bob:100,Smith:0 2. Bob:0,Smith :0 ------------->Bob:0,Smith :300(另一個事務干的) - 上一個狀態為:Bob:100,Smith:0 3. Bob : 0 , Smith :100(轉賬成功) - 上一個狀態為: Bob:0,Smith :0 - unLock Bob and Smith 在事務的開始和結束分別進行加鎖和解鎖。這樣,其他的事務并不可知事務內部的事情。只有在**事務單元內部完全成功了以后才對外可見。** 到現在我們“仿佛”已經解決了**并發、一致**兩個大問題了,但是新的問題也來了,**加鎖**以后,其他的事務無法對數據進行訪問,那么**系統的并發度**是上不來的,這就是下面的**隔離性**要解決的問題。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-6a04c2ae8f334402.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 隔離性 所謂**隔離性**,其實是以性能作為理由,在破壞**一致性**。何以見得?因為如果要保證強一致性,最好的方法就是不管讀寫,統統排隊進行,這樣一定不會出現數據不一致的情況。 然而此時就做不到高的并發,性能也就上不去。所以我們只要做一些妥協,比如只加寫鎖,不加讀鎖。 我們首先需要看看,兩個事務單元對同一個數據,有哪幾種并發模式,然后定義不同的隔離級別,看每種隔離級別可以實現哪些并發模式。 ## 4種可能 同樣我們以一個例子來說明 現在 T1 :Bob要給Smith 100塊,然后T2 : Smith要給Joe 100塊。 這就是兩個事務,如下圖所示,為了保證一致性,Smith賬戶會被兩個事務單元鎖定。也就是兩個事務有共享數據,Bob在給Smith轉錢的時候,另一個事務無法對Smith賬戶進行操作了,并發就上不去。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-ba3e3510adf6547b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 此時兩個事務單元T1,T2之間只有**讀寫并發、寫讀并發、讀讀并發、寫寫并發**4種可能。 - 寫寫并行 什么時候能寫寫并行,只有當兩個事務的數據完全沒有重疊的情況下,比如如下的情況。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-69363eff0a45f0af.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 因為沒有共享數據,所以完全可以**寫寫**并行,也就是寫寫都不加鎖。 - 讀讀并行 也就是讀操作不加鎖,這樣讀與讀可以并行操作,因為讀不會修改數據,所以讀讀可以放心的并行,而不用擔心一致性的問題。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-c38ead35d39e43ff.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 讀寫并行 也就是讀的時候,可以并發寫。我們知道,寫操作會修改數據,但是寫是加鎖的,所以我們無法讀到寫未提交的結果。所以雖然兩次讀到的數據是不一樣的,**不可重復讀**,但是每次讀到的數據都是正確的,不存在不一致。 - 寫讀并行 也就是寫的時候,還可以并發讀。因為數據是在不斷改變的,很可能讀到中間的狀態,如果系統在此時崩潰了,重啟的時候會恢復到修改前的值,此時自然會出現錯亂。 那么我們是否無法實現寫讀并行了嗎?并不是,可以通過Copy on Write。具體怎么做呢?每次寫操作之前都把數據復制一份到log里面,在log里面進行修改。 其實就是把原來的數據復制一份,然后修改。這樣讀操作作用的就是原來的數據,而寫作用的是備份的數據,互不干擾。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-e29595fcad7b3cf3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 這種方法又叫(MVCC,Multi Version content control,多版本內容控制)。那么**多版本**是什么意思。 我們知道數據被復制出去了一份以后,可能會被修改多次,那么**下一次讀應該讀修改后哪個版本的數據呢?**這個時候,我們可以在日志里面加上**版本號**。比如說,現在寫入的數據版本號是10,如果要讀取版本號為5的數據,則可以往前一直找,直到找到對應的位置。 所以如果讀發生在寫操作之后,**讀的版本號一定要大于寫的版本號。**這樣就可以保證讀到想要的數據。 ## 四種隔離級別 上面講了兩個事務單元針對一塊數據其實有4種并發的可能,接下來我們繼續討論**隔離級別**。不同的隔離級別可以實現**讀寫并行、寫讀并行、讀讀并行、寫寫并行**的一種或者幾種。 - 串行化: 就是讀的時候不允許寫,寫的時候不允許讀,這樣可以保證數據強一致,但是性能最低。SQLite默認采用這種方式。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-ceaf5e4338b61993.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 可重復讀,也就是只能實現**讀讀并行**,**讀寫、寫讀、寫寫**等不能實現。 所以在兩個都是讀的時候,不加讀鎖,其他情況均需要加鎖。 MySQL默認是這種方式。 - 讀已提交(Read Committed): 此時當數據被加上讀鎖了以后,一個寫進來,寫鎖替換掉讀鎖,也就是**可以將讀鎖升級為寫鎖。** 那么如果事務T1讀取了數據,然后事務T2把這個數據修改了,因為事務T2也是加鎖的,所以它會提交,那么事務T1**再讀取**這個數據時,原來的數據已經發生變化了。這就是不可重復讀。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-af13ecfee7e609a0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 此時可以做到**讀寫并行、讀讀并行**,做不了**寫讀并行** Oracle , PostgreSQL, SQL Server都是使用的這種模式。 - 讀未提交:顧名思義,就是可以讀到未提交的內容 最低級別的隔離,此時只加上**寫和讀是不加鎖的**。因為數據是在不斷改變的,很可能讀到中間的狀態,如果系統在此時崩潰了,重啟的時候會恢復到修改前的值,此時自然會出現錯亂。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-32395bd8ff86e0f3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 要解決**寫讀并行**的問題,可以使用上面說過的Copy on write,這種方法最大的好處在于可以保證寫讀并行,同時隔離級別還很高 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-fdf07c19981a127c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 持久性 現在我們來討論最后ACID的**持久性**,也就是**只要事務提交了,不管是崩潰還是出錯,數據一定要寫到磁盤上** 那么數據什么情況下會丟失呢? - 首先是磁盤損壞。所以我們可以使用RAID冗余磁盤陣列來保證可靠性。詳見[【大話存儲】學習筆記(4,5章),RAID](http://www.cnblogs.com/dy2903/p/8417836.html) - 還有就是內存如果掉電,里面的數據就必然丟失,持久性得不到保證。但是如果每一次提交操作完成以后,都將內存中的數據同步到硬盤上,則會造成頻繁寫硬盤,性能將下降。所以**持久性和延遲無法兼得** 我們只要進行折中,比如只要把數據提交到**內存**,就立刻返回成功,然后將一段時間的請求**打包**送到磁盤上。這樣就避免了每次提交都寫磁盤 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-087966ebf10ac5c5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 參考 - 慕課網 - 如果有人問你數據庫的原理,叫他看這篇文章[如果有人問你數據庫的原理,叫他看這篇文章](http://blog.csdn.net/xmric/article/details/54972998)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看