MySQL技術內幕讀書筆記(八)——事務

事務的實現

​ 事務隔離性由鎖來實現。原子性、一致性、持久性經過數據庫的redo log和undo log來完成。redo log稱爲重作日誌,用來保證事務的原子性和持久性。undo log用來保證事務的一致性。java

​ redo和undo做用都是一種恢復操做。mysql

  • redo:
    • 恢復提交事務修改的頁操做,
    • 物理日誌,記錄的是頁的物理修改操做。
    • 保證事務的持久性
    • 順序寫
  • undo:
    • 回滾行記錄到某個特定版本
    • 邏輯日誌,根據每行進行記錄
    • 幫助事務回滾和MVCC功能
    • 隨機讀寫

redo

  1. 基本概念算法

    重作日誌用來實現事務的持久性,由兩部分組成sql

    • 內存中的重作日誌緩衝redo log buffer
    • 重作日誌文件redo log file

    ​ InnoDB是事務的存儲引擎,其經過Force Log at Commit機制來實現事務的持久性,即當事務提交時,必須將該事務的全部日誌寫入到重作日誌文件進行持久化,該事務的COMMIT操做纔算完成。這裏的重作日誌文件包括redo 和 undo log。
    用戶也能夠手工設置COMMIT日誌刷新策略,經過參數innodb_flush_log_at_trx_commit來控制。數據庫

    • 0:事務提交時不進行寫入重作日誌操做,這個操做僅在master thread中完成。
    • 1:事務提交時必須調用一次fsync操做,默認值
    • 2:事務提交時將重作日誌寫入重作日誌文件,但只寫入緩存,不進行fsync操做
  2. log block緩存

    ​ 在INNODB中,重作日誌都是以512字節進行存儲的,由於大小和磁盤扇區大小同樣,所以重作日誌的寫入能夠保證原子性。服務器

  3. log group併發

    ​ 稱爲重作日誌組,其中有多個重作日誌文件,可是INNODB進行這個功能,實際上也只有一個log group。分佈式

    ​ 是一個邏輯上的概念,由多個重作日誌文件組成,每一個log group中的日誌文件大小是相同的。大小最大爲512GB。ide

    ​ log buffer也是使用塊進行存儲的管理,一樣爲512字節。從緩存刷新到磁盤的具體規則爲:

    • 事務提交時
    • 當log buffer已經有一半的內存空間被使用時
    • log checkpoint時。
  4. 重作日誌格式

    ​ INNODB的重作日誌格式是基於頁的。雖然有這邊冉的重作日誌格式,可是他們有着通用的頭部格式。

    redo_log_type space page_no redo log body
    • redo_log_type:重作日誌類型
    • space:表空間ID
    • page_no:頁偏移量
  5. LSN

    Log Sequence Number縮寫,表示日誌序列號。在INNODB存儲引擎中,LSN佔用8字節,而且單調遞增,LSN表示含義有:

  • 重作日誌寫入的總量

    若當前重作日誌的LSN爲1000,一個事務寫入了100字節的重作日誌,那麼LSN變爲1100。

  • checkpoint的位置

  • 頁的版本

    ​ 每一個頁的頭部,有一個值FILE_PAGE_LSN,記錄該頁的LSN。表示該頁最後刷新時LSN的大小。用於判斷頁是否須要進行恢復操做。例如P1的LSN爲10000,數據啓動時,檢測到重作日誌文件中的LSN爲13000,而且該事務已經提交,那麼數據庫就要對P1進行恢復操做。

  1. 恢復

    ​ 無論運行時是否正常關閉,都會嚐盡進行恢復操做。由於記錄的都是物理日誌,因此恢復速度比邏輯日誌快不少。

undo

  1. 基本概念

    ​ 進行回滾操做時使用。undo存放在數據庫內部的一個特殊段中,成爲undo段。位於共享表空間中。undo是邏輯日誌,是將數據庫邏輯地恢復到原來的樣子。這是由於在多用戶併發系統中,可能有成百上千個併發事務,若是直接物理回滾頁記錄,會影響其餘正在進行的事務。

    ​ 因此undo的回滾操做是邏輯操做,對於insert,進行對應的delete;對於delete,執行對象的Insert,對於update,進行反向的update。

    ​ undo的另一個做用是MVCC,實現了非鎖定讀取。

    ​ undo log也會產生redo log。這是由於undo log也須要持久性的保護。

  2. undo存儲管理

    ​ 採用段的方式進行管理,首先有rollback segment,每一個回滾段中記錄了1024個undo log segment,而在每一個undo log segment段中進行undo頁的申請。

    ​ 對rollback segment作進一步的設置:

    • innodb_undo_directory

      ​ 用於設置文件所在的路徑。便可以設置爲獨立表空間。該參數默認值爲「.」,表示當前INNODB存儲引擎的目錄。

    • innodb_undo_logs

      ​ 用來設置rollback segment個數,默認值爲128。

    • innodb_undo_tablespaces

      ​ 設置構成rollback segment文件的數量,這樣rollback segment能夠較爲平均地分佈在多個文件中。

    SHOW VARIABLES LIKE 'innodb_undo%';
    
    SHOW VARIABLES LIKE 'datadir';

    ​ 事務在undo log segment分配頁並寫入undo log的這個過程一樣須要寫入重作日誌。

    • undo log放入列表中,以供以後的purge操做
    • 判斷undo log所在的頁是否能夠重用,若能夠分配給下個事務使用。

    事務提交以後並不能立刻刪除undo log以及undo log所在的頁,這是由於可能有MVVC使用。因此將undo log放在一個鏈表中,是否能夠最終刪除由purge線程判斷。

  3. undo log格式

    有兩種:

    • insert undo log

      ​ 在insert操做中產生的undo log。因爲事務隔離性的要求,因此該undo log能夠在事務提交後直接刪除。不須要進行Purge操做。

    • update undo log

      ​ 對delete和update操做產生的undo log。改undo log可能須要提供MVCC機制,所以不能在事務提交時就進行刪除。

  4. 查看undo 信息

    # 查看rollback segment
    DESC INNODB_TRX_ROLLBACK_SEGMENT;
    
    # 查看rollback segment所在的頁
    SELECT segment_id, space, page_no
    from INNODB_TRX_ROLLBACK_SEGMENT;
    
    # 記錄事務對應的undo log
    SELECT * FROM information_schema.INNODB_TRX_UNDO\G;

purge

​ delete和update操做可能並不直接刪除原有的數據,undo log只將對應記錄的delete flag設置爲1,沒有直接刪除記錄,真正刪除的操做被延時到purge操做中完成。

​ 由於MVVC的關係,purge要等待該行記錄已經不被任何其餘事務引用,才進行清理操做。

​ INNODB存儲引擎中含有一個history list,按照事務提交的順序將undo log進行組織。在執行purge的過程當中,INNODB存儲引擎首先從history list中找到第一個須要被清理的記錄,清理以後,會繼續在該記錄所在的undo log頁中繼續尋找能夠被清除的記錄,而後再繼續從history list中去查找,重複執行。

​ 參數innodb_purge_batch_size用來設置每次Purge操做須要清理的undo page數量。

​ 參數innodb_max_purge_lag用來控制history list的長度,若長度大於該參數時,其會「延緩」DML的操做。默認爲0,表示不作任何限制。當大於0時,就會延緩DML的操做,延緩的算法爲:

delay = ((length(history_list) - innodb_max_purge_lag) * 10) - 5

​ 單位是毫秒。delay的對象是行,而不是DML操做。

​ 參數innodb_max_purge_lag_delay,用來控制delay的最大毫秒數。當上述計算的delay數值大於該參數值,限制住。

group commit

​ 若事務爲非只讀事務,則每次提交都要進行依次fsync操做,以此保證重作日誌都已經寫入磁盤。爲了提升磁盤fsync效率,提供了group commit功能,依次fsync能夠刷新確保多個事務日誌被寫入文件。

​ 可是在開啓了二進制日誌以後,爲保證存儲引擎層中的事務和二進制日誌的一致性,兩者之間使用了兩階段事務,步驟以下:

  1. 當事務提交時InnoDB存儲引擎進行prepare操做
  2. MYSQL數據上層寫入二進制日誌
  3. INNODB存儲引擎層將日誌寫入重作日誌文件
    1. 修改內存中事務對應的信息,而且將日誌寫入重作日誌緩衝
    2. 調用fsync將確保日誌都從重作日誌緩衝寫入磁盤。

​ 一旦寫入了二進制日誌,就確保了事務的提交,即便執行步驟3發生了宕機。此外,每一個步驟都須要進行依次fsync才能保證上下兩層數據一致。步驟二由sync_binlog控制,步驟三由innodb_flush_log_at_trx_commit控制。

​ 由於備份以及恢復的須要,須要保證MYSQL數據庫上層二進制日誌的寫入順序和INNODB層的事務提交順序一致,MYSQL數據庫內部使用了prepare_commit_mutex這個鎖。可是啓用這個鎖以後,步驟3中的步驟1不能夠再其餘事務執行步驟二時執行。從而致使group commit失效。

​ 爲了解決這個問題,5.6版本以後使用了BLGC技術。

​ 在MYSQL數據庫上層進行提交時先按照順序將其放入一個隊列中,隊列中的第一個事務成爲leader,其餘事務成爲follower,leader控制follower的行爲。BLGC的步驟分爲如下三個階段:

  • Flush階段:將每一個事務的二進制日誌寫入內存。
  • Sync階段:將內存中的二進制日誌刷新到磁盤,若隊列中有多個事務,那麼僅一次fsync操做就完成了二進制日誌的寫入,這就是BLGC
  • Commit階段,leader根據順序調用存儲引擎層事務的提交,InnoDB存儲引擎本就支持Group commit,因此就解決了gourp commit失效的問題。

​ 當一組事務在進行commit階段時,其餘新事務能夠進行Flush階段,從而使group commit不斷生效。group commit的效果由隊列中書屋的數量決定,若每次隊列中僅有一個事務,那麼可能效果和以前差很少,甚至會更差。但當提交的事務越多時,group commit的效果越明顯,數據庫性能的提高也就越大。

​ 參數binlog_max_flush_queue_time用來控制Flush階段中等待的時間,即便以前的一組事務完成提交,當前一組的事務也不立刻進入Sync階段,而是至少須要等待一段時間。這個好處是group commit數量更多,該參數默認值爲0,推薦這是依舊爲0。

事務控制語句

  • START TRANSACTION | BEGIN:顯式地開啓一個事務
  • COMMIT:提交事務
  • ROLLBACK:回滾會結束用戶的事務,並撤銷正在進行的全部未提交的修改
  • SAVEPOINT identifier:容許在事務中建立一個保存點,一個事務中能夠有多個SAVEPOINT
  • RELEASE SAVEPOINT identifier:刪除一個事務的保存點,當沒有一個保存點執行這句語句時,會拋出一個異常。
  • ROLLBACK TO [SAVEPOINT] identifier:與SAVEPOINT命令一塊兒使用,能夠把事務回滾到標記點,而不會管在此標記點以前的任何工做。
  • SET TRANSACTION:用來設置事務的隔離級別
    • READ UNCOMMITTED
    • READ COMMITTED
    • REPEATABLE READ
    • SERIALIZABLE

COMMIT WORK用來控制事務結束後的行爲是CHAIN仍是RELEASE。若是是CHAIN方式,那麼事務就變成了鏈事務。

​ 經過參數completion_type來控制

  • 該參數默認爲0,表示沒有任何操做,這時和COMMIT是徹底等價的
  • 設置爲1,等同於COMMIT AND CHAIN,表示立刻自動開啓一個相同隔離級別的事務。
  • 設置爲2 ,等同於COMMIT AND RELEASE,表示事務提交以後會自動斷開與服務器的鏈接。

對於事務操做的統計

  • QPS Question Per Second:每秒請求數

  • TPS Transaction Per Second:每秒事務處理能力

    計算TPS的方法是(com_commit + com_rollback) / time。計算前提是全部事務必須是顯式提交的。

事務的隔離級別

SQL標準定義的四個隔離級別爲:

  • READ UNCOMMITTED:瀏覽訪問
  • READ COMMITED:遊標穩定
  • REPEATABLE READ:是2.9999°的隔離,沒有幻讀的保護。
  • SERIALIZABLE:稱爲隔離,或者3°的隔離。

​ MYSQL引擎默認支持的隔離級別是REPEATABLE READ,可是已經經過Next-Key Lock鎖的算法,避免了幻讀產生。達到SQL標準的SERIALIZABLE隔離界別。

​ 隔離級別越低,事務請求的鎖越少或者保持鎖的時間就越短。默認的事務隔離級別是READ COMMITTED

​ 修改MYSQL啓動時設置的默認隔離級別,須要修改MYSQL的配置文件

[mysqld]
transaction-isolation = READ-COMMITTED

​ 如何查看事務隔離級別

# 當前會話的事務隔離級別
SELECT @@tx_isolation\G;

# 全局的事務隔離級別
SELECT @@global.tx_isolation\G;

​ 主從複製若是發生了不一致,可能發生的緣由有兩點:

  • READ COMMITTED事務隔離級別下,事務沒有使用gap lock進行鎖定,所以用戶在會話B中能夠在小於等於5的範圍內插入一條記錄。
  • STATEMENT格式記錄的是master上產生的SQL語句,所以在master服務器上執行的順序爲先刪後插,可是STATEMENT格式記錄的確實先插後刪。邏輯順序上產生了不一致。

分佈式事務

MYSQL數據庫分佈式事務

​ INNODB存儲引擎提供了對XA事務的支持,並經過XA事務來支持分佈式事務的實現。分佈式事務指的是容許多個獨立的事務資源參與到一個全局的事務中。全局事務要求在其中的全部參與的事務要麼都提交,要麼都回滾。

​ INNODB存儲引擎的事務隔離級別必須設置爲SERIALIZABLE

​ XA事務容許不一樣數據庫以前的分佈式事務。分佈式事務可能在銀行的轉帳系統中比較常見。

​ XA事務由一個或多個資源管理器、一個事務管理器以及一個應用程序組成。

  • 資源管理器:提供訪問事務資源的方法。一般一個數據庫就是一個資源管理器
  • 事務管理器:協調參與全局事務中的各個事務。須要和參與全局事務的全部資源管理器進行通訊。
  • 應用程序:定義事務的邊界,指定全局事務中的操做。

​ 分佈式事務採用二段式提交的方式。

  • 在第一階段,全部參與全局事務的節點都開始準備,告訴事務管理器他們準備好提交了。
  • 在第二階段,事務管理器告訴資源管理器執行ROLLBACK仍是COMMIT。若是任何一個節點顯示不能提交,則全部的節點都被告知須要回滾。

​ MYSQL數據庫XA事務的SQL語法以下:

XA {START|BEGIN} xid {JOIN|RESUME}

XA END xid[SUSPEND [FOR MIGRATE]]

XA PREPARE xid

XA COMMIT Xid [ONE PHASE]

XA ROLLBACK xid

XA RECOVER

​ 可是通常來講,單個節點上運行分佈式事務沒有實際意義,通常和變成語言來完成分佈式事務的操做。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class MyXid implements Xid {
    public int formatId;
    public byte gtrid[];
    public byte bqual[];
}

public class xa_demo {
    public static MysqlXADataSource GetDataSource(String conString, String user, String passwd) {
        try {
            MysqlXADataSource ds = new MysqlXADataSource();
            ds.setUrl(connString);
            ds.setUser(user);
            ds.setPassword(passwd);
            return ds;
        }
        catch(Exception e) {
            System.out.println(e.toString());
            return null;
        }
    }
}

public static void main() {
    String connString1 = "jdbc:mysql://192.168.24.43:3306/bank_shanghai";
    String connString2 = "jdbc:mysql://192.168.24.44:3306/bank_beijing";
    
    try {
        MysqlXDataSource ds1 = GetDataSource(connString1, "mxr", "12345");
        MysqlXDataSource ds2 = GetDataSource(connString2, "cxw", "12345");
        
        XAConnection xaConn1 = ds1.getXAConnect();
        XAResource xaRes1 = xaConn1.getXAResource();
        Connection conn1 = xaConn1.getConnection();
        Statement stmt1 = conn1.createStatement();
        
        XAConnection xaConn2 = ds2.getXAConnect();
        XAResource xaRes2 = xaConn2.getXAResource();
        Connection conn2 = xaConn2.getConnection();
        Statement stmt2 = conn2.createStatement();
        
        Xid xid1 = new MyXid(100, new byte[]{0x01}, new byte[]{0x02});
        Xid xid2 = new MyXid(100, new byte[]{0x11}, new byte[]{0x12});
        
        try {
            xaRes1.start(xid1, XAResource.TMNOFLAGS);
            stmt1.execute("UPDATE account SET money = money - 10000 where user = 'mxr'");
            xaRes1.end(xid1, XAResource.TMSUCCESS);
        
            xaRes2.start(xid2, XAResource.TMNOFLAGS);
            stmt2.execute("UPDATE account SET money = money + 10000 where user = 'cxw'");
            xaRes2.end(xid2, XAResource.TMSUCCESS);
        
            int ret2 = xaRes2.prepare(xid2);
            int ret1 = xaRes2.prepare(xid1);
        
            if( ret1 = XAResource.XA_OK && ret2 = XAResource.XA_OK) {
                xaRes1.commit(xid1, false);
                xaRes2.commit(xid2, false);
            }
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }
    catch(Exception e) {
        System.out.println(e.toString());
    }
}

​ 經過參數innodb_support_xa能夠查看是否啓用了XA事務的支持(默認爲ON);

內部XA事務

​ 在MYSQL內,存儲引擎與插件之間,存儲引擎之間也存在一種分佈式事務,成爲內部XA事務。

​ 最多見的是binlogINNODB存儲引擎之間內部XA事務。binlog和存儲引擎的重作日誌必須同時寫入,保證原子性,不然會致使主備數據不一致。

很差的事務習慣

在循環中提交

CREATE PROCEDURE load(count INT UNSIGNED) 
BEGIN
DECLARE s INT UNSIGNED DEFAULT 1;
DECLARE c CHAR(80) DEFAULT REPEAT('a', 80);
WHILE S <= count DO
INSERT INTO t1 SELECT NULL, c;
SET S = S + 1;
END WHILE;
END;

​ 每次的insert都會發生自動提交,若是用戶插入10000條數據,在5000條是發生了錯誤,那這5000已存在的數據如何處理?

​ 若是每次都提交,每次都須要寫重作日誌,影響效率。

​ 因此建議使用同一個事務

CREATE PROCEDURE load(count INT UNSIGNED) 
BEGIN
DECLARE s INT UNSIGNED DEFAULT 1;
DECLARE c CHAR(80) DEFAULT REPEAT('a', 80);
START TRANSACTION;
WHILE S <= count DO
INSERT INTO t1 SELECT NULL, c;
SET S = S + 1;
END WHILE;
COMMIT;
END;

使用自動提交

​ 編寫應用程序開發時,最好把事務的控制權限交給開發人員,在程序端進行事務的開始和結束。

使用自動回滾

​ 使用自動回滾以後,MYSQL在程序段是不會拋出異常信息的,不便於調試,因此通常存儲過程當中值存放邏輯操做便可。管理操做所有放在java中進行。

長事務

​ 長事務:執行時間較長的事務。

​ 這邊通常會將長事務拆解爲多個小事務進行操做。這樣子,若是發生是失敗了,能夠繼續在失敗的小事務上繼續進行重試,而不用所有重試。節省時間。

相關文章
相關標籤/搜索