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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Marvel__Dead/article/details/70186180

介绍

无意中搜索到的一篇博客,《我对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语句还是弄明白了的。只要知道本质点的原理,到时候自己也是能够实现的!!!

展开阅读全文

没有更多推荐了,返回首页