探究MySQL的DML提交事務的意義和DQL是否有必要提交事務

Silence_Mind發表於2017-04-16

介紹

無意中搜尋到的一篇部落格,《我對autocommit以及select語句是否需要加事務的一點理解》,該博主非常的有探究心,感受到了博主的勤能補拙。

看了下上邊的部落格,有點明白DML提交事務的意義,DQL是否有必要提交事務,Hibernate和MyBatis為什麼DQL操作時不需要手動提交事務!!

預備工作

環境:

MySQL資料庫引擎設定為InnoDB,資料庫隔離級別設定為REPEATABLE-READ。

下面的所有操作我都將按照autocommit這個資料庫重要屬性展開!!

autocommit:意為自動提交事務!!它有兩個屬性值1和0,1表示對於每條sql語句都會自動幫你提交事務!!0表示不自動提交事務!!需要各位程式設計師手動提交事務!!預設值為1;

注意:autocommit只涉及是否幫你提交事務,如果你沒有顯示的開啟一個事務,那麼資料庫會幫你自動開啟一個事務的。

DML提交事務的意義

DML是資料操作語言。例如sql中的關鍵字insert , update ,delete這些都是DML操作。

通過sql語句進行試驗:

開啟第一個資料庫連線執行sql:

SET autocommit  = 0;

SELECT @@autocommit;

好,我們現在開啟第二個資料庫連線(別腦殘的關閉當前連線哦,我們還要用呢)

執行以下sql

SELECT @@autocommit;

SELECT * FROM USER WHERE id=53;

在第一個資料庫連線中執行sql:

UPDATE USER SET username = '小楊' WHERE id=53;

SELECT * FROM USER WHERE id=53;

從輸出結果來看,對!!修改成功了

這裡寫圖片描述

在第二個資料庫連線中執行:

SELECT * FROM USER WHERE id=53;

出人意料的答案,為什麼??我不是已經在上一個資料庫連線中修改了嗎??

這裡寫圖片描述

正確答案是:

上一個資料庫連線雖然開啟了事務,並且事務裡面也有很多sql操作,但是你是否已經提交了事務呢??並沒有吧,所以你可以嘗試提交事務!!!但是在這裡不要著急,我們還不提交事務,我們還要看一個有趣的東西,什麼東西呢??

在第二個資料庫連線中輸入修改資訊sql語句!!

UPDATE USER SET username = 'LangGuys' WHERE id=53;
//注意這裡的id和第一個連線用的id是一致的,否則看不出效果!!

執行後發現。。。。嗯??為什麼這條語句執行這麼久了還在執行??我的內心是崩潰的!!*_*我哭了。。。

不行了我要在第一個資料庫連線中提交下事務!!執行sql

commit;

嗯??驚奇的發現第二個資料庫連線執行的sql瞬間執行成功了!!我很慌。。。為什麼會這樣'_'。有點害怕。。。

其實這是資料庫的更新丟失處理解決辦法,當一個事務正在更新某行資料時,在為提交之前,當行資料是被加鎖的,另外一個事務要進行更新當行資料是會進行等待的。直到第一個更新事務提交,其它事務才有機會進行資料更新。這種處理和事務無關,和隔離級別無關,這種處理是應用程式處理的。

總結:

對於DML資料操作,我們必須要記住提交事務,如果autocommit為1的話,當然就不用我們自己操心了!!資料庫會幫我們提交的!!但是在我們的Hibernate和MyBatis等持久層框架中,進行DML操作時我們必須要手動開啟事務,並且手動提交事務!!因為在Hibernate和MyBatis等持久層框架中,它們處理DML語句的時候會自動設定autocommit=0;如果DML中不進行手動提交事務,那麼最後事務就會進行回滾。

問:

如果你要問我為什麼不設定autocommit=1自動提交,反而還要很麻煩的自己開啟事務,並且自己手動提交事務??

答:

為什麼不直接設定為autocommit=1??因為autocommit是針對於單條sql語句的自動提交。反而我們真實寫專案的時候,用到的事務都是包含多條sql語句,所以我們不得不自己手動顯示開啟事務,並且手動進行事務提交!!以此來建立一個事務作用邊界。

最後:DML提交事務的意義:事務是什麼?事務是多個sql操作的集合,而開啟事務就相當於開啟一個執行緒,多個sql操作就相當於在這個執行緒裡面執行多個任務,而我們的提交事務就相當於儲存資料,回滾事務就相當於不儲存資料。

對於MyISAM等不支援事務的資料庫引擎來說,就不會涉及到這些事務開啟和提交的問題,當然每一條sql語句也相當於一個執行緒了。

好!!非常Nice!!

DQL是否有必要提交事務

是否有必要提交事務不是我們說了算,我們還要實踐後才知道。

首先開啟第一個資料庫連線

輸入以下sql:

SET autocommit  = 0;

SELECT @@autocommit;

SELECT * FROM USER WHERE id=1;

執行能看到結果,感覺還不賴嘛

這裡寫圖片描述

現在建立第二個資料庫連線,輸入以下sql:

SELECT @@autocommit;

UPDATE `user` SET username = 'BestGuard' WHERE id =1;

SELECT * FROM USER WHERE id=1;

嗯,不錯!!成功修改值!!

這裡寫圖片描述

好了,準備工作做完了,到第一個資料庫連線裡面見證奇蹟吧!!

輸入查詢語句:

SELECT * FROM USER WHERE id=1;//執行多次你會發現,資料還是一樣的,說明該條語句處於事務裡面。下面我會解釋為什麼處於事務裡面多次查詢資料會相同!!

得到查詢結果

這裡寫圖片描述

嗯??不是吧!!我眼睛應該沒花吧,這個竟然是”FireGuard”,orz ·_·

思考為什麼會這樣呢??我有點害怕!!

要明白這個問題的出現,首先我們需要知道:

  • 我們不顯示開啟事務時,MySQL會幫我們自動開啟事務。
  • 我們總的環境是:MySQL的InnoDB引擎。
  • 第一二個資料庫連線處於的環境是REPEATABLE-READ事務隔離級別。
  • REPEATABLE-READ(防止髒讀,不可重複讀)的意思是能夠在一個事務中查詢出來的資料始終保持相同。

所以由於事務隔離級別的原因,MySQL的預設事務隔離級別為REPEATABLE_READ,可以防止不可重複讀。所以在一個事務進行讀的過程中,如果中間發生了其它事務的資料修改,並且這些其它事務進行了提交事務,那麼我在一個事務中未提交事務前進行的讀操作始終讀取出來的資料是相同的,這些未被修改的資料是被稱為中間資料,這種技術叫做資料庫多版本併發控制!!

試試下面這兩條!!

針對這種情況有兩種解決辦法:


原理:
//通過提交事務來結束上一個開啟的事務
//在REPEATABLE-READ這種事務隔離級別中,如果開啟一個新的事務並且第一次根據條件查詢資料(查詢出來的資料是當前最新的資料),以後再次根據此條件查詢的資料都相同。這就是REPEATABLE-READ這個事務隔離級別的威力!!防止髒讀,能夠在一個事務中可重複讀。

第一種:

COMMIT;

SELECT * FROM USER WHERE id=1;//這時查詢的就是最新資料,注意:這條查詢資料隱含了一個開啟事務的過程。前提條件:如果之前的事務沒有被結束(提交或者回滾事務),這時資料庫是不會幫你自動開啟事務的。這裡就是使用commit去結束之前的事務。

第二種:

//我重新開啟一個事務:
start transaction;

SELECT * FROM USER WHERE id=1;//再次查詢。

這裡寫圖片描述

嗯!!Nice!!

值得注意的一點是:

當一個事務沒有被結束時,此事務執行了更新,插入,刪除等需要提交事務的操作時,如果我們重新開啟一個事務時,上一個事務就會被commit,裡面的更新,插入,刪除資料就會被持久化(儲存到硬碟)!!

開啟第一個資料庫連線:

  • 執行以下sql
SET autocommit=0;//因為是新的資料庫連線,所以在這句資料庫會自動幫我們開啟事務!!我們不顯示開啟,資料庫就會幫我們自動開啟。

SELECT @@autocommit;

UPDATE USER SET username='bibibi' WHERE id=1;

INSERT INTO USER(username,birthday,sex,address) VALUES('中二','1202-09-23',1,'');//直到這裡,事務都還沒被提交!!

開啟第二個資料庫連線:

  • 執行以下sql
SELECT * FROM USER;

資料沒被加進去很正常!!因為我根本就沒有提交事務嘛。
這裡寫圖片描述

在第一個資料庫連線中執行以下sql:

START TRANSACTION;

再次在第二個資料庫連線中查詢資料:

這裡寫圖片描述

很明顯資料被修改了!!所以如果我們上一個事務沒有被提交,現在重新開啟一個事務,那麼上一個事務就會被資料庫自動提交!!

然而在很多框架中,比如Hibernate和MyBatis中,如果我們插入,刪除,更新時沒有提交事務,它們的做法就是進行事務回滾,然而MySQL的預設做法就是自動提交!!

所以我們上面一個驗證中,我們的第二個解決辦法(重新開啟一個事務來解決資料老化問題),其實和第一個解決辦法有點類似,都是通過commit提交來關閉事務的。而第二種只是多了一個再次開啟事務的操作!!

總結:

  • 通過上面的驗證,我們已經知道了,在REPEATABLE-READ事務隔離級別中,進行DQL操作時,我們往往需要commit、回滾或者重新開啟事務來結束當前事務,以此獲取新事務解決資料老化問題。

然而在Hibernate和MyBatis等持久層框架中,你會發現往往不需要我們去管理DQL的事務。我們不需要手動開啟事務,也不需要提交事務。那麼框架是怎麼處理這種問題的呢??而且Hibernate和MyBatis的autocommit總是被框架設定為0。

看下下面的程式碼:

    @Test
    public void showSigleQuery(){
        Session session=HibernateUtils.getSessionFactory().openSession();
        NativeQueryImpl<User> nativeQuery=(NativeQueryImpl<User>)session.createNativeQuery("select * from user");

        List<User> list=nativeQuery.addEntity(User.class).list();

        for(User i:list){
            System.out.println(i);
        }

        System.out.println("########################################");

        Session session2=HibernateUtils.getSessionFactory().openSession();
        Transaction transaction=session.beginTransaction();

        User user=new User("jkjkjk", "123456");

        session2.save(user);

        transaction.commit();

        session2.close();

        list=nativeQuery.addEntity(User.class).list();

        for(User i:list){
            System.out.println(i);
        }
    }

輸出結果:

Hibernate: 
    select
        * 
    from
        user
User [uid=2, uname=FireLang, pword=456123]
User [uid=3, uname=LangSheng, pword=123456]
User [uid=4, uname=FireLang, pword=456123]
User [uid=5, uname=北斗狼神, pword=520]
User [uid=6, uname=CSDN-LANG, pword=123]
User [uid=7, uname=Fire01, pword=123456]
User [uid=9, uname=Fire03, pword=123456]
User [uid=10, uname=Fire04, pword=123456]
User [uid=11, uname=Fire04, pword=123456]
User [uid=12, uname=Fire08, pword=123456]
User [uid=18, uname=565656, pword=123456]
########################################
Hibernate: 
    insert 
    into
        user
        (uname, pword) 
    values
        (?, ?)
Hibernate: 
    select
        * 
    from
        user
User [uid=2, uname=FireLang, pword=456123]
User [uid=3, uname=LangSheng, pword=123456]
User [uid=4, uname=FireLang, pword=456123]
User [uid=5, uname=北斗狼神, pword=520]
User [uid=6, uname=CSDN-LANG, pword=123]
User [uid=7, uname=Fire01, pword=123456]
User [uid=9, uname=Fire03, pword=123456]
User [uid=10, uname=Fire04, pword=123456]
User [uid=11, uname=Fire04, pword=123456]
User [uid=12, uname=Fire08, pword=123456]
User [uid=18, uname=565656, pword=123456]
User [uid=19, uname=jkjkjk, pword=123456]

發現我並沒有結束事務就已經讀取到了最新的資料。由此可以說明,很多事情框架都已經幫我們做了,比如這次的DQL時,事務的結束或者說重新開啟事務就是框架幫我們做的!!你想想啊,我們操作最原始的MySQL語句實踐出來的東西還有錯???框架尋根究底也是對這些操作的封裝罷了!!同時Hibernate和MyBatis的資料庫連線時,基本事務隔離級別的環境也是REPEATABLE-READ。

總結:

  • DML要提交事務後才能夠把資料持久化。
  • 框架中DML要我們手動開啟事務和提交事務。
  • 框架中DQL不需要我們手動開啟事務或者提交事務。我們不做同樣能夠讀取到最新資料!!DQL的事務開啟或者提交是框架幫我們做!!
  • DQL不同隔離級別查詢的值會不同,針對於MySQL,InnoDB的REPEATABLE_READ,其保證了同一個事務裡,查詢的結果和開啟事務時並且第一次查詢資料時的資料總是一樣的。
  • 這裡補充一點,MyBatis在session.close()中如果session.commit()沒有被執行,那麼在session.close()中就會進行事務的回滾。

說到這裡,我就想問各位讀者一個問題了。在框架中為什麼DML操作需要我們手動開啟事務和關閉事務呢??反而DQL不需要呢??

有很多程式設計師說,DQL不需要開啟事務,但是通過我們的最佳實踐知道,就算我們不開啟事務,資料庫也會幫我們開啟事務。而且在一個事務中查詢資料還會一直相同(REPEATABLE_READ事務隔離級別)。所以這和很多程式設計師的觀點:”DQL不需要開啟事務”是矛盾的!!

  • 那麼怎麼回答上面的問題呢??我想是因為有很多MySQL引擎有關。MySQL的MyISAM不是說不支援事務嗎??所以在DML中需要我們自己把控。
  • 那麼為什麼DQL的事務不要我們把控呢??因為開啟事務很浪費CUP資源,同時DQL查詢時為了不要總是讀到老的資料,可能會有一個優化在裡面,雖然事務也能夠防止讀到老的資料,但是很浪費CPU啊,所以不用事務的話,一定用了其它方法代替事務,完成和事務一樣的功能但是不浪費CUP!!!
  • 該問題的回答純屬個人觀點,有更好的實踐請告訴筆者,在下一定不勝感激!!!

DQL探究問題的核心是:資料庫隔離級別!!!

檢視部落格:資料庫隔離級別詳細介紹和更新資料丟失處理介紹

關於本部落格相關參考資料

髒讀,不可重複讀,幻讀講解

結語

有很多人說,作為一個程式設計師,我為什麼還要研究這些??知道怎麼用就行了。
我的回答只有哈哈!!如果你想自己寫一個框架出來,那麼你就必須要知道!!

有些針對我結語前的問題提出控訴的人,我也想給你看看一串程式碼:

@Test
        public void showUser(){

            //資料庫連線
            Connection connection = null;
            //預編譯的Statement,使用預編譯的Statement提高資料庫效能
            PreparedStatement preparedStatement = null;
            //結果 集
            ResultSet resultSet = null;

            try {
                //載入資料庫驅動
                Class.forName("com.mysql.jdbc.Driver");

                //通過驅動管理類獲取資料庫連結
                connection =  DriverManager.getConnection("jdbc:mysql://localhost:3306/hibernate?characterEncoding=utf-8", "root", "");
                connection.setTransactionIsolation(connection.TRANSACTION_REPEATABLE_READ);
                connection.setAutoCommit(false);
                //定義sql語句 ?表示佔位符
                String sql = "select * from user";
                //獲取預處理statement
                preparedStatement = connection.prepareStatement(sql);
                //向資料庫發出sql執行查詢,查詢出結果集
                resultSet =  preparedStatement.executeQuery();
                //遍歷查詢結果集
                while(resultSet.next()){
                    System.out.println(resultSet.getString("uid")+"  "+resultSet.getString("uname"));
                }
                //####################################

                Connection connection2=DriverManager.getConnection("jdbc:mysql://localhost:3306/hibernate?characterEncoding=utf-8", "root", "");
                connection2.setTransactionIsolation(connection.TRANSACTION_REPEATABLE_READ);
                connection2.setAutoCommit(false);


                connection2.prepareStatement("insert into user(uname,pword) values('Fire09','123456')").executeUpdate();


                connection2.commit();
                connection2.close();
                //#####################################

                resultSet =  preparedStatement.executeQuery();
                //遍歷查詢結果集
                while(resultSet.next()){
                    System.out.println(resultSet.getString("uid")+"  "+resultSet.getString("uname"));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally{
                //釋放資源
                if(resultSet!=null){
                    try {
                        resultSet.close();
                    } catch (SQLException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                if(preparedStatement!=null){
                    try {
                        preparedStatement.close();
                    } catch (SQLException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                if(connection!=null){
                    try {
                        connection.close();
                    } catch (SQLException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }

            }

        }

我就想問你,第一次輸出的結果集合第二次輸出的結果集是否一致??

反正Hibernate框架輸出來是不一致的,MyBatis也是不一致的,那麼我的原始JDBC程式碼呢??是否一致??

哼哼,這種原始JDBC程式碼和我們直接在cmd打mysql語句輸出是一樣的。所以這裡我就不說答案了。

如果我現在也像Hibernate等框架使用連線池這種資料來源來獲取connection,每次查詢完畢後connection都會被回收儲存起來,這樣就會導致上次查詢的事務沒有被關閉,下次按照同樣條件查詢時,資料總是和上次查詢的一樣!!那怎麼處理查詢時一直是老資料的問題呢??你可能就會不知道!更無知點還可能說為什麼會出現這種問題!!

雖然最後沒有得出框架為什麼會不用給DQL新增事務,而且還能夠防止資料總是不為老資料,但是基本的MySQL語句還是弄明白了的。只要知道本質點的原理,到時候自己也是能夠實現的!!!

相關文章