分佈式事務,很差的事務習慣

分佈式事務

InnoDB存儲引擎支持XA事務,經過XA事務能夠來支持分佈式事務的實現。分佈式事務指的是容許多個獨立的事務資源(transactional resources)參與一個全局的事務中。事務資源一般是關係型數據庫系統,但也能夠是其餘類型的資源。全局事務要求在其中全部參與的事務要麼都提交、要麼都回滾,這對於事務原有的ACID要求又有了提升。另外,在使用分佈式事務時,InnoDB存儲引擎的事務隔離級別必須設置爲SERIALIABLE。java

XA事務容許不一樣數據庫之間的分佈式事務,如:一臺服務器是MySQL數據庫的,另外一臺是Oracle數據庫的,又可能還有一臺服務器是SQL Server數據庫的,只要參與全局事務中的每一個節點都支持XA事務。分佈式事務可能在銀行系統的轉帳中比較常見,如一個用戶須要從上海轉10 000元到北京的一個用戶上:python

#Bank@Shanghai:mysql

update account set money=money-10000 where user='David';程序員

#Bank@Beijingsql

Update account set money=money+10000 where user='Mariah';數據庫

這種狀況必定須要分佈式的事務,若是不能都提交或都回滾,在任何一個節點出現問題都會致使嚴重的結果:要麼是David的帳戶被扣款,可是Mariah沒收到;又或者是David的帳戶沒有扣款,可是Mariah仍是收到錢了。服務器

分佈式事務由一個或者多個資源管理器(Resource Managers)、一個事務管理器(Transaction Manager)以及一個應用程序(Application Program)組成。分佈式

資源管理器:提供訪問事務資源的方法。一般一個數據庫就是一個資源管理器。性能

事務管理器:協調參與全局事務中的各個事務。須要和參與全局事務中的全部資源管理器進行通訊。this

應用程序:定義事務的邊界,指定全局事務中的操做。

在MySQL的分佈式事務中,資源管理器就是MySQL數據庫,事務管理器爲鏈接到MySQL服務器的客戶端。下圖顯示了一個分佈式事務的模型:

分佈式事務使用兩段式提交(two-phase commit)的方式。在第一個階段,全部參與全局事務的節點都開始準備(PREPARE),告訴事務管理器它們準備好提交了。第二個階段,事務管理器告訴資源管理器執行ROLLBACK仍是COMMIT。若是任何一個節點顯示不能提交,則全部的節點都被告知須要回滾。

當前Java的JTA(Java Transaction API)能夠很好地支持MySQL的分佈式事務,須要使用分佈式事務應該認真參考其API。

下面的一個示例顯示瞭如何使用JTA來調用MySQL的分佈式事務,例子就是前面的銀行轉帳,以下所示。

import javax.transaction.xa.Xid;

class MyXid implements Xid {
    public int formatId;
    public byte gtrid[];
    public byte bqual[];

    public MyXid() {
    }

    public MyXid(int formatId, byte gtrid[], byte bqual[]) {
        this.formatId = formatId;
        this.gtrid = gtrid;
        this.bqual = bqual;
    }

    public int getFormatId() {
        return formatId;
    }

    public byte[] getBranchQualifier() {
        return bqual;
    }

    public byte[] getGlobalTransactionId() {
        return gtrid;
    }
}


import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource;

import javax.sql.XAConnection;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.sql.Connection;
import java.sql.Statement;

public class xa_demo {
    public static MysqlXADataSource GetDataSource(String connString,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[] args) {
        String connString1 =
                "jdbc:mysql://192.168.24.43:3306/bank_shanghai";
        String connString2 =
                "jdbc:mysql://192.168.24.166:3306/bank_beijing";
        try {
            MysqlXADataSource ds1 = GetDataSource(connString1, "peter", "12345");
            MysqlXADataSource ds2 = GetDataSource(connString2, "david", "12345");
            XAConnection xaConn1 = ds1.getXAConnection();
            XAResource xaRes1 = xaConn1.getXAResource();
            Connection conn1 = xaConn1.getConnection();
            Statement stmt1 = conn1.createStatement();
            XAConnection xaConn2 = ds2.getXAConnection();
            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 = 'david'");
                xaRes1.end(xid1, XAResource.TMSUCCESS);
                xaRes2.start(xid2, XAResource.TMNOFLAGS);
                stmt2.execute("update account set money = money + 10000 where user = 'mariah'");
                xaRes2.end(xid2, XAResource.TMSUCCESS);
                int ret2 = xaRes2.prepare(xid2);
                int ret1 = xaRes1.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):

show variables like 'innodb_support_xa'\G

***************************1.row***************************

Variable_name:innodb_support_xa

Value:ON

1 row in set(0.01 sec)

另外須要注意的是,對於XA事務的支持,是在MySQL體系結構的存儲引擎層。所以即便不參與外部的XA事務,MySQL內部不一樣存儲引擎層也會使用XA事務假設咱們用START TRANSACTION開啓了一個本地的事務,往NDB Cluster存儲引擎的表t1插入一條記錄,往InnoDB存儲引擎的表t2插入一條記錄,而後COMMIT。在MySQL內部,也是經過XA事務來協調的,這樣才能夠保證兩張表的原子性。

很差的事務習慣

在循環中提交

開發人員很是喜歡在循環中進行事務的提交,下面是他們可能常寫的一個存儲過程:

CREATE PROCEDURE load1(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;

    commit;

    set s=s+1;

  end while;

end;

其實,在這個例子中,是否加上commit並不關鍵,由於InnoDB存儲引擎默認爲自動提交,所以上面的存儲過程當中去掉commit,結果是徹底同樣的。這也是另外一種容易忽視的問題:

CREATE PROCEDURE load2(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;

不論上面哪一個存儲過程都存在一個問題:當發生錯誤時,數據庫會停留在一個未知的位置。如咱們要插入的是10 000條記錄,可是在插入5000條時,發生了錯誤,而這時前5000條記錄已經存放在數據庫中,那咱們應該怎麼處理呢?還有一個問題是性能問題,上面兩個存儲過程都不會比在下面的一個存儲過程快,由於它是放在一個事務裏:

CREATE PROCEDURE load3(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;

比較這3個存儲過程的執行時間:

call load1(10000);

Query OK,0 rows affected(1 min 3. 15 sec)

truncate table t1;

call load2(10000);

Query OK,1 row affected(1 min 1. 69 sec)

truncate table t1;

call load3(10000);

Query OK,0 rows affected(0. 63 sec)

顯然,第三種方法要快得多!這是由於,每一次提交都要寫一次重作日誌,所以存儲過程load1和load2實際寫了10 000次,而對於存儲過程load3來講,實際只寫了1次。能夠對第二個存儲過程load2的調用進行調整,一樣能夠達到存儲過程load3的性能,以下代碼所示。

begin;

call load2(10000);

commit;

大多數程序員會使用第一種或者第二種方法,有人可能不知道InnoDB存儲引擎自動提交的狀況,另外有些人可能持有如下兩種觀點:首先,在他們曾經使用過的數據庫中,對於事務的要求老是儘快地進行釋放,不能有長時間的事務;其次,他們可能擔憂存在Oracle數據庫中因爲沒有足夠UNDO產生的Snapshot Too Old的經典問題。MySQL InnoDB存儲引擎上述兩個問題都沒有,所以程序員不論從何種角度出發,都不該該在一個循環中反覆進行提交操做,不管是顯式的提交仍是隱式的提交。

使用自動提交

自動提交併非好習慣,由於這對於初級DBA容易犯錯,另外對於一些開發人員可能產生錯誤的理解。MySQL數據庫默認設置使用自動提交(autocommit)。可使用以下語句來改變當前自動提交的方式:

set autocommit=0;

也可使用START TRANSACTION、BEGIN來顯式地開啓一個事務。顯式開啓事務後,在默認設置下(即參數completion_type等於0),MySQL會自動執行SET AUTOCOMMIT=0的命令,並在COMMIT或者ROLLBACK結束一個事務後執行SET AUTOCOMMIT=1。

另外,在不一樣的語言API時,自動提交是不一樣的。MySQL C API默認的提交方式是自動提交的,而MySQL Python API則是自動執行SET AUTOCOMMIT=0,以禁用自動提交。所以在選用不一樣的語言來編寫數據庫應用程序前,應該對鏈接MySQL的API作好研究。

在編寫應用程序開發時,最好把事務的控制權限交給開發人員,即在程序端進行事務的開始和結束。同時,開發人員必須瞭解自動提交可能帶來的問題。

使用自動回滾

InnoDB存儲引擎支持經過定義一個HANDLER來進行自動事務的回滾操做,如一個存儲過程當中發生了錯誤,會自動對其進行回滾操做,所以不少開發人員喜歡在應用程序的存儲過程當中使用自動回滾操做,以下面的一個存儲過程:

create procedure sp_auto_rollback_demo()

begin

  declare exit handler for sqlexception rollback;

  start transaction;

    insert into b select 1;

    insert into b select 2;

    insert into b select 1;

    insert into b select 3;

  commit;

end;

存儲過程sp_auto_rollback_demo首先定義了一個exit類型的handler,當捕獲到錯誤時進行回滾。結構以下所示:

show create table b\G

所以插入第二個記錄1時會發生錯誤,可是由於啓用了自動回滾的操做,所以這個存儲過程的執行結果以下所示:

call sp_auto_rollback_demo;

select * from b;

Empty set(0.00 sec)

看起來運行沒有問題,很是正常。可是,執行sp_auto_rollback_demo這個存儲過程的結果究竟是正確的仍是錯誤的呢?

對於一樣的存儲過程sp_auto_rollback_demo,開發人員可能會進行這樣的處理:

create procedure sp_auto_rollback_demo()

begin

  declare exit handler for sqlexception begin rollback;select -1;end;

  start transaction;

    insert into b select 1;

    insert into b select 2;

    insert into b select 1;

    insert into b select 3;

  commit;

  select 1;

end;

當發生錯誤時,先回滾,而後返回-1,表示運行有錯誤。運行正常,返回值1。所以此次運行的結果就會變成:

call sp_auto_rollback_demo()\G

***************************1.row***************************

-1:-1

1 row in set(0.04 sec)

select * from b;

Empty set(0.00 sec)

看起來咱們能夠獲得運行是否準確的信息。但問題尚未最終解決,對於開發來講,重要的不只是知道發生了錯誤,而是發生了什麼樣的錯誤。所以自動回滾存在這樣的一個問題。

使用自動回滾大可能是之前使用Microsoft SQL Server數據庫。在Microsoft SQL Server數據庫中,可使用SET XABORT ON來回滾一個事務。可是Microsoft SQL Server數據庫不只會自動回滾當前的事務,而且還會拋出異常,開發人員能夠捕獲到這個異常。所以,Microsoft SQL Server數據庫和MySQL數據庫在這方面是有所不一樣的。

對於事務的BEGIN、COMMIT和ROLLBACK操做,應該交給程序端來完成,存儲過程只要完成一個邏輯的操做。

下面演示用Python語言編寫的程序調用一個存儲過程sp_rollback_demo,存儲過程sp_rollback_demo和以前的存儲過程sp_auto_rollback_demo在邏輯上完成的內容大體相同:

create procedure sp_rollback_demo()

begin

  insert into b select 1;

  insert into b select 2;

  insert into b select 1;

  insert into b select 3;

end;

和sp_auto_rollback_demo存儲過程不一樣的是,在sp_rollback_demo存儲過程當中去掉了對於事務的控制語句,將這些操做都交由程序來完成。接着來看test_demo.py的程序源代碼:

#!/usr/bin/env python

#encoding=utf-8

import MySQLdb

try:

conn=MySQLdb.connect(host="192.168.8.7",user="root",passwd="xx「,db="test")

cur=conn.cursor()

cur.execute("set autocommit=0")

cur.execute("call sp_rollback_demo")

cur.execute("commit")

except Exception,e:

cur.execute("rollback")

print e

觀察運行test_demo.py這個程序的結果:

python test_demo.py

starting rollback

(1062,"Duplicate entry'1'for key'PRIMARY'")

在程序中控制事務的好處是,咱們能夠得知發生錯誤的緣由。如上述這個例子中,咱們知道是由於發生了1062這個錯誤,錯誤的提示內容是Duplicate entry'1'for key'PRIMARY',即發生了主鍵重複的錯誤,而後能夠根據發生的緣由來調試咱們的程序。

相關文章
相關標籤/搜索