
Spring 事務(wù)介紹(一)之 數(shù)據(jù)庫的事務(wù)的基本特性
數(shù)據(jù)庫的事務(wù)的基本特性
事務(wù)是區(qū)分文件存儲系統(tǒng)和Nosql數(shù)據(jù)庫重要特性之一,其存在的意義是為了保證即時在并發(fā)的情況下,也能正確的執(zhí)行crud操作,怎樣才能算是正確的?這時提出了事務(wù)需要保證的四個特性ACID:
- A:原子性(atomicity)
事務(wù)中各項操作,要么全做要么不做,任何一項操作的失敗都會導(dǎo)致整個事務(wù)的失??;
- C:一致性(consistency)
事務(wù)結(jié)束后系統(tǒng)狀態(tài)是一致的;
- I:隔離性(isolation)
并發(fā)執(zhí)行的事務(wù)彼此無法看到對方的中間狀態(tài);
- D:持久性(durability)
事務(wù)完成后所做的改動都會被持久化,即使發(fā)生災(zāi)難性的失??;
在高并發(fā)的情況下,要完全保證其ACID是非常困難的,除非把所有的事務(wù)串行化執(zhí)行,但是后果就是性能大打折扣。很多時候我們有些業(yè)務(wù)對事務(wù)的要求是不一樣的,所有數(shù)據(jù)庫中設(shè)計了四種隔離級別,供用戶基于業(yè)務(wù)進行選擇。
| 隔離級別 | 臟讀(Dirty Read) | 不可重復(fù)讀(NonRepeatable Read) | 幻讀(Phantom read) |
|---|---|---|---|
| 讀未提交(Read Uncommitted) | 可能 | 可能 | 可能 |
| 讀已提交(Read Committed) | 不可能 | 可能 | 可能 |
| 可重復(fù)讀(Repeatable Read) | 不可能 | 不可能 | 可能 |
| 可串行化(Serializable) | 不可能 | 不可能 | 不可能 |
- 臟讀:
一個事務(wù)讀取到另一個事務(wù)未提交的更新數(shù)據(jù)。
- 不可重復(fù)讀:
在同一事務(wù)中,多次讀取同一數(shù)據(jù)返回的結(jié)果有所不同,換句話說,后面讀取可以讀到另一個事務(wù)已提交的更新數(shù)據(jù),相反,“可重復(fù)讀”在同一事務(wù)中多次讀取數(shù)據(jù)時,能夠保證所讀數(shù)據(jù)一樣,也就是后續(xù)讀取不能讀取到另一事務(wù)所提交的更新數(shù)據(jù)。
- 幻讀
查詢表中一條數(shù)據(jù)如果不存在就插入一條,并發(fā)的時候卻發(fā)現(xiàn),里面居然有兩條相同的數(shù)據(jù),導(dǎo)致插入失敗,這就是幻讀的問題。
幻讀在mysql中,在默認的可重復(fù)讀的隔離級別下,由mvcc(多版本并發(fā)控制)引起的,其中間隙鎖可以避免幻讀的問題,但是間隙鎖會引起鎖等待問題。
MVCC:
MVCC是通過保存數(shù)據(jù)在某個時間點的快照來實現(xiàn)的. 不同存儲引擎的MVCC. 不同存儲引擎的MVCC實現(xiàn)是不同的,典型的有樂觀并發(fā)控制和悲觀并發(fā)控制.
間隙鎖:
當(dāng)我們用范圍條件而不是相等條件檢索數(shù)據(jù),并請求共享或排他鎖時,InnoDB會給符合條件的已有數(shù)據(jù)記錄的索引項加鎖;對于鍵值在條件范圍內(nèi)但并不存在的記錄,叫做“間隙(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
* 允許讀取未提交事務(wù),會出現(xiàn)臟讀,不可重復(fù)讀,幻讀的問題
*/
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();
// 將參數(shù)升級成 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("執(zhí)行插入成功");
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("執(zhí)行查詢");
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("執(zhí)行刪除");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
執(zhí)行結(jié)果:
執(zhí)行插入成功
執(zhí)行查詢
141 001 test 100
出現(xiàn)臟讀問題,讀取到未提交的插入數(shù)據(jù)。
ReadCommittedTest.java
package com.demo.spring;
import java.sql.*;
/**
* com.demo.spring
*
* @author Zyy
* @date 2019/2/13 22:32
*
* Connection.TRANSACTION_READ_COMMITTED
* 允許讀取已提交事務(wù),會出現(xiàn)不可重復(fù)讀,幻讀的問題
*/
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);
// 將參數(shù)升級成 Connection.TRANSACTION_REPEATABLE_READ 即可解決不可重復(fù)讀問題
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// 第一次讀取不到
select("test", connection);
// 釋放鎖
synchronized (lock) {
lock.notify();
}
// 第二次讀取到(數(shù)據(jù)不一至)
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("執(zhí)行插入成功");
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("執(zhí)行查詢");
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("執(zhí)行刪除");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
執(zhí)行結(jié)果
執(zhí)行查詢
141 001 test 100
142 001 test 100
143 001 test 100
執(zhí)行插入成功
執(zhí)行查詢
141 001 test 100
142 001 test 100
143 001 test 100
144 001 test 100
出現(xiàn)不可重復(fù)讀的問題,兩次讀取結(jié)果不一致。
ReadRepeatableTest.java
package com.demo.spring;
import java.sql.*;
/**
* com.demo.spring
*
* @author Zyy
* @date 2019/2/13 23:15
*
* Connection.TRANSACTION_REPEATABLE_READ
* 可重復(fù)讀 ,在一個事務(wù)中同一SQL語句 無論執(zhí)行多少次都會得到相同的結(jié)果
* 會出現(xiàn)幻讀的問題
*/
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);
// 第一次讀取 讀取到的數(shù)據(jù)為未修改前的數(shù)據(jù)
select("test", conn);
// 釋放鎖
synchronized (lock) {
lock.notify();
}
// 第二次讀取 TRANSACTION_REPEATABLE_READ級別,讀取到的數(shù)據(jù)也為未修改前的數(shù)據(jù) 兩次讀取數(shù)據(jù)一至
// 設(shè)置id為主鍵 如果此時t1做插入(id=1),t2按主鍵查詢(id=1)
// 因為此時為TRANSACTION_REPEATABLE_READ級別 ,所以查詢?yōu)榭眨缓筮M行插入(id=1)
// 此時會出現(xiàn)主鍵沖突的異常,這種情況為幻讀,有興趣的可以嘗試一下
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("執(zhí)行插入成功");
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("執(zhí)行刪除成功");
} 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("執(zhí)行修改成功");
} 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("執(zhí)行查詢");
while (resultSet.next()) {
for (int i = 1; i <= 4; i++) {
System.out.print(resultSet.getString(i) + " ");
}
System.out.println();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
執(zhí)行結(jié)果:
執(zhí)行查詢
141 001 test 100
142 001 test 100
143 001 test 100
144 001 test 100
執(zhí)行修改成功
執(zhí)行查詢
141 001 test 100
142 001 test 100
143 001 test 100
144 001 test 100
兩次查詢結(jié)果一致,已解決了不可重復(fù)讀的問題,可是會出現(xiàn)幻讀的問題。
幻讀場景描述:
設(shè)置id為主鍵,在兩個同時進行的事務(wù)中,如果此時事務(wù)t1做插入(id=1),事務(wù)t2按主鍵查詢(id=1)因為此時為TRANSACTION_REPEATABLE_READ級別 ,所以查詢?yōu)榭?,然后進行插入(id=1)
此時會出現(xiàn)主鍵沖突的異常,這種情況主要是由MVCC導(dǎo)致的,t2查詢的數(shù)據(jù)因為沒有改動所以是之前保留的查詢數(shù)據(jù),為快照版本,但實際上數(shù)據(jù)庫已經(jīng)新增了一條,此時進行插入,就拋出主鍵沖突異常了,明明查詢沒有數(shù)據(jù)然后進行插入,可是會出現(xiàn)插入失敗的情況,這種場景就是幻讀。
數(shù)據(jù)庫默認隔離級別:
Oracle:讀已提交(Read Committed)
Mysql:可重復(fù)讀(Repeatable Read)
另外,mysql執(zhí)行一條查詢語句默認是一個獨立的事務(wù),所以看上去效果與讀已提交一樣。
Mysql:
查看當(dāng)前會話隔離級別
select @@tx_isolation;
查看系統(tǒng)當(dāng)前隔離級別
select @@global.tx_isolation;
設(shè)置當(dāng)前會話隔離級別
set session transaction isolatin level repeatable read;
設(shè)置系統(tǒng)當(dāng)前隔離級別
set global transaction isolation level repeatable read;
Oracle
查看系統(tǒng)默認事務(wù)隔離級別,也是當(dāng)前會話隔離級別
#首先創(chuàng)建一個事務(wù)
declare
trans_id Varchar2(100);
begin
trans_id := dbms_transaction.local_transaction_id( TRUE );
end;
#查看事務(wù)隔離級別
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
歡迎留言交流:)