什么是事务?
要么全部都要执行,要么就都不执行。
事务所具有的四种特性
原子性,一致性,隔离性,持久性
原子性
个人理解,就是事务执行不可分割,要么全部完成,要么全部拉倒不干。
一致性
关于一致性这个概念我们来举个例子说明吧,假设张三给李四转了100元,那么需要先从张三那边扣除100,然后李四那边增加100,这个转账的过程对于其他事务而言是无法看到的,这种状态始终都在保持一致,这个过程我们称之为一致性。
隔离性
并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据是独立的;
持久性
一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
为什么会出现事务的隔离级别?
我们都知道,数据库都是有相应的事物隔离级别。之所以需要分成不同级别的事务,这个是因为在并发的场景下,读取数据可能会有出现脏读,不可重复读以及幻读的情况,因此需要设置相应的事物隔离级别。
为了方便理解,我们将使用java程序代码来演示并发读取数据时候会产生的相应场景:
环境准备:
- jdk8
- mysql数据
建立测试使用表:
CREATE TABLE `money` ( `id` int(11) NOT NULL AUTO_INCREMENT, `money` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
一个方便于操作mysql的简单JdbcUtil工具类:
import java.io.IOException; import java.sql.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Properties; /** * Jdbc操作数据库工具类 * * @author idea * @version 1.0 */ public class JdbcUtil { public static final String DRIVER; public static final String URL; public static final String USERNAME; public static final String PASSWORD; private static Properties prop = null; private static PreparedStatement ps = null; /** * 加载配置文件中的信息 */ static { prop = new Properties(); try { prop.load(JdbcUtil.class.getClassLoader().getResourceAsStream("db.properties")); } catch (IOException e) { e.printStackTrace(); } DRIVER = prop.getProperty("driver"); URL = prop.getProperty("url"); USERNAME = prop.getProperty("username"); PASSWORD = prop.getProperty("password"); } /** * 获取连接 * * @return void * @author blindeagle */ public static Connection getConnection() { try { Class.forName(DRIVER); Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); return conn; } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } return null; } /** * 数据转换为list类型 * * @param rs * @return * @throws SQLException */ public static List convertList(ResultSet rs) throws SQLException { List list = new ArrayList(); //获取键名 ResultSetMetaData md = rs.getMetaData(); //获取行的数量 int columnCount = md.getColumnCount(); while (rs.next()) { //声明Map HashMap<String,Object> rowData = new HashMap(); for (int i = 1; i <= columnCount; i++) { //获取键名及值 rowData.put(md.getColumnName(i), rs.getObject(i)); } list.add(rowData); } return list; } }
脏读
所谓的脏读是指读取到没有提交的数据信息。
模拟场景:两个线程a,b同时访问数据库进行操作,a线程需要插入数据到库里面,但是没有提交事务,这个时候b线程需要读取数据库的信息,将a里面所要插入的数据(但是没有提交)给读取了进来,造成了脏读现象。
代码如下所示:
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; /** * @author idea * @date 2019/7/2 * @Version V1.0 */ public class DirtyReadDemo { public static final String READ_SQL = "SELECT * FROM money"; public static final String WRITE_SQL = "INSERT INTO `money` (`id`, `money`) VALUES ('3', '350')"; public Object lock = new Object(); /** * 脏读模拟(注意:需要设置表的存储引擎为innodb类型) */ public static void dirtyRead() { try { Connection conn = JdbcUtil.getConnection(); conn.setAutoCommit(false); PreparedStatement writePs = conn.prepareStatement(WRITE_SQL); writePs.executeUpdate(); System.out.println("执行写取数据操作----"); Thread.sleep(500); //需要保证连接不同 Connection readConn = JdbcUtil.getConnection(); //注意这里面需要保证提交的事物等级为:未提交读 readConn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); PreparedStatement readPs = readConn.prepareStatement(READ_SQL); ResultSet rs = readPs.executeQuery(); System.out.println("执行读取数据操作----"); List list = JdbcUtil.convertList(rs); for (Object o : list) { System.out.println(o); } readConn.close(); } catch (SQLException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { dirtyRead(); } }
由于这个案例里面的事物隔离级别知识设置在了TRANSACTION_READ_UNCOMMITTED层级,因此对于没有提交事务的数据也会被读取进来。造成了脏数据读取的情况。
因此程序运行之后的结果如下:
为了预防脏读的情况发生,我们通常需要提升事务的隔离级别,从原先的TRANSACTION_READ_UNCOMMITTED提升到TRANSACTION_READ_COMMITTED,这个时候我们再来运行一下程序,会发现原先有的脏数据读取消失了:
不可重复读
所谓的不可重复读,我的理解是,多个线程a,b同时读取数据库里面的数据,a线程负责插入数据,b线程负责写入数据,b线程里面有两次读取数据库的操作,分别是select1和select2,由于事务的隔离级别设置在了TRANSACTION_READ_COMMITTED,所以当select1执行了之后,a线程插入了新的数据,再去执行select2操作的时候会读取出新的数据信息,导致出现了不可重复读问题。
演示代码:
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; /** * 不可重复读案例 * @author idea * @date 2019/7/2 * @Version V1.0 */ public class NotRepeatReadDemo { public static final String READ_SQL = "SELECT * FROM money"; public static final String WRITE_SQL = "INSERT INTO `money` (`id`, `money`) VALUES ('3', '350')"; public Object lock = new Object(); /** * 不可重复读模拟 */ public void notRepeatRead() { Thread writeThread = new Thread(new Runnable() { @Override public void run() { try (Connection conn = JdbcUtil.getConnection();) { //堵塞等待唤醒 synchronized (lock) { lock.wait(); } conn.setAutoCommit(true); PreparedStatement ps = conn.prepareStatement(WRITE_SQL); ps.executeUpdate(); System.out.println("执行写取数据操作----"); ps.close(); } catch (SQLException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread readThread = new Thread(new Runnable() { @Override public void run() { try { Connection readConn = JdbcUtil.getConnection(); readConn.setAutoCommit(false); readConn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); PreparedStatement readPs = readConn.prepareStatement(READ_SQL); ResultSet rs = readPs.executeQuery(); System.out.println("执行读取数据操作1----"); List list = JdbcUtil.convertList(rs); for (Object obj : list) { System.out.println(obj); } synchronized (lock){ lock.notify(); } Thread.sleep(1000); ResultSet rs2 = readPs.executeQuery(); System.out.println("执行读取数据操作2----"); List list2 = JdbcUtil.convertList(rs2); for (Object obj : list2) { System.out.println(obj); } readConn.commit(); readConn.close(); } catch (SQLException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }); writeThread.start(); readThread.start(); } public static void main(String[] args) { NotRepeatReadDemo notRepeatReadDemo=new NotRepeatReadDemo(); notRepeatReadDemo.notRepeatRead(); } }