实战分析:事务的隔离级别和传播属性(上)

什么是事务?


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


事务所具有的四种特性


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


原子性


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


一致性


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


上一篇:一篇文章学会shell工具篇之sed


下一篇:WCF技术剖析之三十二:一步步创建一个完整的分布式事务应用