它是由C和C++編寫的,對於數據庫鏈接還得明確如下兩個概念:html
InnoDB表數據的組織方式爲主鍵聚簇索引,二級索引(非聚簇索引)中採用的是(索引鍵值, 主鍵鍵值)的組合來惟一肯定一條記錄。InnoDBl默認爲每一個索引行添加了4個隱藏的字段:java
當咱們對記錄作了變動操做時就會產生Undo記錄,Undo記錄默認被記錄到系統表空間(ibdata)中,但從5.6開始,也可使用獨立的Undo表空間。Undo記錄中存儲的是老版本數據,當一箇舊的事務須要讀取數據時,爲了能讀取到老版本的數據,須要順着undo鏈找到知足其可見性的記錄。當版本鏈很長時,一般能夠認爲這是個比較耗時的操做。
爲了保證事務併發操做時,在寫各自的undo log時不產生衝突,InnoDB採用回滾段(Rollback Segment,簡稱Rseg)的方式來維護undo log的併發寫入。回滾段其實是一種 Undo 文件組織方式,每一個回滾段又有多個undo log slot。
結合行結構,若是有兩個事務前後修改同一行數據,以下圖所示:node
例如:數據庫中A=1,如今須要update A=3
那麼整個步驟以下:mysql
這個就是undo log工做流程,也就是在數據庫斷電或者crash的時候,在進行恢復的時候,把undo log裏面的數據寫回到數據庫,這樣就讓數據回滾了。這樣實現了事務的原子性,同時保證了數據的一致性。可是,這樣每一個操做都會進行磁盤IO的寫入,頻繁的磁盤IO對性能是很大的下降。算法
引入redo log實現持久性,這個時候就在考慮若是隻須要將日誌寫入磁盤,將數據緩存在內存中,必定時間後再進行更新。
例如:數據庫中A=1,B=2,須要update A=3,B=4sql
整個過程當中,數據修改都是在內存中,極大提高磁盤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
Mysql的原子性由Undo Log機制保證,若是事務失敗就用Undo Log進行回滾緩存
隔離性是爲了應對多個事務併發進行時的問題,能夠分爲如下幾個隔離級別:
Mysql在實現READ COMMITTED和REPEATABLE READ時用到了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來判斷是否對這行數據可見:
在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鎖的分類:
Innodb實現了行級鎖,可是這是基於索引的,若是一條SQL語句用不到索引的話就會使用表鎖。有了意向鎖,若是有人嘗試對全表進行修改就不須要判斷表中的每一行數據是否被加鎖了,只須要經過等待意向鎖被釋放就能夠了。意向鎖其實不會阻塞全表掃描以外的任何請求,它們的主要目的是爲了表示是否有人請求鎖定表中的某一行數據。有關Latch的介紹參加關於MySQL latch爭用深刻分析與判斷
InnoDB存儲引擎有3種行鎖的算法,其分別是:
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.
上面說的併發事務隔離都是隻有一個事務在修改數據,另一個事務在讀取數據,可是若是兩個事務都須要修改數據,且是先查詢再更新就會出現丟失更新問題:
要麼採用悲觀加鎖的方式: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;
數據庫的一致性分爲外部一致性和內部一致性,外部一致性由應用開發人員保證,如將A到B帳戶轉帳的多個操做放到一個事務中進行,內部一致性是由原子性和隔離性共同保證的。
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)