【深刻淺出事務】(2):Java的事務

Java事務分爲:JDBC事務、JTA事務,容器事務。java

JDBC事務

利用JDBC定義的java.sql.Connection接口中setAutoCommit(boolean)來達到自動提交和手動提交,只能侷限在同一個數據庫鏈接,不能跨庫。
切記:MySQL只有InnoDB纔有事務效果,其餘引擎設置了autoCommit無效。mysql

不用JDBC事務的DEMO

SQLsql

CREATE TABLE `test1` (
`id`  bigint(1) NOT NULL DEFAULT 0 ,
`name`  varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)

CREATE TABLE `test2` (
`id`  bigint(1) NOT NULL DEFAULT 0 ,
`name`  varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)

Java數據庫

package com.mousycoder.server.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class JdbcNoTransaction {

    public final static String DB_DRIVER_CLASS = "com.mysql.jdbc.Driver";
    public final static String DB_URL = "jdbc:mysql://xxx.mysql.rds.aliyuncs.com/xxx";
    public final static String DB_USERNAME = "xx";
    public final static String DB_PASSWORD = "xxx";
    public static final String INSERT_TEST1 = "INSERT INTO test1(id,name) VALUES(?,?)";
    public static final String INSERT_TEST2 = "INSERT INTO test2(id,name) VALUES(?,?)";

    public static void main(String[] args) {
        Connection con = null;
        try {
            con = getConnection();
            insertTest1(con);
            insertTest2(con);
        } catch (ClassNotFoundException|SQLException  e ) {
            e.printStackTrace();
        } finally {
            if (con != null) {
                try {
                    con.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    public static Connection getConnection() throws ClassNotFoundException,
            SQLException {
        Connection conn = null;
        Class.forName(DB_DRIVER_CLASS);
        conn = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);
        return conn;
    }

    public static void insertTest1(Connection conn) throws SQLException {
        PreparedStatement stmt;
        stmt = conn.prepareStatement("INSERT INTO test1(id,name) VALUES(?,?)");
        stmt.setInt(1, 1);
        stmt.setString(2, "1");
        stmt.executeUpdate();
        System.out.println("======insert into test1 successfully======");
        stmt.close();

    }

    public static void insertTest2(Connection conn) throws SQLException {
        PreparedStatement stmt;
        stmt = conn.prepareStatement("INSERT INTO test2(id,name) VALUES(?,?)");
        stmt.setInt(1, 1);
        stmt.setString(2, "11"); // 故意長度超出
        stmt.executeUpdate();
        System.out.println("======insert into test2 successfully========");
        stmt.close();

    }
}

控制檯輸出緩存

======insert into test1 successfully======
com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'name' at row 1
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3885)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3823)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2435)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2582)
    at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2530)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1907)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2141)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2077)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2062)
    at com.mousycoder.server.jdbc.JdbcNoTransaction.insertTest2(JdbcNoTransaction.java:61)
    at com.mousycoder.server.jdbc.JdbcNoTransaction.main(JdbcNoTransaction.java:22)

結果:
表test1有數據,表test2裏沒有數據,可是咱們想讓test1,test2 要麼同時插入數據,要麼都不插數據,這個時候就須要用到事務了。
順便提一下,若是讓test1拋異常,那麼直接異常處理,結果是test1沒數據,test2也沒數據,可是這裏並非用到了事務,而是異常處理改變了程序走向。服務器

利用JDBC事務的DEMO

Java微信

package com.mousycoder.server.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class JdbcTransaction {

    public final static String DB_DRIVER_CLASS = "com.mysql.jdbc.Driver";
    public final static String DB_URL = "jdbc:mysql://xx.mysql.rds.aliyuncs.com/xxx";
    public final static String DB_USERNAME = "xxx";
    public final static String DB_PASSWORD = "xxx";
    public static final String INSERT_TEST1 = "INSERT INTO test1(id,name) VALUES(?,?)";
    public static final String INSERT_TEST2 = "INSERT INTO test2(id,name) VALUES(?,?)";

    public static void main(String[] args) {
        Connection con = null;
        try {
            con = getConnection();
            con.setAutoCommit(false);
            insertTest1(con);
            insertTest2(con);
            con.commit();
            System.out.println("=======JDBC Transaction commit===========");
        } catch (ClassNotFoundException|SQLException  e ) {
            try {
                con.rollback();
                System.out
                        .println("=======JDBC Transaction rolled back successfully=======");
            } catch (SQLException e1) {
                System.out.println("=======SQL Exception in rollback"
                        + e1.getMessage()); // 回滾必要條件:1.同一個transaction
                                            // 2.connection未關,因此這裏要加異常處理
            }
            e.printStackTrace();
        } finally {
            if (con != null) {
                try {
                    con.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    public static Connection getConnection() throws ClassNotFoundException,
            SQLException {
        Connection conn = null;
        Class.forName(DB_DRIVER_CLASS);
        conn = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);
        return conn;
    }

    public static void insertTest1(Connection conn) throws SQLException {
        PreparedStatement stmt;
        stmt = conn.prepareStatement("INSERT INTO test1(id,name) VALUES(?,?)");
        stmt.setInt(1, 1);
        stmt.setString(2, "1");
        stmt.executeUpdate();
        System.out.println("======insert into test1 successfully======");
        stmt.close();

    }

    public static void insertTest2(Connection conn) throws SQLException {
        PreparedStatement stmt;
        stmt = conn.prepareStatement("INSERT INTO test2(id,name) VALUES(?,?)");
        stmt.setInt(1, 1);
        stmt.setString(2, "11"); // 故意長度超出
        stmt.executeUpdate();
        System.out.println("======insert into test2 successfully========");
        stmt.close();

    }
}

控制檯輸出session

======insert into test1 successfully======
=======JDBC Transaction rolled back successfully=======
com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'name' at row 1
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3885)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3823)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2435)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2582)
    at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2530)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1907)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2141)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2077)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2062)
    at com.mousycoder.server.jdbc.JdbcTransaction.insertTest2(JdbcTransaction.java:73)
    at com.mousycoder.server.jdbc.JdbcTransaction.main(JdbcTransaction.java:23)

結果:
表test1和表test2是沒有數據的,由於insertTest2方法拋出了異常,直接到異常處理,進行了手動的rollback(結束事務,回滾先前操做,釋放掉鎖),若是insertTest1()和insertTest2()都正常插入,則調用commit()方法(結束事務,提交先前改變,釋放掉鎖)oracle

源碼分析

com.mysql.jdbc.ConnectionImpl.rollback()ide

/**
     * The method rollback() drops all changes made since the previous
     * commit/rollback and releases any database locks currently held by the
     * Connection.
     *
     * @exception SQLException
     *                if a database access error occurs
     * @see commit
     */
    public void rollback() throws SQLException {
        synchronized (getConnectionMutex()) {
            checkClosed();
 
            try {
                if (this.connectionLifecycleInterceptors != null) {
                    IterateBlock<Extension> iter = new IterateBlock<Extension>(this.connectionLifecycleInterceptors.iterator()) {
 
                        @Override
                        void forEach(Extension each) throws SQLException {
                            if (!((ConnectionLifecycleInterceptor) each).rollback()) {
                                this.stopIterating = true;
                            }
                        }
                    };
 
                    iter.doForAll();
 
                    if (!iter.fullIteration()) {
                        return;
                    }
                }
                // no-op if _relaxAutoCommit == true
                if (this.autoCommit && !getRelaxAutoCommit()) {
                    throw SQLError.createSQLException("Can't call rollback when autocommit=true", SQLError.SQL_STATE_CONNECTION_NOT_OPEN,
                            getExceptionInterceptor());
                } else if (this.transactionsSupported) {
                    try {
                        rollbackNoChecks(); //真正執行rollback地方,發送rollback指令到數據庫
                    } catch (SQLException sqlEx) {
                        // We ignore non-transactional tables if told to do so
                        if (getIgnoreNonTxTables() && (sqlEx.getErrorCode() == SQLError.ER_WARNING_NOT_COMPLETE_ROLLBACK)) {
                            return;
                        }
                        throw sqlEx;
 
                    }
                }
            } catch (SQLException sqlException) {
                if (SQLError.SQL_STATE_COMMUNICATION_LINK_FAILURE.equals(sqlException.getSQLState())) {
                    throw SQLError.createSQLException("Communications link failure during rollback(). Transaction resolution unknown.",
                            SQLError.SQL_STATE_TRANSACTION_RESOLUTION_UNKNOWN, getExceptionInterceptor());
                }
 
                throw sqlException;
            } finally {
                this.needsPing = this.getReconnectAtTxEnd();
            }
        }
    }

解析:

  1. 只容許拿到鏈接互斥鎖的線程進入方法。

  2. 檢查鏈接是否被強制關閉,若被強制關閉,則拋出"No operations allowed after connection closed." 異常。

  3. 若是autoCommit且relaxAutoCommit爲false,則拋出"Can't call commit when autocommit=true"異常。

  4. 若是使用本地事務狀態以及MySql的版本號至少大於5.0.0而且事務在服務器上。

  5. 若是支持事務,則執行。

  6. 執行rollback。

com.mysql.jdbc.ConnectionImpl.commit()

public void commit() throws SQLException {
        synchronized (getConnectionMutex()) {
            checkClosed();
 
            try {
                if (this.connectionLifecycleInterceptors != null) {
                    IterateBlock<Extension> iter = new IterateBlock<Extension>(this.connectionLifecycleInterceptors.iterator()) {
 
                        @Override
                        void forEach(Extension each) throws SQLException {
                            if (!((ConnectionLifecycleInterceptor) each).commit()) {
                                this.stopIterating = true;
                            }
                        }
                    };
 
                    iter.doForAll();
 
                    if (!iter.fullIteration()) {
                        return;
                    }
                }
 
                // no-op if _relaxAutoCommit == true
                if (this.autoCommit && !getRelaxAutoCommit()) {
                    throw SQLError.createSQLException("Can't call commit when autocommit=true", getExceptionInterceptor());
                } else if (this.transactionsSupported) {
                    if (getUseLocalTransactionState() && versionMeetsMinimum(5, 0, 0)) {
                        if (!this.io.inTransactionOnServer()) {
                            return; // effectively a no-op
                        }
                    }
 
                    execSQL(null, "commit", -1, null, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY, false, this.database, null, false);
                }
            } catch (SQLException sqlException) {
                if (SQLError.SQL_STATE_COMMUNICATION_LINK_FAILURE.equals(sqlException.getSQLState())) {
                    throw SQLError.createSQLException("Communications link failure during commit(). Transaction resolution unknown.",
                            SQLError.SQL_STATE_TRANSACTION_RESOLUTION_UNKNOWN, getExceptionInterceptor());
                }
 
                throw sqlException;
            } finally {
                this.needsPing = this.getReconnectAtTxEnd();
            }
        }
        return;
    }

解析:

這段代碼與rollback()基本同樣,不一樣之處是commit()方法裏執行的是 execSQL(null, "commit", -1, null, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY, false, this.database, null, false);
rollback()方法裏執行的是 execSQL(null, "rollback", -1, null, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY, false, this.database, null, false);

執行事務期間涉及到的sql

  • /* mysql-connector-java-5.1.34 ( Revision: jess.balint@oracle.com-20141014163213-wqbwpf1ok2kvo1om ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'license' OR Variable_name = 'init_connect'

  • /* mysql-connector-java-5.1.34 ( Revision: jess.balint@oracle.com-20141014163213-wqbwpf1ok2kvo1om ) */SELECT @@session.auto_increment_increment

  • SET character_set_results = NULL

  • SET autocommit=1

  • SET sql_mode='STRICT_TRANS_TABLES'

  • SET autocommit=0

  • select @@session.tx_read_only

以上爲不徹底統計,jdbc包在執行一個真正的sql語句的時候,實際上會先執行一部分經常使用sql,把數據庫的一些信息給緩存起來。

mysql&oracle區別

oracle的commit詳細步驟

  • SGA中生成undo塊

  • SGA中生成了產生改變的數據塊和索引塊

  • SGA中生成前兩項的緩存redo信息

  • 依賴於前三項產生的數據量大小以及操做所須要的時間,buffer中的數據可能已經有一部分輸出到磁盤

  • 得到所需所有鎖

  • commit

  • 爲事務生成一個SCN,SCN是oracle數據庫的一種計時信息,用以保證事務的順序性,同時還用於失敗恢復和保證數據庫的讀一致性和檢查點,不管什麼時候何人提交,SCN自動加1

  • 將事務相關的未寫入redo log file 中的redo信息從redo log buffer寫入到redo log file ,這一步是真正的commit,這一步完成,才叫真正完成commit,事務條目從V$TRANSACTION中「刪除

  • V$LOCK中記錄的SESSION關於該事務的鎖會釋放,其餘須要這些鎖的事務被喚醒

  • 執行塊清理,清理快頭保存的事務信息

oracle的rollback詳細步驟

  • 撤銷已作的全部修改,從undo段讀回數據,逆向執行commit中的操做,並將undo條目標記爲已用,先前插入,則會刪除,先前更新,則會回滾取消更新,先前刪除,則回滾再次插入

  • 釋放會話中全部的鎖,喚醒等待鎖。

mysql的commit詳細步驟

  • InnoDB每次提交事務都會刷新日誌innodb_log到磁盤,磁盤速度比較慢,不要頻繁提交事務

JTA事務

JTA是一種高層,與實現無關,與協議無關的API,應用程序和應用服務器可使用JTA來訪問事務
計劃用JTA界定事務,那麼須要實現javax.sql.XADataSource、javax.sql.XAConnection、java.sql.XAResource。

容器事務

基於JTA,以及JNDI完成,容器負責事務的管理任務。

三種事務差別

  • JDBC 事務侷限於一個數據庫鏈接,使用簡單。

  • JTA事務功能強大,能夠跨多個數據庫多個DAO,比較複雜。

  • 容器事務,侷限於EJB使用。

補充知識

MySql經常使用命令

  • 查詢隔離級別
    select @@tx_isolation;

  • 設置手動提交
    set autocommit=0 ;

  • 查看當前事務自動提交模式
    select @@autocommit;

  • 設置隔離級別
    set tx_isolation = 'READ-COMMITTED';

  • 查詢表的狀態
    show table status like 'test1';

  • 修改表的存儲引擎
    alter table test1 engine = INNODB

  • 查看是否開啓日誌
    show variables like 'log_bin';

  • 查看日誌狀態
    show master status;


感謝您的耐心閱讀,若是您發現文章中有一些沒表述清楚的,或者是不對的地方,請給我留言,您的鼓勵是做者寫做最大的動力,
若是您認爲本文質量不錯,讀後以爲收穫很大,不妨請我喝杯咖啡,讓我更有動力繼續寫出高質量的文章。

  • 支付寶

  • 微信

做 者 : @mousycoder

原文出處 : http://mousycoder.com/2016/02/15/explain-transaction-in-simple-language-2/

創做時間:2016-2-15

更新時間:2016-2-15

相關文章
相關標籤/搜索