互聯網的金融和電商行業,最關注數據庫事務。
業務核心 | 說明 |
---|---|
金融行業-金融產品金額 | 不容許發生錯誤 |
電商行業-商品交易金額,商品庫存 | 不容許發生錯誤 |
面臨的難點:java
高併發下保證: 數據一致性,高性能;
spring對事物的處理:mysql
採用AOP技術提供事務支持,申明式事務,去除了代碼中重複的try-catch-finally代碼;
兩個場景的解決方案:git
場景 | 解決辦法 |
---|---|
庫存扣減,交易記錄,帳戶金額的數據一致性 | 數據庫事務保證一致性 |
批量處理部分任務失敗不影響批量任務的回滾 | 數據庫事務傳播行爲 |
代碼github
package com.springbootpractice.demo.demo_jdbc_tx.biz; import lombok.SneakyThrows; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Objects; import java.util.Optional; /** * 說明:代碼方式事務編程 VS 申明式事物編程 * @author carter * 建立時間: 2020年01月08日 11:02 上午 **/ @Service public class TxJdbcBiz { private final JdbcTemplate jdbcTemplate; public TxJdbcBiz(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @SneakyThrows public int insertUserLogin(String username, String note) { Connection connection = null; int result = 0; try { connection = Objects.requireNonNull(jdbcTemplate.getDataSource()).getConnection(); connection.setAutoCommit(false); connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); final PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO user_login(user_name,password,sex,note) VALUES(?,?,?,?)"); preparedStatement.setString(1, username); preparedStatement.setString(2, "abc123"); preparedStatement.setInt(3, 1); preparedStatement.setString(4, note); result = preparedStatement.executeUpdate(); connection.commit(); } catch (Exception e) { Optional.ofNullable(connection) .ifPresent(item -> { try { item.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } }); e.printStackTrace(); } finally { Optional.ofNullable(connection) .filter(this::closeConnection) .ifPresent(item -> { try { item.close(); } catch (SQLException e) { e.printStackTrace(); } }); } return result; } private boolean closeConnection(Connection item) { try { return !item.isClosed(); } catch (SQLException e) { e.printStackTrace(); return false; } } @Transactional public int insertUserLoginTransaction(String username, String note) { String sql = "INSERT INTO user_login(user_name,password,sex,note) VALUES(?,?,?,?)"; Object[] params = {username, "abc123", 1, note}; return jdbcTemplate.update(sql, params); } }
測試代碼redis
package com.springbootpractice.demo.demo_jdbc_tx; import com.springbootpractice.demo.demo_jdbc_tx.biz.TxJdbcBiz; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.TransactionManager; import org.springframework.util.Assert; @SpringBootTest class DemoJdbcTxApplicationTests { @Autowired private TxJdbcBiz txJdbcBiz; @Autowired private TransactionManager transactionManager; @Test void testInsertUserTest() { final int result = txJdbcBiz.insertUserLogin("monika.smith", "xxxx"); Assert.isTrue(result > 0, "插入失敗"); } @Test void insertUserLoginTransactionTest() { final int result = txJdbcBiz.insertUserLoginTransaction("stefan.li", "hello transaction"); Assert.isTrue(result > 0, "插入失敗"); } @Test void transactionManagerTest() { System.out.println(transactionManager.getClass().getName()); } }
代碼中有一個很討厭的地方,就是 try-catch-finally;
流程圖spring
graph TD A[開始] --> B(開啓事務) B --> C{執行SQL} C -->|發生異常| D[事務回滾] C -->|正常| E[事物提交] D --> F[釋放事務資源] E --> F[釋放事務資源] F --> G[結束]
總體流程跟AOP的流程很是的類似,使用AOP,能夠把執行sql的步驟抽取出來單獨實現,其它的固定流程放到通知裏去作。
經過註解@Transaction來標註申明式事務,能夠標準在類或者方法上;
@Tranaction使用位置 | 說明 |
---|---|
類上或者接口上 | 類中全部的 公共非靜態方法 都將啓用事務,spring推薦放在實現類上,不然aop必須基於接口的代理生效的時候才能生效 |
方法上 | 本方法 |
@Transaction的源碼和配置項數據庫
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.transaction.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { @AliasFor("transactionManager") String value() default ""; @AliasFor("value") String transactionManager() default ""; Propagation propagation() default Propagation.REQUIRED; Isolation isolation() default Isolation.DEFAULT; int timeout() default -1; boolean readOnly() default false; Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; }
說明:編程
屬性 | 說明 |
---|---|
isolation | 事務的隔離級別 |
propagation | 傳播行爲 |
rollbackFor,rollbakcForClassName | 哪一種異常會觸發事務回滾 |
value | 事務管理器 |
timeout | 事務超時時間 |
readOnly | 是不是隻讀事務 |
noRollbackFor,noRollbackForClassName | 哪些異常不會觸發事務回滾 |
事務的安裝過程:api
springIOC容器啓動的時候,會把@Transactional註解的配置信息解析出來,而後存到事務定義器(TransactionDefinition),並記錄哪些類的方法須要啓動事務,採起什麼策略去執行事務。咱們要作的只是標註@Transactional和配置屬性便可;
流程如圖:
graph TD A[開始] --> B(開啓和設置事務) B --> C{執行方法邏輯} C -->|發生異常| D[事務回滾] C -->|正常| E[事物提交] D --> F[釋放事務資源] E --> F[釋放事務資源] F --> G[結束]
使用方式大大簡化;
代碼
@Transactional public int insertUserLoginTransaction(String username, String note) { String sql = "INSERT INTO user_login(user_name,password,sex,note) VALUES(?,?,?,?)"; Object[] params = {username, "abc123", 1, note}; return jdbcTemplate.update(sql, params); }
事務的打開,提交,回滾都是放在事務管理器上的。TransactionManager;
TransactionManager代碼
package org.springframework.transaction; public interface TransactionManager { }
這是一個空接口,實際起做用的是PlatfromTransactionManager;
PlatfromTransactionManager代碼:
package org.springframework.transaction; import org.springframework.lang.Nullable; public interface PlatformTransactionManager extends TransactionManager { TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; void commit(TransactionStatus var1) throws TransactionException; void rollback(TransactionStatus var1) throws TransactionException; }
3個架子的事務管理器對比:
架子 | 事務管理 | 說明 |
---|---|---|
spring-jdbc | DatasourceTransactionManager | |
jpa | JpaTransactionManager | |
mybatis | DatasourceTransactionManager |
mybatis的代碼實例點我!
場景:電商行業的庫存扣減,時刻都是多線程的環境中扣減庫存,對於數據庫而言,就會出現多個事務同事訪問同一記錄,這樣引發的數據不一致的狀況,就是數據庫丟失更新。
數據庫事務4個特性
即ACID
事務的特性 | 英文全稱 | 說明 |
---|---|---|
原子性 | Atomic | 一個事務中包含多個步驟操做A,B,C,原子性是標識這些操做要目所有成功,要麼所有失敗,不會出現第三種狀況 |
一致性 | Consistency | 在事務完成後,全部的數據都保持一致狀態 |
隔離性 | Isolation | 多個線程同時訪問同一數據,每一個線程處在不一樣的事務中,爲了壓制丟失更新的產生,定了隔離級別,經過隔離性的設置,能夠壓制丟失更新的發生,這裏存在一個選擇的過程 |
持久性 | Durability | 事務結束後,數據都會持久化,斷電重啓後也是能夠提供給程序繼續使用 |
隔離級別:
隔離級別 | 說明 | 問題 | 併發性能 |
---|---|---|---|
讀未提交【read uncommitted】 | 容許事務讀取另一個事務沒有提交的數據,事務要求比較高的狀況下不適用,適用於對事務要求不高的場景 | 髒讀(單條) | 併發性能最高 |
讀已提交【read committed】 | 一個事務只能讀取另一個事務已經提交的數據 | 不可重複讀(單條) | 併發性能通常 |
可重複讀【read repeated】 | 事務提交的時候也會判斷最新的值是否變化 | 幻想讀(多條數據而言) | 併發性能比較差 |
串行化【serializable】 | 全部的sql都按照順序執行 | 數據徹底一致 | 併發性能最差 |
選擇依據
隔離級別 | 髒讀 | 不可重複讀 | 幻象讀 |
---|---|---|---|
讀未提交 | 是 | 是 | 是 |
讀已提交 | 否 | 是 | 是 |
可重複讀 | 否 | 否 | 是 |
串行化 | 否 | 否 | 否 |
按照實際場景的容許狀況來設置事務的隔離級別;
隔離級別會帶來鎖的代價;優化方法:
- 樂觀鎖,
- redis分佈式鎖,
- zk分佈式鎖;
數據庫 | 事務隔離級別 | 默認事務隔離級別 |
---|---|---|
mysql | 4種 | 可重複讀 |
oracle | 讀已提交,串行化 | 讀已提交 |
springboot配置應用默認的事務隔離級別:spring.datasource.xxx.default-transaction-isolation=2
數字 | 對應隔離級別 |
---|---|
-1 | 無 |
1 | 讀未提交 |
2 | 讀已提交 |
4 | 可重複讀 |
8 | 串行化 |
傳播行爲是方法之間調用事務採起的策略問題。
場景:一個批量任務處在一個事務A中,每一個單獨是事務都有一個獨立的事務Bn; 子任務的回滾不影響事務A的回滾;
傳播行爲源碼
package org.springframework.transaction.annotation; import org.springframework.transaction.TransactionDefinition; public enum Propagation { REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED), SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS), MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY), REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW), NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED), NEVER(TransactionDefinition.PROPAGATION_NEVER), NESTED(TransactionDefinition.PROPAGATION_NESTED); private final int value; Propagation(int value) { this.value = value; } public int value() { return this.value; } }
列舉了7種傳播配置屬性,下面分別說明:
傳播行爲 | 父方法中存在事務子方法行爲 | 父方法中不存在事務子方法行爲 |
---|---|---|
REQUIRED | 默認傳播行爲,沿用, | 建立新的事務 |
SUPPORTS | 沿用; | 無事務,子方法中也無事務 |
MANDATORY | 沿用 | 拋出異常 |
REQUIRES_NEW | 建立新事務 | 建立新事務 |
NOT_SUPPORTED | 掛起事務,運行子方法 | 無事務,運行子方法 |
NEVER | 拋異常 | 無事務執行子方法 |
NESTED | 子方法發生異常,只回滾子方法的sql,而不回滾父方法中的事務 | 發生異常,只回滾子方法的sql,跟父方法無關 |
經常使用的三種傳播行爲:
代碼測試這三種傳播行爲:
spring使用了save point的技術來讓子事務回滾,而父事務不會滾;若是不支持save point,則新建一個事務來運行子事務;
區別點 | RequestNew | Nested |
---|---|---|
傳遞 | 擁有本身的鎖和隔離級別 | 沿用父事務的隔離級別和鎖 |
事務的實現原理是基於AOP,同一個類中方法的互相調用,是本身調用本身,而沒有代理對象的產生,就不會用到aop,因此,事務會失效;
解決辦法:經過spring的ioc容器獲得當前類的代理對象,調用本類的方法解決; 原創不易,轉載請註明出處,歡迎溝通交流。