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