Spring 事务底层原理,你会了吗?
??點擊上方?好好學java?,選擇?星標?公眾號
重磅資訊、干貨,第一時間送達 今日推薦:八個開源的 Spring Boot 學習資源,你值得擁有個人原創+1博客:點擊前往,查看更多 作者:香沙小熊 鏈接:https://www.jianshu.com/p/fc97500bcb5f一、數據庫的事物的基本特性
事物是區分文件存儲系統與Nosql數據庫重要特性之一,其存在的意義是為了保證即使在并發情況下也能正確的執行crud操作。怎樣才算是正確的呢?這時提出了事物需要保證的四個特性即ACID:
A: 原子性(atomicity)事物中各項操作,要么全做要么全不做,任何一項操作的失敗都會導致整個事物的失敗;
C: 一致性(consistency)事物結束后系統狀態是一致的;
I: 隔離性(isolation)并發執行的事物彼此無法看到對方的中間狀態;
D: 持久性(durability)事物完成后所做的改動都會被持久化,即使發生災難性的失敗。在高并發的情況下,要完全保證其ACID特性是非常困難的,除非把所有的事物串行化執行,但帶來的負面的影響將是性能大打折扣。很多時候我們有些業務對事物的要求是不一樣的,所以數據庫中設計了四種隔離級別,供用戶基于業務進行選擇。
| 讀未提交(Read uncommitted) | 可能 | 可能 | 可能 |
| 讀已提交(Read committed) | 不可能 | 可能 | 可能 |
| 可重復讀(Repeatable read) | 不可能 | 不可能 | 可能 |
| 可串行化(SERIALIZABLE) | 不可能 | 不可能 | 不可能 |
臟讀 :
一個事物讀取到另一事物未提交的更新數據
不可重復讀 :
在同一事物中,多次讀取同一數據返回的結果有所不同, 換句話說, 后續讀取可以讀到另一事物已提交的更新數據. 相反, “可重復讀”在同一事物中多次讀取數據時, 能夠保證所讀數據一樣, 也就是后續讀取不能讀到另一事物已提交的更新數據。
幻讀 :
查詢表中一條數據如果不存在就插入一條,并發的時候卻發現,里面居然有兩條相同的數據。這就幻讀的問題。
二、Sring 對事物的支持與使用
知識點:1.spring 事物相關API說明 2.聲明式事物的使用 3.事物傳播機制
spring 事物相關API說明 spring 事物是在數據庫事物的基礎上進行封裝擴展 其主要特性如下:a.支持原有的數據事物的隔離級別 b.加入了事物傳播的概念 提供多個事物的和并或隔離的功能 c.提供聲明式事物,讓業務代碼與事物分離,事物變得更易用。
怎么樣去使用Spring事物呢?spring 提供了三個接口供使用事物。分別是:
TransactionDefinition 事物定義
image.png
PlatformTransactionManager 事物管理
image.png
TransactionStatus 事物運行時狀態
image.png
基于API實現事物
public class SpringTransactionExample {private static String url = "jdbc:mysql:///localhost:3306/test";private static String user = "root";private static String password = "123456";public static Connection openConnection() throws ClassNotFoundException, SQLException {Class.forName("com.mysql.jdbc.Driver");Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");return conn;}public static void main(String[] args) {final DriverManagerDataSource ds = new DriverManagerDataSource(url, user, password);final TransactionTemplate template = new TransactionTemplate();template.setTransactionManager(new DataSourceTransactionManager(ds));template.execute(new TransactionCallback<Object>() {@Overridepublic Object doInTransaction(TransactionStatus status) {Connection conn = DataSourceUtils.getConnection(ds);Object savePoint = null;try {{// 插入PreparedStatement prepare = conn.prepareStatement("insert INTO account (accountName,user,money) VALUES (?,?,?)");prepare.setString(1, "111");prepare.setString(2, "aaaa");prepare.setInt(3, 10000);prepare.executeUpdate();}// 設置保存點savePoint = status.createSavepoint();{// 插入PreparedStatement prepare = conn.prepareStatement("insert INTO account (accountName,user,money) VALUES (?,?,?)");prepare.setString(1, "222");prepare.setString(2, "bbb");prepare.setInt(3, 10000);prepare.executeUpdate();}{// 更新PreparedStatement prepare = conn.prepareStatement("UPDATE account SET money= money+1 where user=?");prepare.setString(1, "asdflkjaf");Assert.isTrue(prepare.executeUpdate() > 0, "");}} catch (SQLException e) {e.printStackTrace();} catch (Exception e) {System.out.println("更新失敗");if (savePoint != null) {status.rollbackToSavepoint(savePoint);} else {status.setRollbackOnly();}}return null;}});} }輸出
更新失敗查詢數據庫
2、聲明示事物
我們前面是通過調用API來實現對事物的控制,這非常的繁瑣,與直接操作JDBC事物并沒有太多的改善,所以Spring提出了聲明示事物,使我們對事物的操作變得非常簡單,甚至不需要關心它。
編寫服務類
@Transactionalpublic void addAccount(String name, int initMenoy) {String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());jdbcTemplate.update("insert INTO account (accountName,user,money) VALUES (?,?,?)", accountid, name, initMenoy);// 人為報錯int i = 1 / 0;</code>}演示添加 @Transactional 注解和不添加注解的情況。
3、事物傳播機制
| 支持當前事物 | PROPAGATION_REQUIRED | |
| (必須的) | 如果當前沒有事物,就新建一個事物,如果已經存在一個事物中,加入到這個事物中。這是最常見的選擇。 | |
| 支持當前事物 | PROPAGATION_SUPPORTS | |
| (支持) | 支持當前事物,如果當前沒有事物,就以非事物方式執行。 | |
| 支持當前事物 | PROPAGATION_MANDATORY | |
| (強制) | 使用當前的事物,如果當前沒有事物,就拋出異常。 | |
| 不支持當前事物 | PROPAGATION_REQUIRES_NEW | |
| (隔離) | 新建事物,如果當前存在事物,把當前事物掛起。 | |
| 不支持當前事物 | PROPAGATION_NOT_SUPPORTED | |
| (不支持) | 以非事物方式執行操作,如果當前存在事物,就把當前事物掛起。 | |
| 不支持當前事物 | PROPAGATION_NEVER | |
| (強制非事物) | 以非事物方式執行,如果當前存在事物,則拋出異常。 | |
| 套事物 | PROPAGATION_NESTED | |
| (嵌套事物) | 如果當前存在事物,則在嵌套事物內執行。如果當前沒有事物,則執行與PROPAGATION_REQUIRED類似的操作。 |
常用事物傳播機制:
PROPAGATION_REQUIRED, 這個也是默認的傳播機制;
PROPAGATION_NOT_SUPPORTED 可以用于發送提示消息,站內信、短信、郵件提示等。不屬于并且不應當影響主體業務邏輯,即使發送失敗也不應該對主體業務邏輯回滾。
PROPAGATION_REQUIRES_NEW 總是新啟一個事物,這個傳播機制適用于不受父方法事物影響的操作,比如某些業務場景下需要記錄業務日志,用于異步反查,那么不管主體業務邏輯是否完成,日志都需要記錄下來,不能因為主體業務邏輯報錯而丟失日志;
l 演示常用事物的傳播機制
用例1:
創建用戶時初始化一個帳戶,表結構和服務類如下。
| user | UserSerivce | 創建用戶,并添加帳戶 |
| account | AccountService | 添加帳戶 |
UserSerivce.createUser(name) 實現代碼
@Transactional public void createUser(String name) {// 新增用戶基本信息jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);//調用accountService添加帳戶accountService.addAccount(name, 10000);}AccountService.addAccount(name,initMoney) 實現代碼(方法的最后有一個異常)
@Transactional(propagation = Propagation.REQUIRED) public void addAccount(String name, int initMoney) {String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());jdbcTemplate.update("insert INTO account (accountName,user,money) VALUES (?,?,?)", accountid, name, initMenoy);// 出現分母為零的異常int i = 1 / 0;}實驗預測一:
| 場景一 | 無事物 | required | createUser (成功) addAccount(不成功) |
| 場景二 | required | 無事物 | createUser (不成功) addAccount(不成功) |
| 場景三 | required | not_supported | createUser (不成功) addAccount(成功) |
| 場景四 | required | required_new | createUser (不成功) addAccount(不成功) |
| 場景五 | required(異常移至createUser方法未尾) | required_new | createUser(不成功) |
| addAccount(成功) | |||
| 場景六 | required(異常移至createUser方法未尾)(addAccount 方法移至createUser方法的同一個類里) | required_new | createUser (不成功) addAccount(不成功) |
三、aop 事物底層實現原理
講事物原理之前我們先來做一個實驗,當場景五的環境改變,把addAccount 方法移至UserService 類下,其它配置和代碼不變:
@Override @Transactional public void createUser(String name) {jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);addAccount(name, 10000);// 人為報錯int i = 1 / 0;}@Transactional(propagation = Propagation.REQUIRES_NEW) public void addAccount(String name, int initMoney) {String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());jdbcTemplate.update("insert INTO account (accountName,user,money) VALUES (?,?,?)", accountid, name, initMoney);}演示新場景
經過演示我們發現得出的結果與場景五并不 一至,required_new 沒有起到其對應的作用。原因在于spring 聲明示事物使用動態代理實現,而當調用同一個類的方法時,是會不會走代理邏輯的,自然事物的配置也會失效。
通過一個動態代理的實現來模擬這種場景
UserSerivce proxyUserSerivce = (UserSerivce) Proxy.newProxyInstance(LubanTransaction.class.getClassLoader(),new Class[]{UserSerivce.class}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {System.out.println("開啟事物:"+method.getName());return method.invoke(userSerivce, args);} finally {System.out.println("關閉事物:"+method.getName());}}});proxyUserSerivce.createUser("kpioneer");當我們調用createUser 方法時 僅打印了 createUser 的事物開啟、關閉,并沒有打印addAccount 方法的事物開啟、關閉,由此可見addAccount 的事物配置是失效的。
如果業務當中上真有這種場景該如何實現呢?在spring xml中配置 暴露proxy 對象,然后在代碼中用AopContext.currentProxy() 就可以獲當前代理對象
<!-- 配置暴露proxy --> <aop:aspectj-autoproxy expose-proxy="true"/>// 基于代理對象調用創建帳戶,事物的配置又生效了
@Transactional public void createUser(String name) {// 新增用戶基本信息jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);// 暴露proxy 對象 調用accountService添加帳戶((UserSerivce) AopContext.currentProxy()).addAccount(name, 10000);// 人為報錯int i = 1 / 0;} @Transactional(propagation = Propagation.REQUIRES_NEW) public void addAccount(String name, int initMoney) {String accountid = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());jdbcTemplate.update("insert INTO account (accountName,user,money) VALUES (?,?,?)", accountid, name, initMoney); }但是并不推薦這樣寫事務,還是另寫AccountService類 調用addAccount最好
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的Spring 事务底层原理,你会了吗?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 中的 BigDecimal 类
- 下一篇: 如何异地加载 Spring Boot 配