事务的概念
事务是一组SQL组成的逻辑处理单元,通常有四个特性,简称ACID:
原子性(Atomic):数据库事务是不可分割的工作单位。事务中的SQL语句要么都执行成功,要么都执行失败。
一致性(Consistency):数据库事务不能破坏数据库的完整性和业务逻辑的一致性。例如银行转账业务,A给B转账,不管转账失败还是成功,A和B的总存款不能变。
隔离性(Isolation):在并发的环境中,当不同的事务操作相同的数据时,每个事务都有各自完整的数据空间。
持久性(Durability):只要事务提交成功,对数据库做的更改必须永久的保存下来。即使数据库奔溃或者数据库重启,也能恢复到事务提交后的状态。
事务的隔离性
1、我们先看看如果不考虑事务的隔离性,会发生的几种问题:
(1)脏读
脏读是指在一个事务读取了另一个未提交的数据。
当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下
update account set money=money+100 where name=’B’; (此时A通知B)update account set money=money - 100 where name=’A’;
当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。
(2)不可重复读
不可重复读是指在对于数据库中的某一行数据,一个事务范围内多次查询却返回了不同的数据值。原因是另外一个事务做了更新操作。
例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发生了不可重复读。
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……
(3)虚读(幻读)
幻读是指一个事务内的多次查询返回的结果集不一致。原因是另外一个事务做了插入或者删除操作。
例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
幻读和不可重复读都是读取了另一个事务已提交的数据(这点和脏读不同),所不同的是不可重复读查询的都是同一行数据,是另外一个事务update操作导致出现的问题。而幻读针对的是一批数据整体(比如数据的个数),是另外一个事务insert或delete操作导致出现的问题。
2、现在来看看MySQL数据库为我们提供的四种隔离级别:
① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
② Repeatable read (可重复读):可避免脏读、不可重复读的发生。
③ Read committed (提交读):可避免脏读的发生。
④ Read uncommitted (未提交读):最低级别,任何情况都无法保证。
以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低。像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。
注:在mysql5.6以上版本引入了MVCC,在Repeatable read隔离级别下已经不存在幻读问题了。
在MySQL数据库中,支持上面四种隔离级别,默认的为Repeatable read (可重复读);而在Oracle数据库中,只支持Serializable (串行化)级别和Read committed (提交读)这两种级别,其中默认的为Read committed级别。
验证Oracle数据库的隔离级别
1、证明一个事务不会读取到另一个事务未提交的数据,即证明oracle数据库不会发生脏读
package org.cc;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;public class JDBCDemo {public static void main(String[] args) throws InterruptedException {Thread updateThread = new Thread(new UpdateAndAdd());//修改和插入数据后,1分钟后再提交数据Thread readData = new Thread(new ReadData());//读取数据,看看能否读取到上面事务未提交的数据updateThread.start();Thread.sleep(2000);//停顿2秒,保证前一个事务已经操作了数据库readData.start();}private static Connection getConn() throws ClassNotFoundException, SQLException{Class.forName("oracle.jdbc.driver.OracleDriver");return DriverManager.getConnection("jdbc:oracle:thin:@//127.0.0.1:1521/orcl", "scott", "tigger");}static class UpdateAndAdd implements Runnable {@Overridepublic void run() {Connection conn = null;Statement state = null;try {conn = getConn();conn.setAutoCommit(false);state = conn.createStatement();System.out.println("即将操作数据库!");state.executeUpdate("update emp set comm=1000 where ENAME='SMITH'");state.executeUpdate("insert into emp values('6666','lipiao','CLERK','7902',to_date('20200725','yyyymmdd'),800,1000,'20')");Thread.sleep(10000);//一分钟后再提交事务,另外一个线程看能够读取到这些数据conn.commit();} catch (SQLException | ClassNotFoundException | InterruptedException e) {e.printStackTrace();} finally {try {if (state != null) {state.close();}if (conn != null) {conn.close();}} catch (SQLException e) {e.printStackTrace();}}}}static class ReadData implements Runnable{@Overridepublic void run() {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {conn = getConn();ps = conn.prepareStatement("select * from emp where ename in(?,?)");ps.setString(1, "SMITH");ps.setString(2, "lipiao");System.out.println("读取数据");rs = ps.executeQuery();while(rs.next()){System.out.println("读取到了一行数据:comm:"+rs.getDouble("comm"));}} catch (SQLException | ClassNotFoundException e) {e.printStackTrace();} finally {try {if(rs!=null){rs.close();}if (ps != null) {ps.close();}if (conn != null) {conn.close();}} catch (SQLException e) {e.printStackTrace();}}}}
}
2 证明会发生不可重复度和幻读,意思说当前事务两次读取同一行记录数据不一致(不可重复度,其他事务做了修改,并且提交了事务),当前事务第一次和第二次读取的记录数不一致(幻读,其他事务有新增记录,并且提交了事务)
package org.cc;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;public class JDBCDemo2 {public static void main(String[] args) throws InterruptedException {Thread updateThread = new Thread(new UpdateAndAdd());//修改和插入数据Thread readData = new Thread(new ReadData());//两次读取数据readData.start();Thread.sleep(3000);updateThread.start();}private static Connection getConn() throws ClassNotFoundException, SQLException{Class.forName("oracle.jdbc.driver.OracleDriver");return DriverManager.getConnection("jdbc:oracle:thin:@//127.0.0.1:1521/orcl", "scott", "tigger");}static class UpdateAndAdd implements Runnable {@Overridepublic void run() {Connection conn = null;Statement state = null;try {conn = getConn();state = conn.createStatement();System.out.println("即将操作数据库!");state.executeUpdate("update emp set comm=2000 where ENAME='SMITH'");state.executeUpdate("insert into emp values('6667','cc','CLERK','7902',to_date('20200725','yyyymmdd'),800,1000,'20')");} catch (SQLException | ClassNotFoundException e) {e.printStackTrace();} finally {try {if (state != null) {state.close();}if (conn != null) {conn.close();}} catch (SQLException e) {e.printStackTrace();}}}}static class ReadData implements Runnable{@Overridepublic void run() {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {conn = getConn();ps = conn.prepareStatement("select * from emp where ename in(?,?)");ps.setString(1, "SMITH");ps.setString(2, "cc");System.out.println("第一次读取数据");rs = ps.executeQuery();//先查询while(rs.next()){System.out.println("读取到了一行数据:ename:"+rs.getString("ename")+",comm:"+rs.getDouble("comm"));}Thread.sleep(10000);//等另外一个事务提交System.out.println("另外一个事务提交后读取数据");rs.close();rs = ps.executeQuery();//先查询while(rs.next()){System.out.println("读取到了一行数据:ename:"+rs.getString("ename")+",comm:"+rs.getDouble("comm"));}} catch (SQLException | ClassNotFoundException | InterruptedException e) {e.printStackTrace();} finally {try {if(rs!=null){rs.close();}if (ps != null) {ps.close();}if (conn != null) {conn.close();}} catch (SQLException e) {e.printStackTrace();}}}}
}