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
歡迎留言交流:)