Spring 事務(wù)介紹(一)之 數(shù)據(jù)庫的事務(wù)的基本特性
數(shù)據(jù)庫的事務(wù)的基本特性
事務(wù)是區(qū)分文件存儲(chǔ)系統(tǒng)和Nosql數(shù)據(jù)庫重要特性之一栈顷,其存在的意義是為了保證即時(shí)在并發(fā)的情況下往堡,也能正確的執(zhí)行crud操作咐蝇,怎樣才能算是正確的爆侣?這時(shí)提出了事務(wù)需要保證的四個(gè)特性ACID:
- A:原子性(atomicity)
事務(wù)中各項(xiàng)操作只损,要么全做要么不做搔涝,任何一項(xiàng)操作的失敗都會(huì)導(dǎo)致整個(gè)事務(wù)的失斈卦睢;
- C:一致性(consistency)
事務(wù)結(jié)束后系統(tǒng)狀態(tài)是一致的见芹;
- I:隔離性(isolation)
并發(fā)執(zhí)行的事務(wù)彼此無法看到對(duì)方的中間狀態(tài)剂娄;
- D:持久性(durability)
事務(wù)完成后所做的改動(dòng)都會(huì)被持久化,即使發(fā)生災(zāi)難性的失斝骸阅懦;
在高并發(fā)的情況下,要完全保證其ACID是非常困難的徘铝,除非把所有的事務(wù)串行化執(zhí)行耳胎,但是后果就是性能大打折扣。很多時(shí)候我們有些業(yè)務(wù)對(duì)事務(wù)的要求是不一樣的惕它,所有數(shù)據(jù)庫中設(shè)計(jì)了四種隔離級(jí)別怕午,供用戶基于業(yè)務(wù)進(jìn)行選擇。
隔離級(jí)別 | 臟讀(Dirty Read) | 不可重復(fù)讀(NonRepeatable Read) | 幻讀(Phantom read) |
---|---|---|---|
讀未提交(Read Uncommitted) | 可能 | 可能 | 可能 |
讀已提交(Read Committed) | 不可能 | 可能 | 可能 |
可重復(fù)讀(Repeatable Read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable) | 不可能 | 不可能 | 不可能 |
- 臟讀:
一個(gè)事務(wù)讀取到另一個(gè)事務(wù)未提交的更新數(shù)據(jù)淹魄。
- 不可重復(fù)讀:
在同一事務(wù)中郁惜,多次讀取同一數(shù)據(jù)返回的結(jié)果有所不同,換句話說甲锡,后面讀取可以讀到另一個(gè)事務(wù)已提交的更新數(shù)據(jù)兆蕉,相反羽戒,“可重復(fù)讀”在同一事務(wù)中多次讀取數(shù)據(jù)時(shí),能夠保證所讀數(shù)據(jù)一樣虎韵,也就是后續(xù)讀取不能讀取到另一事務(wù)所提交的更新數(shù)據(jù)易稠。
- 幻讀
查詢表中一條數(shù)據(jù)如果不存在就插入一條,并發(fā)的時(shí)候卻發(fā)現(xiàn)包蓝,里面居然有兩條相同的數(shù)據(jù)驶社,導(dǎo)致插入失敗,這就是幻讀的問題养晋。
幻讀在mysql中衬吆,在默認(rèn)的可重復(fù)讀的隔離級(jí)別下梁钾,由mvcc(多版本并發(fā)控制)引起的绳泉,其中間隙鎖可以避免幻讀的問題,但是間隙鎖會(huì)引起鎖等待問題姆泻。
MVCC:
MVCC是通過保存數(shù)據(jù)在某個(gè)時(shí)間點(diǎn)的快照來實(shí)現(xiàn)的. 不同存儲(chǔ)引擎的MVCC. 不同存儲(chǔ)引擎的MVCC實(shí)現(xiàn)是不同的,典型的有樂觀并發(fā)控制和悲觀并發(fā)控制.
間隙鎖:
當(dāng)我們用范圍條件而不是相等條件檢索數(shù)據(jù)零酪,并請(qǐng)求共享或排他鎖時(shí),InnoDB會(huì)給符合條件的已有數(shù)據(jù)記錄的索引項(xiàng)加鎖拇勃;對(duì)于鍵值在條件范圍內(nèi)但并不存在的記錄四苇,叫做“間隙(GAP)”,InnoDB也會(huì)對(duì)這個(gè)“間隙”加鎖方咆,這種鎖機(jī)制就是所謂的間隙鎖(Next-Key鎖)月腋。
幾種隔離級(jí)別的代碼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ù),會(huì)出現(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ù)升級(jí)成 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ù)妓肢,會(huì)出現(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ù)升級(jí)成 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ù)讀 ,在一個(gè)事務(wù)中同一SQL語句 無論執(zhí)行多少次都會(huì)得到相同的結(jié)果
* 會(huì)出現(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級(jí)別,讀取到的數(shù)據(jù)也為未修改前的數(shù)據(jù) 兩次讀取數(shù)據(jù)一至
// 設(shè)置id為主鍵 如果此時(shí)t1做插入(id=1)卷拘,t2按主鍵查詢(id=1)
// 因?yàn)榇藭r(shí)為TRANSACTION_REPEATABLE_READ級(jí)別 喊废,所以查詢?yōu)榭眨缓筮M(jìn)行插入(id=1)
// 此時(shí)會(huì)出現(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ù)讀的問題横腿,可是會(huì)出現(xiàn)幻讀的問題颓屑。
幻讀場景描述:
設(shè)置id為主鍵斤寂,在兩個(gè)同時(shí)進(jìn)行的事務(wù)中,如果此時(shí)事務(wù)t1做插入(id=1)揪惦,事務(wù)t2按主鍵查詢(id=1)因?yàn)榇藭r(shí)為TRANSACTION_REPEATABLE_READ級(jí)別 遍搞,所以查詢?yōu)榭眨缓筮M(jìn)行插入(id=1)
此時(shí)會(huì)出現(xiàn)主鍵沖突的異常器腋,這種情況主要是由MVCC導(dǎo)致的溪猿,t2查詢的數(shù)據(jù)因?yàn)闆]有改動(dòng)所以是之前保留的查詢數(shù)據(jù),為快照版本纫塌,但實(shí)際上數(shù)據(jù)庫已經(jīng)新增了一條诊县,此時(shí)進(jìn)行插入,就拋出主鍵沖突異常了措左,明明查詢沒有數(shù)據(jù)然后進(jìn)行插入依痊,可是會(huì)出現(xiàn)插入失敗的情況,這種場景就是幻讀怎披。
數(shù)據(jù)庫默認(rèn)隔離級(jí)別:
Oracle:讀已提交(Read Committed)
Mysql:可重復(fù)讀(Repeatable Read)
另外胸嘁,mysql執(zhí)行一條查詢語句默認(rèn)是一個(gè)獨(dú)立的事務(wù),所以看上去效果與讀已提交一樣凉逛。
Mysql:
查看當(dāng)前會(huì)話隔離級(jí)別
select @@tx_isolation;
查看系統(tǒng)當(dāng)前隔離級(jí)別
select @@global.tx_isolation;
設(shè)置當(dāng)前會(huì)話隔離級(jí)別
set session transaction isolatin level repeatable read;
設(shè)置系統(tǒng)當(dāng)前隔離級(jí)別
set global transaction isolation level repeatable read;
Oracle
查看系統(tǒng)默認(rèn)事務(wù)隔離級(jí)別,也是當(dāng)前會(huì)話隔離級(jí)別
#首先創(chuàng)建一個(gè)事務(wù)
declare
trans_id Varchar2(100);
begin
trans_id := dbms_transaction.local_transaction_id( TRUE );
end;
#查看事務(wù)隔離級(jí)別
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
歡迎留言交流:)