第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小時制。所以在使用相應的字元串格式的時候,我們還需要注意每種格式的使用方法,否則在對時間進行篩選的時候,很容易出錯。