第29次文章:事务机制

  • 2019 年 10 月 8 日
  • 笔记

开始进入事务咯,一点点学习数据库内容啦~


一、事务

1、事务的基本概念

一组要么同时执行成功,要么同时执行失败的SQL语句。是数据库操作的一个执行单元!

事务开始于:

-连接到数据库上,并执行一条DML语句(insert、update或delete)。

-前一个事务结束后,又输入了另外一条DML语句。

事务结束于:

-执行commit或rollback语句

-执行一条DDL语句,例如create table、grant语句;在这种情况下,会自动执行commit语句。

-断开与数据库的连接。

-执行了一条DML语句,该语句却失败了;在这种情况中,会为这个无效的DML语句执行rollback语句。

2、事务的四大特点(ACID)

-atomicity(原子性):表示一个事务内部的所有操作是一个整体,要么全部成功,要么全部失败;

-consistency(一致性):表示一个事务内有一个操作失败时,所有的更改过的数据都必须回滚到修改前的状态;

-isolation(隔离性):事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事物修改它之后的状态,事务不会查看中间状态的数据。

-durability(持久性):持久性事务完成之后,它对于系统的影响是永久性的。

隔离性级别,从低到高:

读取未提交(Read uncommitted)——>读取已提交(read committed)——>可重复读(repeatable read)——>序列化(serializable)

在现实的使用中,一般使用的是读取已提交,在整个顺序中,越往后效率越低。

3、实际实现

事务的这种特点在现实生活中是十分容易被理解的,比如我们去银行取钱的过程中,卡上的余额和银行给出的金额,属于整个事务,只有每一步都执行成功之后,整个事务才会一起改变原有的状态。这也防止了,客户没有拿到钱,卡上余额却减少的情况发生。

下面我们结合简单实例来分析一下事务的运行机制:

package com.peng.jdbc;    import java.sql.DriverManager;  import java.sql.PreparedStatement;  import java.sql.SQLException;    import com.mysql.jdbc.Connection;  /**   * 测试事务   */  public class Demo06 {    public static void main(String[] args) {      Connection conn = null;      PreparedStatement ps1 = null;      PreparedStatement ps2 = null;      try {        //加载驱动类        Class.forName("com.mysql.jdbc.Driver");        conn = (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/testjdbc", "root", "123456");        conn.setAutoCommit(false);//JDBC中默认是true,自动提交事务          //此时ps1和ps2为同一事务,要么同时成功,要么同时失败        String sql = "insert into t_user (username,pwd) values (?,?)";        ps1 = conn.prepareStatement(sql);        ps1.setObject(1, "peng1");        ps1.setObject(2, "123456");        ps1.execute();        System.out.println("插入一条记录,peng1");          try {          Thread.sleep(6000);        } catch (InterruptedException e) {          e.printStackTrace();        }          ps2 = conn.prepareStatement(sql);        ps2.setObject(1, "peng2");        ps2.setObject(2, "123456");        ps2.execute();        System.out.println("插入一条记录,peng2");          conn.commit();//对此事务进行手动提交      } catch (ClassNotFoundException e) {        e.printStackTrace();        try {          conn.rollback();//如果报有异常,则回滚到原来的位置        } catch (SQLException e1) {          e1.printStackTrace();        }      } catch (SQLException e) {        e.printStackTrace();      }finally {              try {          if(ps1 != null) {            ps1.close();          }        } catch (SQLException e) {          e.printStackTrace();        }        try {          if(ps2 != null) {            ps2.close();          }        } catch (SQLException e) {          e.printStackTrace();        }        try {          if(conn != null) {            conn.close();          }        } catch (SQLException e) {          e.printStackTrace();        }      }    }  }

tips:

(1)在这段代码中,我们将JDBC的事务提交方式由自动提交事务改为手动提交(false)。

(2)我们在整段代码中创建了两个PreparedStatement对象,分别是ps1和ps2,在两个对象的向数据库中插入记录之后,我们进行手动提交事务。所以,此时ps1和ps2合在一起构成了一个完整的事务,只有两个对象同时成功,才能够完成整个事务。为了模拟一个事务机制,我们在两个对象ps1和对象ps2之间线程休眠6秒。

我们先清空数据库的t_user表格,成功的插入两条语句之后,我们依次查看控制台和数据库表格中的信息。

fig1:控制台信息

fig2:数据库信息

从控制台和数据库表格的情况来看,两者都是已经成功的插入了相应的语句。

下面我们来检测一下加入第二条插入命令失败之后的结果(我们还是先清空一下数据表格),在上段代码中注释掉命令“ps2.setObject(2, "123456");”,不向sql语句的第二个占位符处输送参数。继续查看控制台和数据库的结果:

fig3:插入失败控制台信息

fig4:事务失败后数据库中的信息

在这次的结果中,我们可以从控制台的信息中知道只有对象ps2的语句运行失败。当我们查看数据库的表格的时候,发现数据库依旧为空,代表着两条记录都没有被插入进来。

tips:经过上面成功和失败的案列对比,可以很好的体现事务的特点:我们将ps1和ps2当做一个事务来进行处理,只有当两条命令ps1和ps2都成功的执行之后,数据库中才会产生相应的结果,否则会回滚(rollback)到整个事务开始之前的状态,回滚操作放在异常出现的地方。

二、时间类型

在数据库中,我们经常会涉及到一些时间相关的操作,比如登录时间,注册时间等等,这些时间也有不一样的类型,分别代表着不同的精度。

1、java.util.Date

数据库中的时间类型都是继承自java.util.Date中的类型。

-子类:java.sql.Date 表示年月日

-子类:java.sql.Time 表示时分秒

-子类:java.sql.TimeStamp 表示年月日时分秒

2、日期比较处理

通过上面不同时间类型的简单介绍,我们可以根据它们的差别,进行简单的下面两个层面的应用。在实际中,一般使用的都是Date和TimeStamp进行使用。

(1)插入随机日期

我们向表格中插入1000条记录,测试一下上面我们介绍到几种时间类型。

package com.peng.jdbc;    import java.sql.DriverManager;  import java.sql.PreparedStatement;  import java.sql.SQLException;  import java.util.Random;    import com.mysql.jdbc.Connection;  /**   * 测试时间类型(Date,Time,TimeStamp)   */  public class Demo07 {    public static void main(String[] args) {      Connection conn = null;      PreparedStatement ps = null;      try {        //加载驱动类        Class.forName("com.mysql.jdbc.Driver");        conn = (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/testjdbc", "root", "123456");        conn.setAutoCommit(false);        String sql = "insert into t_user (username,pwd,regTime,lastLoginTime) values (?,?,?,?)";        for (int i = 0;i<1000;i++) {          ps = conn.prepareStatement(sql);          ps.setObject(1, "peng"+i);          ps.setObject(2, "123456");          long rand = 1000000000 + new Random().nextInt(1000000000);          java.sql.Date date1 = new java.sql.Date(System.currentTimeMillis() - rand);          java.sql.Timestamp date2 = new java.sql.Timestamp(System.currentTimeMillis() - rand);          ps.setDate(3, date1);          ps.setTimestamp(4, date2);          ps.execute();        }        System.out.println("插入一条记录,peng");        conn.commit();//对此事务进行手动提交      } catch (ClassNotFoundException e) {        e.printStackTrace();        try {          conn.rollback();//如果报有异常,则回滚到原来的位置        } catch (SQLException e1) {          e1.printStackTrace();        }      } catch (SQLException e) {        e.printStackTrace();      }finally {        try {          if(ps != null) {            ps.close();          }        } catch (SQLException e) {          e.printStackTrace();        }        try {          if(conn != null) {            conn.close();          }        } catch (SQLException e) {          e.printStackTrace();        }      }    }  }

随机查看一部分结果:

tips:在我们设计数据库表格的时候,我们将“regTime”的类型设计为Date类型,将“lastLoginTime”设计为Timestamp,在表格中的显示上也可以看出两者之间的不同。

(2)取出指定日期范围的记录

根据上面已经插入的表格,我们对时间一定范围内的记录进行筛选

/**   * 测试时间类型(Date,Time,TimeStamp),取出指定时间段的数据   */  public class Demo08 {      /**     * 将时间字符串转换为一个long型数字     * @param dateStr     * @return     */    public static long str2date (String dateStr) {      DateFormat formate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");      try {        return formate.parse(dateStr).getTime();      } catch (ParseException e) {        e.printStackTrace();        return 0;      }    }      public static void main(String[] args) {      Connection conn = null;      PreparedStatement ps = null;      ResultSet rs = null;      try {        //加载驱动类        Class.forName("com.mysql.jdbc.Driver");        conn = (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/testjdbc", "root", "123456");        //根据regTime的Date类型进行筛选  //      String sql = "select * from t_user where regTime>? and regTime<?";//?代表占位符,在此处是占位等待后续的参数传进来  //      ps = conn.prepareStatement(sql);  //      ps.setObject(1, new java.sql.Date(str2date("2019-07-08 12:00:00")));  //      ps.setObject(2, new java.sql.Date(str2date("2019-07-12 00:00:00")));  //      rs = ps.executeQuery();  //      while(rs.next()) {  //        System.out.println(rs.getInt("id")+"----"+rs.getString("username")+"----"+rs.getDate("regTime"));  //      }          //根据lastLogintime的Timestamp进行筛选        String sql = "select * from t_user where lastLoginTime>? and lastLoginTime<? order by lastLoginTime";        ps = conn.prepareStatement(sql);        ps.setObject(1, new Timestamp(str2date("2019-07-12 12:00:00")));        ps.setObject(2, new Timestamp(str2date("2019-07-12 13:00:00")));        rs = ps.executeQuery();        while(rs.next()) {          System.out.println(rs.getInt("id")+"----"+rs.getString("username")+"----"+rs.getTimestamp("lastLoginTime"));        }      } catch (ClassNotFoundException e) {        e.printStackTrace();      } catch (SQLException e) {        e.printStackTrace();      }finally {        try {          if(rs != null) {            rs.close();          }        } catch (SQLException e) {          e.printStackTrace();        }        try {          if(ps != null) {            ps.close();          }        } catch (SQLException e) {          e.printStackTrace();        }        try {          if(conn != null) {            conn.close();          }        } catch (SQLException e) {          e.printStackTrace();        }      }    }  }

我们简单看一下结果图:

tips:

(1)在对时间进行筛选的时候,我们可以根据regTime的Date类型进行筛选(注释掉的那一段),也可以根据lastLogintime的Timestamp进行筛选。两种方式在上面的代码中都已经被呈现出来。

(2)在new Date对象和Timestamp对象的时候,由于都可以向构造器中传递long类型的毫秒参数,所以我们自己定义一个方法str2date(),将我们传递的时间字符串转化为长整形数据毫秒。

(3)在使用SimpleDateFormat定义时间字符串类型的时候,我们需要注意一下每个单位的大小写,比如:时分秒中的小时“H”代表着24小时制,"h"代表12小时制。所以在使用相应的字符串格式的时候,我们还需要注意每种格式的使用方法,否则在对时间进行筛选的时候,很容易出错。