Spring 事务介绍(一)之 数据库的事务的基本特性

spring.png

Spring 事务介绍(一)之 数据库的事务的基本特性

数据库的事务的基本特性

事务是区分文件存储系统和Nosql数据库重要特性之一,其存在的意义是为了保证即时在并发的情况下,也能正确的执行crud操作,怎样才能算是正确的?这时提出了事务需要保证的四个特性ACID:

  • A:原子性(atomicity)

事务中各项操作,要么全做要么不做,任何一项操作的失败都会导致整个事务的失败;

  • C:一致性(consistency)

事务结束后系统状态是一致的;

  • I:隔离性(isolation)

并发执行的事务彼此无法看到对方的中间状态;

  • D:持久性(durability)

事务完成后所做的改动都会被持久化,即使发生灾难性的失败;

在高并发的情况下,要完全保证其ACID是非常困难的,除非把所有的事务串行化执行,但是后果就是性能大打折扣。很多时候我们有些业务对事务的要求是不一样的,所有数据库中设计了四种隔离级别,供用户基于业务进行选择。

隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom read)
读未提交(Read Uncommitted) 可能 可能 可能
读已提交(Read Committed) 不可能 可能 可能
可重复读(Repeatable Read) 不可能 不可能 可能
可串行化(Serializable) 不可能 不可能 不可能
  • 脏读:

一个事务读取到另一个事务未提交的更新数据。

  • 不可重复读:

在同一事务中,多次读取同一数据返回的结果有所不同,换句话说,后面读取可以读到另一个事务已提交的更新数据,相反,“可重复读”在同一事务中多次读取数据时,能够保证所读数据一样,也就是后续读取不能读取到另一事务所提交的更新数据。

  • 幻读

查询表中一条数据如果不存在就插入一条,并发的时候却发现,里面居然有两条相同的数据,导致插入失败,这就是幻读的问题。

幻读在mysql中,在默认的可重复读的隔离级别下,由mvcc(多版本并发控制)引起的,其中间隙锁可以避免幻读的问题,但是间隙锁会引起锁等待问题。

MVCC:

MVCC是通过保存数据在某个时间点的快照来实现的. 不同存储引擎的MVCC. 不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制.

间隙锁:

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。

几种隔离级别的代码demo:

ReadUncommittedTest.java

package com.demo.spring;

import java.sql.*;

/**
 * com.demo.spring
 *
 * @author Zyy
 * @date 2019/2/13 22:55
 *
 * Connection.TRANSACTION_READ_UNCOMMITTED
 * 允许读取未提交事务,会出现脏读,不可重复读,幻读的问题
 */
public class ReadUncommittedTest {
    private static String jdbcUrl = "jdbc:mysql://192.168.5.104:3306/spring";
    private static String userName = "root";
    private static String password = "root";
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException, SQLException, ClassNotFoundException {
        Thread t1 = run(new Runnable() {
            public void run() {
                insert("001", "test", 100);
            }
        });

        Thread t2 = run(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(500);
                    Connection conn = openConnection();
                    // 将参数升级成 Connection.TRANSACTION_READ_COMMITTED 即可解决脏读的问题
                    conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
                    select("test", conn);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        t1.join();

    }

    public static Thread run(Runnable runnable) {
        Thread thread = new Thread(runnable);
        thread.start();
        return thread;
    }

    public static Connection openConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection(jdbcUrl, userName, password);
        return conn;
    }

    static {
        try {
            Connection connection = openConnection();
            deleteAccount(connection);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void insert(String accountName, String name, int money) {
        try {
            Connection conn = openConnection();
            PreparedStatement prepare = conn.
                    prepareStatement("insert into account (accountname,user,money) values (?,?,?)");
            prepare.setString(1, accountName);
            prepare.setString(2, name);
            prepare.setInt(3, money);
            prepare.executeUpdate();
            System.out.println("执行插入成功");
            conn.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void select(String name, Connection conn) {
        try {
            PreparedStatement prepare = conn.
                    prepareStatement("select * from account where user = ?");
            prepare.setString(1, name);
            ResultSet resultSet = prepare.executeQuery();
            System.out.println("执行查询");
            while (resultSet.next()) {
                for (int i = 1; i <= 4; i++) {
                    System.out.print(resultSet.getString(i) + " ");
                }
                System.out.println();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void deleteAccount(Connection conn) {
        try {
            PreparedStatement prepare = conn.prepareStatement("delete from account");
            prepare.executeUpdate();
            System.out.println("执行删除");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

执行插入成功
执行查询
141 001 test 100 

出现脏读问题,读取到未提交的插入数据。

ReadCommittedTest.java

package com.demo.spring;

import java.sql.*;

/**
 * com.demo.spring
 *
 * @author Zyy
 * @date 2019/2/13 22:32
 *
 * Connection.TRANSACTION_READ_COMMITTED
 * 允许读取已提交事务,会出现不可重复读,幻读的问题
 */
public class ReadCommittedTest {
    private static String jdbcUrl = "jdbc:mysql://192.168.5.104:3306/spring";
    private static String userName = "root";
    private static String password = "root";
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = run(new Runnable() {
            public void run() {
                synchronized (lock) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    insert("001", "test", 100);
                }
            }
        });

        Thread t2 = run(new Runnable() {
            public void run() {
                try {
                    Connection connection = openConnection();
                    connection.setAutoCommit(false);
                    // 将参数升级成 Connection.TRANSACTION_REPEATABLE_READ 即可解决不可重复读问题
                    connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
                    // 第一次读取不到
                    select("test", connection);
                    // 释放锁
                    synchronized (lock) {
                        lock.notify();
                    }
                    // 第二次读取到(数据不一至)
                    Thread.sleep(500);
                    select("test", connection);
                    connection.close();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (SQLException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.join();
        t2.join();

    }

    public static Thread run(Runnable runnable) {
        Thread thread = new Thread(runnable);
        thread.start();
        return thread;
    }

    public static Connection openConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection(jdbcUrl, userName, password);
        return conn;
    }

    static {
        try {
            Connection connection = openConnection();
            //deleteAccount(connection);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void insert(String accountName, String name, int money) {
        try {
            Connection conn = openConnection();
            PreparedStatement prepare = conn.
                    prepareStatement("insert into account (accountname,user,money) values (?,?,?)");
            prepare.setString(1, accountName);
            prepare.setString(2, name);
            prepare.setInt(3, money);
            prepare.executeUpdate();
            System.out.println("执行插入成功");
            conn.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void select(String name, Connection conn) {
        try {
            PreparedStatement prepare = conn.
                    prepareStatement("select * from account where user = ?");
            prepare.setString(1, name);
            ResultSet resultSet = prepare.executeQuery();
            System.out.println("执行查询");
            while (resultSet.next()) {
                for (int i = 1; i <= 4; i++) {
                    System.out.print(resultSet.getString(i) + " ");
                }
                System.out.println();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void deleteAccount(Connection conn) {
        try {
            PreparedStatement prepare = conn.prepareStatement("delete from account");
            prepare.executeUpdate();
            System.out.println("执行删除");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

执行结果

执行查询
141 001 test 100 
142 001 test 100 
143 001 test 100 
执行插入成功
执行查询
141 001 test 100 
142 001 test 100 
143 001 test 100 
144 001 test 100 

出现不可重复读的问题,两次读取结果不一致。

ReadRepeatableTest.java

package com.demo.spring;

import java.sql.*;

/**
 * com.demo.spring
 *
 * @author Zyy
 * @date 2019/2/13 23:15
 *
 * Connection.TRANSACTION_REPEATABLE_READ
 * 可重复读 ,在一个事务中同一SQL语句 无论执行多少次都会得到相同的结果
 * 会出现幻读的问题
 */
public class ReadRepeatableTest {

    private static String jdbcUrl = "jdbc:mysql://192.168.5.104:3306/spring";
    private static String userName = "root";
    private static String password = "root";
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException, SQLException, ClassNotFoundException {
        Thread t1 = run(new Runnable() {
            public void run() {
                try {
                    synchronized (lock) {
                        lock.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                update("test");
            }
        });

        Thread t2 = run(new Runnable() {
            public void run() {
                try {
                    Connection conn = openConnection();
                    conn.setAutoCommit(false);
                    conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
                    // 第一次读取 读取到的数据为未修改前的数据
                    select("test", conn);
                    // 释放锁
                    synchronized (lock) {
                        lock.notify();
                    }
                    // 第二次读取 TRANSACTION_REPEATABLE_READ级别,读取到的数据也为未修改前的数据 两次读取数据一至
                    // 设置id为主键 如果此时t1做插入(id=1),t2按主键查询(id=1)
                    // 因为此时为TRANSACTION_REPEATABLE_READ级别 ,所以查询为空,然后进行插入(id=1)
                    // 此时会出现主键冲突的异常,这种情况为幻读,有兴趣的可以尝试一下
                    Thread.sleep(500);
                    select("test", conn);
                    conn.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        t1.join();

    }

    public static Thread run(Runnable runnable) {
        Thread thread = new Thread(runnable);
        thread.start();
        return thread;
    }

    public static Connection openConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection(jdbcUrl, userName, password);
        return conn;
    }

    static {
        try {
            Connection connection = openConnection();
            //deleteAccount(connection);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void insert(String accountName, String name, int money) {
        try {
            Connection conn = openConnection();
            PreparedStatement prepare = conn.
                    prepareStatement("insert into account (accountname,user,money) values (?,?,?)");
            prepare.setString(1, accountName);
            prepare.setString(2, name);
            prepare.setInt(3, money);
            prepare.executeUpdate();
            System.out.println("执行插入成功");
            conn.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void deleteAccount(Connection conn) {
        try {
            PreparedStatement prepare = conn.prepareStatement("delete from account");
            prepare.executeUpdate();
            System.out.println("执行删除成功");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void update(String user) {
        try {
            Connection conn = openConnection();
            PreparedStatement prepare = conn.
                    prepareStatement("update account set money = money + 1 where user = ?");
            prepare.setString(1, user);
            prepare.executeUpdate();
            conn.close();
            System.out.println("执行修改成功");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void select(String name, Connection conn) {
        try {
            PreparedStatement prepare = conn.
                    prepareStatement("select * from account where user = ?");
            prepare.setString(1, name);
            ResultSet resultSet = prepare.executeQuery();
            System.out.println("执行查询");
            while (resultSet.next()) {
                for (int i = 1; i <= 4; i++) {
                    System.out.print(resultSet.getString(i) + " ");
                }
                System.out.println();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

执行查询
141 001 test 100 
142 001 test 100 
143 001 test 100 
144 001 test 100 
执行修改成功
执行查询
141 001 test 100 
142 001 test 100 
143 001 test 100 
144 001 test 100 

两次查询结果一致,已解决了不可重复读的问题,可是会出现幻读的问题。

幻读场景描述:

设置id为主键,在两个同时进行的事务中,如果此时事务t1做插入(id=1),事务t2按主键查询(id=1)因为此时为TRANSACTION_REPEATABLE_READ级别 ,所以查询为空,然后进行插入(id=1)
此时会出现主键冲突的异常,这种情况主要是由MVCC导致的,t2查询的数据因为没有改动所以是之前保留的查询数据,为快照版本,但实际上数据库已经新增了一条,此时进行插入,就抛出主键冲突异常了,明明查询没有数据然后进行插入,可是会出现插入失败的情况,这种场景就是幻读。

数据库默认隔离级别:

Oracle:读已提交(Read Committed)

Mysql:可重复读(Repeatable Read)

另外,mysql执行一条查询语句默认是一个独立的事务,所以看上去效果与读已提交一样。

Mysql

查看当前会话隔离级别

select @@tx_isolation;

查看系统当前隔离级别

select @@global.tx_isolation;

设置当前会话隔离级别

set session transaction isolatin level repeatable read;

设置系统当前隔离级别

set global transaction isolation level repeatable read;

Oracle

查看系统默认事务隔离级别,也是当前会话隔离级别

#首先创建一个事务

declare
     trans_id Varchar2(100);
  begin
     trans_id := dbms_transaction.local_transaction_id( TRUE );
  end; 
  
#查看事务隔离级别

SELECT s.sid, s.serial#,

  CASE BITAND(t.flag, POWER(2, 28))
    WHEN 0 THEN 'READ COMMITTED'
    ELSE 'SERIALIZABLE'
  END AS isolation_level
FROM v$transaction t
JOIN v$session s ON t.addr = s.taddr AND s.sid = sys_context('USERENV', 'SID');

github : https://github.com/zhaoyybalabala/spring-test


欢迎留言交流:)

推荐阅读更多精彩内容