第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小时制。所以在使用相应的字符串格式的时候,我们还需要注意每种格式的使用方法,否则在对时间进行筛选的时候,很容易出错。