事务的隔离级别和传播属性(上)

简介: 事务的隔离级别和传播属性(上)

什么是事务?


要么全部都要执行,要么就都不执行。


事务所具有的四种特性


原子性,一致性,隔离性,持久性


原子性


个人理解,就是事务执行不可分割,要么全部完成,要么全部拉倒不干。


一致性


关于一致性这个概念我们来举个例子说明吧,假设张三给李四转了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();
    }
}
复制代码


在设置了TRANSACTION_READ_COMMITTED隔离级别的情况下,上述程序的运行结果为:


网络异常,图片无法展示
|


为了避免这种情况的发生,需要保证在同一个事务里面,多次重复读取的数据都是一致的,因此需要将事务的隔离级别从TRANSACTION_READ_COMMITTED提升到TRANSACTION_REPEATABLE_READ级别,这种情况下,上述程序的运行结果为:


网络异常,图片无法展示
|


幻读


官方文档对于幻读的定义如下:


The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.


读到上一次没有返回的记录,看起来是幻影一般。


幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。为了解决这种情况,可以选择将事务的隔离级别提升到TRANSACTION_SERIALIZABLE。

目录
相关文章
|
6月前
|
存储 Java 数据库
Spring事务和事务传播机制
Spring事务和事务传播机制
53 0
|
4月前
|
Java 关系型数据库 MySQL
Spring事务和事务传播机制(1)
Spring事务和事务传播机制(1)
45 0
Spring事务和事务传播机制(1)
|
4月前
|
SQL Java 关系型数据库
Spring事务和事务传播机制(2)
Spring事务和事务传播机制(2)
36 0
Spring事务和事务传播机制(2)
|
5月前
|
关系型数据库 Java MySQL
数据库事务特性、传播行为和隔离级别总结
数据库事务特性、传播行为和隔离级别总结
51 1
|
6月前
|
关系型数据库 MySQL 数据库
事务的基本特性和隔离级别
事务的基本特性和隔离级别
25 0
|
8月前
声明式事务-- 事务的传播机制-- 事务传播机制种类
声明式事务-- 事务的传播机制-- 事务传播机制种类
43 0
|
10月前
|
XML Java 数据库
详解事务的7种传播行为
什么是Spring事务的传播行为?事务传播行为的七种类型有哪些?将异常try-catch捕获,事务是否还会回滚?
192 0
详解事务的7种传播行为
|
12月前
|
Java 关系型数据库 MySQL
Spring事务(事务的实现、隔离级别、传播机制)(上)
Spring事务(事务的实现、隔离级别、传播机制)(上)
107 0
|
12月前
|
Java 关系型数据库 MySQL
Spring事务(事务的实现、隔离级别、传播机制)(下)
Spring事务(事务的实现、隔离级别、传播机制)(下)
126 0
|
Oracle 安全 Java
Spring事务传播属性和隔离级别
Spring事务传播属性和隔离级别 🍅程序员小王的博客:程序员小王的博客 🍅 欢迎点赞 👍 收藏 ⭐留言 📝 🍅 如有编辑错误联系作者,如果有比较好的文章欢迎分享给我,我会取其精华去其糟粕 🍅java自学的学习路线:java自学的学习路线
139 0
Spring事务传播属性和隔离级别