MySQL

MySQL服務器邏輯架構圖

clipboard.png

它是由C和C++編寫的,對於數據庫鏈接還得明確如下兩個概念:html

  • ODBC(Open Database Connectivity):是用於鏈接數據庫管理系統的接口協議,與操做系統、編程語言、數據庫系統無關,只須要使用該數據庫提供的OBJC driver。
  • JDBC(Java DatabBase Connectivity):是由JAVA語言提供的數據庫接口API,能夠用於鏈接實現了JDBC driver的數據庫,好比:MySQL提供的mysql-connector-java.jar

ACID特性

  • Atomicity - In a transaction involving two or more discrete pieces of information, either all of the pieces are committed or none are.
  • Consistency - A transaction either creates a new and valid state of data, or, if any failure occurs, returns all data to its state before the transaction was started.
  • Isolation - A transaction in process and not yet committed must remain isolated from any other transaction. Each transaction behaves as if it’s the only one that is executing in the system.
  • Durability - Once the system commits a transaction, that transaction must persist in the system, even if the system crashes or loses power

InnoDB存儲引擎的行結構

clipboard.png
InnoDB表數據的組織方式爲主鍵聚簇索引,二級索引(非聚簇索引)中採用的是(索引鍵值, 主鍵鍵值)的組合來惟一肯定一條記錄。InnoDBl默認爲每一個索引行添加了4個隱藏的字段:java

  • DB_ROW_ID:InnoDB引擎中一個表只能有一個主鍵,用於聚簇索引,若是表沒有定義主鍵會選擇第一個非Null的惟一索引做爲主鍵,若是尚未,生成一個隱藏的DB_ROW_ID做爲主鍵構造聚簇索引。
  • DB_TRX_ID:最近更改該行數據的事務ID。
  • DB_ROLL_PTR:undo log的指針,用於記錄以前歷史數據在undo log中的位置。
  • DELETE BIT:索引刪除標誌,若是DB刪除了一條數據,是優先通知索引將該標誌位設置爲1,而後經過(purge)清除線程去異步刪除真實的數據。

Undo Log

當咱們對記錄作了變動操做時就會產生Undo記錄,Undo記錄默認被記錄到系統表空間(ibdata)中,但從5.6開始,也可使用獨立的Undo表空間。Undo記錄中存儲的是老版本數據,當一箇舊的事務須要讀取數據時,爲了能讀取到老版本的數據,須要順着undo鏈找到知足其可見性的記錄。當版本鏈很長時,一般能夠認爲這是個比較耗時的操做。
爲了保證事務併發操做時,在寫各自的undo log時不產生衝突,InnoDB採用回滾段(Rollback Segment,簡稱Rseg)的方式來維護undo log的併發寫入。回滾段其實是一種 Undo 文件組織方式,每一個回滾段又有多個undo log slot。
結合行結構,若是有兩個事務前後修改同一行數據,以下圖所示:node

clipboard.png

Redo Log

例如:數據庫中A=1,如今須要update A=3
那麼整個步驟以下:mysql

  1. 事務開始
  2. 記錄A=1到undo log
  3. 修改A=3
  4. 將undo log寫入磁盤
  5. 將A=3數據寫入磁盤
  6. 事務提交

這個就是undo log工做流程,也就是在數據庫斷電或者crash的時候,在進行恢復的時候,把undo log裏面的數據寫回到數據庫,這樣就讓數據回滾了。這樣實現了事務的原子性,同時保證了數據的一致性。可是,這樣每一個操做都會進行磁盤IO的寫入,頻繁的磁盤IO對性能是很大的下降。算法

引入redo log實現持久性,這個時候就在考慮若是隻須要將日誌寫入磁盤,將數據緩存在內存中,必定時間後再進行更新。
例如:數據庫中A=1,B=2,須要update A=3,B=4sql

  1. 事務開始
  2. 記錄A=1到undo log
  3. 修改A=3
  4. 記錄A=3到redo log
  5. 記錄B=2到undo log
  6. 修改B=4
  7. 記錄B=4到redo log
  8. 將redo log順序寫入磁盤
  9. 事務提交

整個過程當中,數據修改都是在內存中,極大提高磁盤IO速度,並且將redo log提早寫入磁盤,這就是Write-Ahead-Logging(WAL)。
若是整個事務執行的過程系統崩潰或者斷電了,在系統重啓的時候,恢復機制會將redo log中已提交的事務重作,保證事務的持久性;而undo log中未提交的事務進行回滾,保證事務的原子性。數據庫

Redo Log由兩部分組成,一是內存中的redo log buffer,是易失的。二是redo log file,是持久的。參數innodb_flush_log_at_trx_commit用來控制redo log buffer刷新到redo log file的策略:
0:表示事務提交時不進行寫redo log file的操做,這個操做僅在master thread中完成(master thread每隔1秒進行一次fsync操做)。
1:默認值,表示每次事務提交時進行寫redo log file的操做。
2:表示事務提交時將redo log寫入文件,不過僅寫入文件系統的緩存中,不進行fsync操做編程

與Mysql中Binlog進行比較:segmentfault

  • Binary Log是在Mysql server層實現,Redo Log是在Innodb存儲引擎中實現
  • Binary Log是記錄的SQL邏輯語句,Redo Log是以512字節的block記錄的物理格式上的日誌,是數據庫中每一個頁的修改
  • Binary Log只在提交的時候一次性寫入,一次提交對應一次記錄。redo log文件中同一個事務可能屢次記錄,不一樣事務之間的不一樣版本的記錄會穿插寫入到redo log文件中,例如可能redo log的記錄方式以下:T1-1,T1-2,T2-1,T2-2,T2,T1-3,T1

Atomicity

Mysql的原子性由Undo Log機制保證,若是事務失敗就用Undo Log進行回滾緩存

Isolation

隔離性是爲了應對多個事務併發進行時的問題,能夠分爲如下幾個隔離級別:

  • READ UNCOMMITTED(讀未提交):在此級別裏,事務的修改,即便沒有提交,對其餘事務也都是可見的。事務能夠讀取未提交的數據,也就是會產生髒讀,還有後面提到的不可重複讀和幻讀問題,在實際應用中通常不多使用。
  • READ COMMITTED(讀已提交):大多數數據庫系統的默認隔離級別都是它,可是MySQL不是。它可以避免髒讀問題,可是在一個事務裏對同一條數據的屢次查詢可能會獲得不一樣的結果,也就是會產生不可重複讀問題。
  • REPEATABLE READ(重複讀):該隔離級別是MySQL默認的隔離級別,看名字就知道它可以防止不可重複讀問題,可是在一個事務裏對一段數據的屢次讀取可能會出現多行或少行的狀況,也就是會有幻讀的問題。
  • SERIALIZABLE(串行化):該隔離級別是級別最高的,它經過鎖來強制事務串行執行,避免了前面說的全部問題。在高併發下,可能致使大量的超時和鎖爭用問題。實際應用中也不多用到這個隔離級別,由於RR級別解決了全部問題。

Mysql在實現READ COMMITTED和REPEATABLE READ時用到了MVCC:

MVCC

Multiversion concurrency control,在每次事務開始時,Mysql會根據當前事務鏈表(當一個事務開始的時候,會將當前數據庫中正在活躍的全部事務--執行begin可是尚未commit的事務--保存到一個叫trx_sys的事務鏈表中,事務鏈表中保存的都是未提交的事務,當事務提交以後會從其中刪除)建立一個ReadView:

// Friend declaration
class MVCC;
/** Read view lists the trx ids of those transactions for which a consistent
read should not see the modifications to the database. */
...
class ReadView {
    ...
    private:
        // Prevent copying
        ids_t(const ids_t&);
        ids_t& operator=(const ids_t&);
    private:
        /** Memory for the array */
        value_type* m_ptr;
        /** Number of active elements in the array */
        ulint       m_size;
        /** Size of m_ptr in elements */
        ulint       m_reserved;
        friend class ReadView;
    };
public:
    ReadView();
    ~ReadView();
    /** Check whether transaction id is valid.
    @param[in]  id      transaction id to check
    @param[in]  name        table name */
    static void check_trx_id_sanity(
        trx_id_t        id,
        const table_name_t& name);
    /** Check whether the changes by id are visible.
    @param[in]  id  transaction id to check against the view
    @param[in]  name    table name
    @return whether the view sees the modifications of id. */
    bool changes_visible(
        trx_id_t        id,
        const table_name_t& name) const
        MY_ATTRIBUTE((warn_unused_result))
    {
        ut_ad(id > 0);
        if (id < m_up_limit_id || id == m_creator_trx_id) {
            return(true);
        }
        check_trx_id_sanity(id, name);
        if (id >= m_low_limit_id) {
            return(false);
        } else if (m_ids.empty()) {
            return(true);
        }
        const ids_t::value_type*    p = m_ids.data();
        return(!std::binary_search(p, p + m_ids.size(), id));
    }
    
private:
    // Disable copying
    ReadView(const ReadView&);
    ReadView& operator=(const ReadView&);
private:
    // 事務鏈表中最大的id
    /** The read should not see any transaction with trx id >= this
    value. In other words, this is the "high water mark". */
    trx_id_t    m_low_limit_id;
    // 事務鏈表中最小的id
    /** The read should see all trx ids which are strictly
    smaller (<) than this value.  In other words, this is the
    low water mark". */
    // 
    trx_id_t    m_up_limit_id;
    /** trx id of creating transaction, set to TRX_ID_MAX for free
    views. */
    trx_id_t    m_creator_trx_id;
    /** Set of RW transactions that was active when this snapshot
    was taken */
    ids_t       m_ids;
    /** The view does not need to see the undo logs for transactions
    whose transaction number is strictly smaller (<) than this value:
    they can be removed in purge if not needed by other views */
    trx_id_t    m_low_limit_no;
    /** AC-NL-RO transaction view that has been "closed". */
    bool        m_closed;
    typedef UT_LIST_NODE_T(ReadView) node_t;
    /** List of read views in trx_sys */
    byte        pad1[64 - sizeof(node_t)];
    node_t      m_view_list;
};

經過該ReadView,新的事務能夠根據查詢到的每行數據最近的DB_TRX_ID來判斷是否對這行數據可見:

  • 當檢索到的數據的事務ID小於事務鏈表中的最小值(數據行的DB_TRX_ID < m_up_limit_id)表示這個數據在當前事務開啓前就已經被其餘事務修改過了,因此是可見的。
  • 當檢索到的數據的事務ID表示的是當前事務本身修改的數據(數據行的DB_TRX_ID = m_creator_trx_id) 時,數據可見。
  • 當檢索到的數據的事務ID大於事務鏈表中的最大值(數據行的DB_TRX_ID >= m_low_limit_id) 表示這個數據在當前事務開啓後到下一次查詢之間又被其餘的事務修改過,那麼就是不可見的。
  • 若是事務鏈表爲空,那麼也是可見的,也就是當前事務開始的時候,沒有其餘任意一個事務在執行。
  • 當檢索到的數據的事務ID在事務鏈表中的最小值和最大值之間,從m_low_limit_id到m_up_limit_id進行遍歷,取出DB_ROLL_PTR指針所指向的回滾段的事務ID,把它賦值給trx_id_current,而後從步驟1從新開始判斷,這樣總能最後找到一個可用的記錄。

在READ COMMITTED事務隔離級別下,每次語句執行都關閉ReadView,而後從新建立一份ReadView。
在REPEATABLE READ事務隔離級別下,事務開始後第一個讀操做建立ReadView,一直到事務結束關閉。因此每次看到的數據是同樣的
Mysql在REPEATABLE READ事務隔離級別下默認使用MVCC,又叫一致性非鎖定讀,可是會有幻讀的問題:A事務開始以後,B事務插入了一行,原本A事務看不到,可是若是A事務執行了update,而且update的篩選條件裏又包含了B插入的那一行,那麼這一行數據最近修改的事務就會變成A,這樣下次A事務再進行前文所述的可見性判斷時就會包含這一行數據。又或者A事務開始後查詢表發現沒有要插入的數據,而後B事務插入了一條一樣的數據,等A事務插入數據時會出現Duplicate entry(若是有unique index),這樣就須要顯示的使用一致性鎖定讀:

SELECT … FOR UPDATE (排它鎖)
SELECT … LOCK IN SHARE MODE (共享鎖)

下面先來介紹一下Mysql鎖的分類:

clipboard.png

  • 共享鎖(讀鎖):容許事務讀一行數據。
  • 排他鎖(寫鎖):容許事務刪除或更新一行數據。
  • 意向共享鎖(IS):事務想要在得到表中某些記錄的共享鎖,須要在表上先加意向共享鎖。
  • 意向排他鎖(IX):事務想要在得到表中某些記錄的互斥鎖,須要在表上先加意向互斥鎖。

Innodb實現了行級鎖,可是這是基於索引的,若是一條SQL語句用不到索引的話就會使用表鎖。有了意向鎖,若是有人嘗試對全表進行修改就不須要判斷表中的每一行數據是否被加鎖了,只須要經過等待意向鎖被釋放就能夠了。意向鎖其實不會阻塞全表掃描以外的任何請求,它們的主要目的是爲了表示是否有人請求鎖定表中的某一行數據。有關Latch的介紹參加關於MySQL latch爭用深刻分析與判斷
InnoDB存儲引擎有3種行鎖的算法,其分別是:

  • Record Lock:單個行記錄上的鎖。
  • Gap Lock:間隙鎖,鎖定一個範圍,但不包含記錄自己。
  • Next-Key Lock:Gap Lock+Record Lock,鎖定一個範圍,而且鎖定記錄自己。

Mysql在REPEATABLE READ事務隔離級別下默認使用Next-Key Lock(在查詢的列是惟一索引(包含主鍵索引)的狀況下,Next-key Lock會降級爲Record Lock),因此在使用一致性鎖定讀時,A事務執行select後,會阻止B事務的插入。

For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE),UPDATE, and DELETE statements, locking depends on whether the statement uses a unique index with a unique search condition, or a range-type search condition. For a unique index with a unique search condition, InnoDB locks only the index record found, not the gap before it. For other search conditions, InnoDB locks the index range scanned, using gap locks or next-key (gap plus index-record) locks to block insertions by other sessions into the gaps covered by the range.

上面說的併發事務隔離都是隻有一個事務在修改數據,另一個事務在讀取數據,可是若是兩個事務都須要修改數據,且是先查詢再更新就會出現丟失更新問題:

clipboard.png

要麼採用悲觀加鎖的方式:select quantity from items where id = 1 for update阻止事務B的修改
要麼採用樂觀鎖(CAS:compare and swap):select quantity,version from items where id = 1;a=q-1,v2=v1+1; update items set quantity=a, version=v2 where id = 1 and version = v1;(使用遞增version避免ABA問題)
不過最好仍是在一條語句中完成原子操做:update items set quantity=quantity-1 where id = 1 and quantity - 1 >= 0;

Consistency

數據庫的一致性分爲外部一致性和內部一致性,外部一致性由應用開發人員保證,如將A到B帳戶轉帳的多個操做放到一個事務中進行,內部一致性是由原子性和隔離性共同保證的。

Durability

Mysql的持久性由Redo Log機制保證,數據在commit以前會持久化到Redo Log中,若是系統crash,能夠用它來恢復

談談MySQL InnoDB存儲引擎事務的ACID特性
談談MySQL的鎖
詳細分析MySQL事務日誌(redo log和undo log)
Mysql中的MVCC
Mysql如何保證事務性?
超全面的MySQL語句加鎖分析

後面會分析如何利用MyBatis來管理MySQL的鏈接:
MyBatis鏈接管理(1)
MyBatis鏈接管理(2)

相關文章
相關標籤/搜索