java事務管理-上

JDBC基礎事務管理:

事務簡介
事務的作用是保護數據完整性.它將一系列操作作爲一個原子操作來處理, 從而防止多個操作中有的操作失敗有的操作成功, 造成數據不完整. 在 JDBC 中, 一個連接默認的提交方式是 Auto-Commit 模式, 在這種模式下, 每個單獨的SQL語句都會被視爲一個事務, 當SQL_執行時(更確切的說是完成_時,詳見 這裏), 就會被提交到數據庫. 然而, 在要求多個 SQL 同時成功或者失敗的數據完整性問題時, 這種模式不能滿足需要.


禁用 Auto-Commit模式
JDBC 爲我們提供了手動管理事務的方式. 在取得 Connection 後, 調用:


    conn.setAutoCommit(false);
就可以關閉自動提交, 同時, 這也開啓了一個事務.


提交/回滾事務
當 Auto-Commit 禁用時, 我們需要自己管理事務, 提交事務的 API 爲:


    conn.commit()
調用後, 事務內所有的 SQL 語句將會被視爲一個原子被提交.


然而, 執行 SQL 的時候也有可能出現異常, 異常產生後, 我們需要保持數據完整性, 要回滾已經做的改變:


    conn.rollback();
調用 conn.rollback() 的時機是在 catch 語句內, 因爲當有SQL執行失敗時, 會有SQLException拋出,這也是唯一知道是否成功提交與否的途徑. 所以將 conn.rollback() 寫在 catch 中吧.


Note: 已經 commit 的事務, 無法再 rollback.


事務隔離等級
利用事務,我們可以保證一系列操作的原子性, 但是卻不能保證多個事務之間的影響. 考慮以下情況:


1) 當事務A在更新表的一行數據, 當執行更新後但是在提交事務前, 另外一個事務B來讀走了數據, 這時事務A發現自己更新的數據有問題, 並沒有做提交, 而是回滾了剛纔的操作, 事務B實際上讀到的是未提交的數據.


2) 當事務A讀取了一個行數據, 之後事務B對這行數據進行了修改並提交, 這時事務A又重讀了一次這行, 發現同一個事務內兩次讀到數據不一樣.


3) 當事務A根據條件查詢了若干行數據, 之後事務B插入/更新/刪除了同一個表中的幾行數據, 而事務B中修改的數據恰好有幾條符合事務A的查詢條件, 這時事務A由根據之前的條件重新查詢, 發現同一個事務根據同一查詢條件查到的結果卻不同.


第一種現象叫 Dirty Read(髒讀). 第二種現象叫 Repeatable Read(重複讀). 第三種現象叫 Phantom Read(幻讀).


解決這三種現象是數據庫的鎖機制, 比如行級鎖, 表級鎖, 但是各個數據庫的實現方法不同. JDBC中我們可以控制事務的隔離級別, 也就是鎖的各種機制, 來避免這三種現象. 相關API爲:


    conn.setTransactionIsolation(/*...*/);
可以傳入的隔離級別有:


Using Transactions  The Java™ Tutorials   JDBC TM  Database Access   JDBC Basics 


值得注意的是, 並不是隔離級別越高越好, 隔離級別越高, 數據越精確, 但是由於鎖的範圍也會相對變大, 數據多的話與數據庫I/O的開銷也會越大. 所以選擇合適的隔離等級也是提升系統性能的關鍵之一.


對於事務的隔離級別, JDBC 並不是要求每種都支持的, 廠商可以有不同的方式以及實現情況, 可以調用 DatabaseMetaData.supportsTransactionIsolationLevel() 來查看支持情況. 調用 DatabaseMetaData.getDefaultTransactionIsolation 可以得到默認的隔離級別.


以下代碼示例如何獲取隔離級別的支持情況:


    Connection conn = getConnection();
    DatabaseMetaData md = conn.getMetaData();
    System.out.println("TRANSACTION NONE: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_NONE));
    System.out.println("TRANSACTION READ COMMITTED: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_COMMITTED));
    System.out.println("TRANSACTION READ UNCOMMITTED: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_UNCOMMITTED));
    System.out.println("TRANSACTION REPEATABLE READ: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_REPEATABLE_READ));
    System.out.println("TRANSACTION SERIALIZABLE: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_SERIALIZABLE));
    System.out.print("Default Transaction: ");
    switch(md.getDefaultTransactionIsolation()) {
        case Connection.TRANSACTION_NONE:
            System.out.println("None");
            break;
        case Connection.TRANSACTION_READ_COMMITTED:
            System.out.println("Read Commit");
            break;
        case Connection.TRANSACTION_READ_UNCOMMITTED:
            System.out.println("Read Uncommit");
            break;
        case Connection.TRANSACTION_REPEATABLE_READ:
            System.out.println("Repeatable read");
            break;
        case Connection.TRANSACTION_SERIALIZABLE:
            System.out.println("Serializable");
            break;
    }
    conn.close();
Mysql 和 JavaDB 的輸出結果都爲:


    TRANSACTION NONE: false
    TRANSACTION READ COMMITTED: true
    TRANSACTION READ UNCOMMITTED: true
    TRANSACTION REPEATABLE READ: true
    TRANSACTION SERIALIZABLE: true
    Default Transaction: Read Commit
Savepoint 保存點
JDBC 3.0 引入了 Savepoint 接口. 我們可以在代碼中設立 Savepoint 然後根據條件 rollback 到某個點, 這樣的好處是可以更靈活的控制事務的提交內容. 創建檢查點的API爲:


     Savepoint svpt1 = conn.setSavepoint();
回滾到某一檢查點的 API 爲:


    conn.rollback(svpt1);
釋放某個檢查點的 API 爲:


    conn.releaseSavepoint(svpt1);
Note: 當整個事務提交或回滾時, 事務內的 Savepoint 會自動失效. 當事務回滾到某個 Savepoint 時, 之後創建的 Savepoint 也會釋放.


    public class Transaction {


        private static final String INSERT_POST
                = "INSERT INTO POSTS VALUES(NULL, ?, ?, DEFAULT, ?)";


        private static final String INSERT_TAG
                = "INSERT IGNORE INTO TAGS VALUES(NULL, ?)";


        private static final String INSERT_POST_TAG
                = "INSERT INTO POST_TAG VALUES(NULL, ?, ?)";


        private Properties dbProps = new Properties();
        private Transaction() {}


        public Properties getDbProps() {
            return dbProps;
        }


        public void setDbProps(Properties dbProps) {
            this.dbProps = dbProps;
        }


        public void insertPost(Post post) {


            Connection conn = null;
            try {
                conn = DBUtils.getConnection(dbProps);
                conn.setAutoCommit(false);


                PreparedStatement insertTag = conn.prepareStatement(INSERT_TAG);
                Statement queryTags = conn.createStatement();
                PreparedStatement insertPost = conn.prepareStatement(INSERT_POST, Statement.RETURN_GENERATED_KEYS);
                PreparedStatement insertPostTag = conn.prepareStatement(INSERT_POST_TAG);


                ArrayList<Tag> tags = post.getTagList();
                ArrayList<String> tagNameList = new ArrayList<String>();
                for (Tag t : tags) {
                    tagNameList.add(t.getName());
                    insertTag.setString(1, t.getName());
                    insertTag.addBatch();
                }
                insertTag.executeBatch();
                // 更好的方法是存儲過程,MySQL不支持 ANY(?)
                StringBuilder sqlbuilder = new StringBuilder();
                for (Tag t : tags) {
                    sqlbuilder.append("\"" + t.getName() + "\"" +",");
                }
                String sqlQueryTags = "SELECT * FROM TAGS WHERE NAME IN ("
                        + sqlbuilder.deleteCharAt(sqlbuilder.length()-1) + ")";


                if (tags.size() > 0 && !queryTags.execute(sqlQueryTags)) {
                    throw new SQLException("Query tags failed.");
                } else {
                    ResultSet rsTags = queryTags.getResultSet();
                    ArrayList<Integer> tagIDList = new ArrayList<Integer>();
                    while (rsTags.next()) {
                        tagIDList.add(rsTags.getInt("ID"));
                    }
                    rsTags.close();


                    insertPost.setString(1, post.getTitle());
                    insertPost.setString(2, post.getContent());
                    insertPost.setBoolean(3, post.isVisible());
                    insertPost.executeUpdate();
                    ResultSet generatedKeys = insertPost.getGeneratedKeys();
                    if (generatedKeys.next()) {
                        post.setId(generatedKeys.getInt(1));
                        System.out.println("Generated new key for post : " + generatedKeys.getInt(1));
                    } else {
                        throw new SQLException("Creating user failed, no ID obtained");
                    }


                    for (int i : tagIDList) {
                        insertPostTag.setInt(1, post.getId());
                        insertPostTag.setInt(2, i);
                        insertPostTag.addBatch();
                    }
                    insertPostTag.executeBatch();
                    conn.commit();
                }
            } catch (SQLException e) {
                DBUtils.printSQLException(e);
                try {
                    if (conn != null) {
                        conn.rollback();
                    }
                } catch (SQLException e1) {
                    DBUtils.printSQLException(e1);
                }
            } finally {
                try {
                    if (conn != null) {
                        conn.close();
                    }
                } catch (SQLException e2) {
                    DBUtils.printSQLException(e2);
                }
            }
        }


        public static void main(String [] args) throws IOException, SQLException {
            Properties props = new Properties();
            props.load(ClassLoader.getSystemResourceAsStream("db.mysql.props"));
            Connection conn = DBUtils.getConnection(props);
            DBUtils.printTransactionIsolation(conn);


            Transaction trans = new Transaction();
            trans.setDbProps(props);


            Post post = new Post();
            post.setTitle("JDBC");
            post.setContent("JDBC Introduction");


            ArrayList<Tag> tagList = new ArrayList<Tag>();
            tagList.add(new Tag("JDBC"));
            tagList.add(new Tag("Java"));
            post.setTagList(tagList);
            trans.insertPost(post);
        }
    } 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章