什么是事務?
要么全部都要執行,要么就都不執行。
事務所具有的四種特性
原子性,一致性,隔離性,持久性
原子性
個人理解,就是事務執行不可分割,要么全部完成,要么全部拉倒不干。
一致性
關於一致性這個概念我們來舉個例子說明吧,假設張三給李四轉了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(); } }
在設置了TRANSACTION_READ_COMMITTED隔離級別的情況下,上述程序的運行結果為:
為了避免這種情況的發生,需要保證在同一個事務里面,多次重復讀取的數據都是一致的,因此需要將事務的隔離級別從TRANSACTION_READ_COMMITTED提升到TRANSACTION_REPEATABLE_READ級別,這種情況下,上述程序的運行結果為:
幻讀
官方文檔對於幻讀的定義如下:
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
讀到上一次沒有返回的記錄,看起來是幻影一般。
幻讀與不可重復讀類似。它發生在一個事務(T1)讀取了幾行數據,接着另一個並發事務(T2)插入了一些數據時。在隨后的查詢中,第一個事務(T1)就會發現多了一些原本不存在的記錄,就好像發生了幻覺一樣,所以稱為幻讀。為了解決這種情況,可以選擇將事務的隔離級別提升到TRANSACTION_SERIALIZABLE。
什么是TRANSACTION_SERIALIZABLE?
TRANSACTION_SERIALIZABLE是當前事務隔離級別中最高等級的設置,可以完全服從ACID的規則,通過加入行鎖的方式(innodb存儲引擎中)來防止出現數據並發導致的數據不一致性問題。為了方便理解,可以看看下方的程序:
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.concurrent.CountDownLatch; /** * @author idea * @date 2019/7/2 * @Version V1.0 */ public class FantasyReadDemo { public static final String READ_SQL = "SELECT * FROM money"; public static final String UPDATE_SQL = "UPDATE `money` SET `money` = ? WHERE `id` = 3;n"; public CountDownLatch countDownLatch=new CountDownLatch(2); public void readAndUpdate1() { try (Connection conn = JdbcUtil.getConnection();) { conn.setAutoCommit(false); PreparedStatement ps = conn.prepareStatement(READ_SQL); conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); ResultSet rs = ps.executeQuery(); rs.next(); int currentMoney = (int) rs.getObject(2); System.out.println("執行寫取數據操作----" + currentMoney); //堵塞等待喚醒 countDownLatch.countDown(); PreparedStatement writePs = conn.prepareStatement(UPDATE_SQL); writePs.setInt(1, currentMoney - 1); writePs.execute(); conn.commit(); writePs.close(); ps.close(); System.out.println("執行寫操作結束---1"); } catch (Exception e) { e.printStackTrace(); readAndUpdate1(); } } public void readAndUpdate2() { try (Connection conn = JdbcUtil.getConnection();) { conn.setAutoCommit(false); PreparedStatement ps = conn.prepareStatement(READ_SQL); conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); ResultSet rs = ps.executeQuery(); rs.next(); int currentMoney = (int) rs.getObject(2); System.out.println("執行寫取數據操作----" + currentMoney); //堵塞喚醒 countDownLatch.countDown(); PreparedStatement writePs = conn.prepareStatement(UPDATE_SQL); writePs.setInt(1, currentMoney - 1); writePs.execute(); conn.commit(); writePs.close(); ps.close(); System.out.println("執行寫操作結束---2"); } catch (Exception e) { //使用串行化事務級別能夠較好的保證數據的一致性,可串行化事務 serializable 是事務的最高級別,在每個讀數據上加上鎖 //innodb里面是加入了行鎖,因此出現了異常的時候,只需要重新執行一遍事務即可。 e.printStackTrace(); readAndUpdate2(); } } public void fantasyRead() { Thread thread1 = new Thread(new Runnable() { @Override public void run() { readAndUpdate1(); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { readAndUpdate2(); } }); try { thread1.start(); // Thread.sleep(500); thread2.start(); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { FantasyReadDemo fantasyReadDemo = new FantasyReadDemo(); fantasyReadDemo.fantasyRead(); } }
這里面將事務的隔離級別設置到了TRANSACTION_SERIALIZABLE,但是在運行過程中為了保證數據的一致性,串行化級別的事物會給相應的行數據加入行鎖,因此在執行的過程中會拋出下面的相關異常:
com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:377) .......
這里為了方便演示,在拋出異常的時候重新再次執行了一遍事務的方法,從而完成多次事務並發執行。
但是實際應用場景中,我們對於這種並發狀態造成的問題都會交給業務層面加入鎖來解決沖突,因此TRANSACTION_SERIALIZABLE隔離級別一般在應用場景中比較少見。
七種事務的傳播機制
事務的七種傳播機制分別為:
REQUIRED(默認) 默認的事務傳播機制,如果當前不支持事務,那么就創建一個新的事務。
SUPPORTS 表示支持當前的事務,如果當前沒有事務,則不會單獨創建事務
以上的這兩種事務傳播機制比較好理解,接下來的幾種事務傳播機制就比上邊的這幾類稍微復雜一些了。
REQUIRES_NEW
定義: 創建一個新事務,如果當前事務已經存在,把當前事務掛起。
為了更好的理解REQUIRES_NEW的含義,我們通過下邊的這個實例來進一步理解:
有這么一個業務場景,需要往數據插入一個account賬戶信息,然后同時再插入一條userAccount的流水信息。(只是模擬場景,所以對象的命名有點簡陋)
直接來看代碼實現,內容如下所示:
/** * @author idea * @data 2019/7/6 */ @Service public class AccountService { @Autowired private AccountDao accountDao; @Autowired private UserAccountService userAccountService; /** * 外層定義事務, userAccountService.saveOne單獨定義事務 * * @param accountId * @param money */ @Transactional(propagation = Propagation.REQUIRED) public void saveOne(Integer accountId, Double money) { accountDao.insert(new Account(accountId, money)); userAccountService.saveOne("idea", 1001); //這里模擬拋出異常 int j=1/0; } }
再來看userAccountService.saveOne函數:
/** * @author idea * @data 2019/7/6 */ @Service public class UserAccountService { @Autowired private UserAccountDao userAccountDao; /** * @param username * @param accountId */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveOne(String username,Integer accountId){ userAccountDao.insert(new UserAccount(username,accountId)); } }
執行程序的時候,AccountService.saveOne里面的 userAccountService.saveOne函數為單獨定義的一個事務,而且傳播屬性為REQUIRES_NEW。因此在執行外層函數的時候,即使后邊拋出了異常,也並不會影響到內部 userAccountService.saveOne的函數執行。
REQUIRES_NEW 總是新啟一個事務,這個傳播機制適用於不受父方法事物影響的操作,比如某些業務場景下需要記錄業務日志,用於異步反查,那么不管主體業務邏輯是否完成,日志都需要記錄下來,不能因為主體業務邏輯報錯而丟失日志;但是本身是一個單獨的事物,會受到回滾的影響,也就是說 userAccountService.saveOne里面要是拋了異常,子事務內容一起回滾。
NOT_SUPPORTED
定義:無事務執行,如果當前事務不存在,把已存在的當前事務掛起。
還是接上邊的代碼來進行試驗:
賬戶的轉賬操作:
userAccountService內部的saveOne操作:
在執行的過程中,userAccountService.saveOne拋出了異常,但是由於該方法申明的事物傳播屬性為NOT_SUPPORTED級別,因此當子事務內部拋出異常的時候,子事務本身不會回滾,而且也不會影響父類事務的執行。
NOT_SUPPORTED可以用於發送提示消息,站內信、短信、郵件提示等。不屬於並且不應當影響主體業務邏輯,即使發送失敗也不應該對主體業務邏輯回滾,並且執行過程中,如果父事務出現了異常,進行回滾,也不會影響子類的事務。
NESTED
定義:嵌套事務,如果當前事務存在,那么在嵌套的事務中執行。如果當前事務不存在,則表現跟REQUIRED一樣。
關於Nested的定義,我個人感覺網上寫的比較含糊,所以自己通過搭建Demo來強化理解,還是原來的例子,假設說父類事務執行的過程中拋出了異常如下,那么子類也要跟着回滾:
當父事務出現了異常之后,進行回滾,子事務也會被牽扯進來一起回滾。
MANDATORY
定義:MANDATORY單詞中文翻譯為強制,支持使用當前事務,如果當前事務不存在,則拋出Exception。
這個比較好理解
當子方法定義了事務,且事務的傳播屬性為MANDATORY級別的時候,如果父方法沒有定義事務操作的話,就會拋出異常。(此時的子方法會將數據記錄到數據庫里面)
NEVER
定義:當前如果存在事務則拋出異常
在執行userAccountService.saveOne函數的時候,發現父類的方法定義了事務,因此會拋出異常信息,並且userAccountService.saveOne會回滾。
傳播屬性小結:
PROPAGATION_NOT_SUPPORTED
不會受到父類事務影響而回滾,自己也不會影響父類函數,出現異常后會自動回滾。
PROPAGATION_REQUIRES_NEW
不會受到父類事務影響而回滾,自己也不會影響父類函數,出現異常后會自動回滾。
NESTED
會受到父類事務影響而回滾,出現異常后自身也回滾。如果不希望影響父類函數,那么可以通過使用try catch來控制操作。
MANDATORY
強制使用當期的事物,如果當前的父類方法沒有事務,那么在處理數據的時候就會拋出異常
NEVER
當前如果存在事務則拋出異常
REQUIRED(默認) 默認的事務傳播機制,如果當前不支持事務,那么就創建一個新的事務。
SUPPORTS 表示支持當前的事務,如果當前沒有事務,則不會單獨創建事務
本文的全部相關代碼都已經上傳到gitee上邊了,歡迎感興趣的朋友前往進行代碼下載:
https://gitee.com/IdeaHome_admin/wfw