Author:
bugall
Wechat:bugallF
Email:769088641@qq.com
Github: https://github.com/bugallmysql
InnoDB是符合MVCC(Multi-Version Concurrency Control)規範的,通俗的講就是寫加鎖,讀不加鎖,讀寫不衝突(有些狀況下是不符合MVCC的,好比當isolation級別爲serializable時,git
是讀寫衝突的,這又跟autocommit參數的值有關,這裏不展開講)。有這麼一個前提mysql才能對併發有不錯的處理能力,可是不少時候咱們不但願多個線程同時修改同一個數據,咱們的作法就是設github
置isolation的級別,保證數據的一致性。咱們接下來就來探討下InnoDB中的同步機制。sql
好比咱們如今mysql-server上跟客戶端創建裏3個鏈接,某一時刻這個三個鏈接同時發來了一個請求,須要把名爲"bugall"的用戶的money增長10000,在這個時候你不但願別的鏈接再來修改數據庫
這個值,一般咱們會在"bugall"這個用戶的數據上加一把排它鎖,這樣的話全部的對"bugall"對應數據作修改的請求鏈接都會被序列化,對"bugall"作修改的時候其它修改的請求都會被阻塞。這個數據結構
阻塞過程是怎麼實現的呢?或是說這個同步機制是怎麼實現的?咱們接着往下看併發
內存模型決定裏CPU怎樣訪問內存,以及併發狀況下各CPU之間的影響。可是內存模型並不包括虛擬地址轉換,由於最終CPU訪問的內存的物理地址,內存模型主要關心的是CPU和內存之間數據和物理函數
地址的傳輸。不一樣硬件之間內存模型的差別在於硬件是根據怎樣的順序來執行load和store指令,改變執行順序或許有可能帶性能的提高,除此以外,內存模型還指定了多個處理器訪問同一內存地址的性能
行爲,最簡單的內存模型就是順序內存模型(sequential memory model),也稱爲strong ordering,在這個模型下,全部的load和store指令是根據程序運行順序執行的。atom
load %r1,A //將內存地址A中的值放入寄存器r1 load %r2,B //將內存地址B中的值放入寄存器r2 add %r3,%r1,%r2 //將寄存器r1與r2的值相加,並放入寄存器r3 store %r3,c //將寄存器r3中的值寫入到內存地址c中 在數序內存模型下,執行的順序都是按照程序運行的順序進行的,若還未將內存地址A中的值取到,則不能執行從內存地址B中取值的操做除了要求內存操做的順序與程序運行順序一致外,順序內存模型 還要求從CPU或者I/O設備中讀取或者寫入操做是原子的,即一旦開始了,這些操做就不能被其餘的內存操做中斷
雖然順序內存模型的執行順序是根據程序的運行順序,可是多個CPU對同一個內存地址的訪問順序確實不肯定的,而正是由於少裏訪問的肯定性從而致使競爭(race condition)條件的發生。爲了說
明這個問題,假設有一個全局的計數器counter,CPU操做每次將該值加1,同時要求該計數器須要很是精確的展現當前CPU的操做次數
load %r1,counter //將計數器counter的值讀取到寄存器r1 add %r1,1 //將寄存器r1中的值加1 store %r1,counter //並存放到計數器counter中
接下來有兩種CPU順序的執行累加操做
在該順序下,兩個CPU執行的時間交錯,沒有發生race condition, 所以最後獲得的值符合以前的預期,然而,還有一種可能性
能夠發現:若當兩個CPU同時進行load操做時,那麼最終將會產生錯誤的結果。由於每一個CPU在自增前讀到的數據都是0,那麼無論以後的操做順尋如何,獲得的結果永遠會是1,而正確的值應爲2。
在兩個或者多個CPU之間更新共享的數據結構指令序列會產生race condition,指令序列自己稱爲臨界區(critical section),操做的數據稱爲臨界資源(critical resource)。如上面代碼
中的三個指令序列可視爲臨界區,爲裏消除多個CPU併發訪問臨界區而致使的race condition,故須要限制同一個時刻只容許一個CPU執行臨界區,而這就是互斥(mutex exclusion)
爲了保證同一時刻只容許一個CPU執行臨界區,當前硬件都提供裏基於原子的read-modify-write操做。read-modify-write操做容許一個CPU讀取一個值,修改該值,並將修改完成的值寫回到內
存的三個操做做爲一個原子總線操做,其在CPU中是一個特別的指令,而且只有在須要同步的時候才使用.對於具體進行怎樣的modify操做每一個實現標準可能並不相同,但一般來講,目前的CPU都支持
test-and-set(TAS)指令,該指令從內存中讀取一個字節或者一個word(4個字節),而後和0進行比較,而且無條件的將其在內存中的值設置爲1,全部這些操做都是原子操做。一旦CPU在執行test-
and-set操做,其它任何CPU和I/O設備都不能使用總線,經過test-and-set指令,操做系統或者數據庫系統能夠構造更高級別的同步操做,如spin lock(自旋鎖),semephore(信號量)
在TAS的基礎上,能夠實現不少互斥的數據結構,而spin lock則是使用最爲普遍,也最爲簡單的一種互斥結構。spin lock使用來對short-term critical section進行互斥的數據結構,特別需
要注意的是,spin lock用來互斥的critical section的代碼應該比較少,即通常能夠較快執行完代碼,並釋放spin lock,由於spin lock會使其它須要獲取鎖的線程進入忙等,佔用CPU。爲在
多CPU環境中利用test_and_set指令實現進程互斥,硬件須要提供進一步的支持,以保證test_and_set指令執行的原子性. 這種支持目前多以"鎖總線"(bus locking)的形式提供的,因爲
test_and_set指令對內存的兩次操做都須要通過總線,在執行test_and_set指令以前鎖住總線,在執行test_and_set指令後鎖定總線,便可保證test_and_set指令執行的原子性.
實現最基本的TAS指令就是使用swap-atomic操做。該操做僅僅將寄存器中的值與內存的值進行交換,經過swap-atomic 能夠用來構造test-and-set操做,首先將寄存器中的值設置爲1,而後執行
atomic swap,最後和寄存器中的值進行比較。
int test_and_set(volatile int* addr){ int old_value; old_value = swap_atomic(addr,1); if(old_value==0){ return 0; } return 1; }
變量addr的類型是init,代表須要操做的單位是word.volatile修飾詞告訴編譯器從內存中讀取addr的值,由於即便本操做沒有修改addr的值,其它CPU也可能修改該值,那麼在這中狀況下,可能會
致使執行test_and_set獲得錯誤的結果。swap_atomic函數執行swap-atomic的硬件指令,並返回交換前內存中addr的值,test-and-set操做是由兩個獨立的操做組合爲一個指令,第一個階段
是將addr中的值設置爲1,第二階段比較以前取得的結果。初始化時,將其值設置爲0
typedef init lock_t void initlock(volatile lock_t *lock_status){ *lock_status = 0; }
使用前面的TAS方法講一個spin lock對象上鎖
void lock(volatile lock_t* lock_statue){ while(test_and_set(lock_status)==1); }
當lock_status的值爲0時,test_and_set返回的結果爲0,上鎖成功。若該對象已經被使用,那麼須要在while中循環(spin),知道對象釋放鎖。這也是spin lock名字的由來