javascript
Spring 事务底层原理,你会了吗?
??點(diǎn)擊上方?好好學(xué)java?,選擇?星標(biāo)?公眾號(hào)
重磅資訊、干貨,第一時(shí)間送達(dá) 今日推薦:八個(gè)開(kāi)源的 Spring Boot 學(xué)習(xí)資源,你值得擁有個(gè)人原創(chuàng)+1博客:點(diǎn)擊前往,查看更多 作者:香沙小熊 鏈接:https://www.jianshu.com/p/fc97500bcb5f一、數(shù)據(jù)庫(kù)的事物的基本特性
事物是區(qū)分文件存儲(chǔ)系統(tǒng)與Nosql數(shù)據(jù)庫(kù)重要特性之一,其存在的意義是為了保證即使在并發(fā)情況下也能正確的執(zhí)行crud操作。怎樣才算是正確的呢?這時(shí)提出了事物需要保證的四個(gè)特性即ACID:
A: 原子性(atomicity)事物中各項(xiàng)操作,要么全做要么全不做,任何一項(xiàng)操作的失敗都會(huì)導(dǎo)致整個(gè)事物的失敗;
C: 一致性(consistency)事物結(jié)束后系統(tǒng)狀態(tài)是一致的;
I: 隔離性(isolation)并發(fā)執(zhí)行的事物彼此無(wú)法看到對(duì)方的中間狀態(tài);
D: 持久性(durability)事物完成后所做的改動(dòng)都會(huì)被持久化,即使發(fā)生災(zāi)難性的失敗。在高并發(fā)的情況下,要完全保證其ACID特性是非常困難的,除非把所有的事物串行化執(zhí)行,但帶來(lái)的負(fù)面的影響將是性能大打折扣。很多時(shí)候我們有些業(yè)務(wù)對(duì)事物的要求是不一樣的,所以數(shù)據(jù)庫(kù)中設(shè)計(jì)了四種隔離級(jí)別,供用戶(hù)基于業(yè)務(wù)進(jìn)行選擇。
| 讀未提交(Read uncommitted) | 可能 | 可能 | 可能 |
| 讀已提交(Read committed) | 不可能 | 可能 | 可能 |
| 可重復(fù)讀(Repeatable read) | 不可能 | 不可能 | 可能 |
| 可串行化(SERIALIZABLE) | 不可能 | 不可能 | 不可能 |
臟讀 :
一個(gè)事物讀取到另一事物未提交的更新數(shù)據(jù)
不可重復(fù)讀 :
在同一事物中,多次讀取同一數(shù)據(jù)返回的結(jié)果有所不同, 換句話(huà)說(shuō), 后續(xù)讀取可以讀到另一事物已提交的更新數(shù)據(jù). 相反, “可重復(fù)讀”在同一事物中多次讀取數(shù)據(jù)時(shí), 能夠保證所讀數(shù)據(jù)一樣, 也就是后續(xù)讀取不能讀到另一事物已提交的更新數(shù)據(jù)。
幻讀 :
查詢(xún)表中一條數(shù)據(jù)如果不存在就插入一條,并發(fā)的時(shí)候卻發(fā)現(xiàn),里面居然有兩條相同的數(shù)據(jù)。這就幻讀的問(wèn)題。
二、Sring 對(duì)事物的支持與使用
知識(shí)點(diǎn):1.spring 事物相關(guān)API說(shuō)明 2.聲明式事物的使用 3.事物傳播機(jī)制
spring 事物相關(guān)API說(shuō)明 spring 事物是在數(shù)據(jù)庫(kù)事物的基礎(chǔ)上進(jìn)行封裝擴(kuò)展 其主要特性如下:a.支持原有的數(shù)據(jù)事物的隔離級(jí)別 b.加入了事物傳播的概念 提供多個(gè)事物的和并或隔離的功能 c.提供聲明式事物,讓業(yè)務(wù)代碼與事物分離,事物變得更易用。
怎么樣去使用Spring事物呢?spring 提供了三個(gè)接口供使用事物。分別是:
TransactionDefinition 事物定義
image.png
PlatformTransactionManager 事物管理
image.png
TransactionStatus 事物運(yùn)行時(shí)狀態(tài)
image.png
基于API實(shí)現(xiàn)事物
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();}// 設(shè)置保存點(diǎn)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;}});} }輸出
更新失敗查詢(xún)數(shù)據(jù)庫(kù)
2、聲明示事物
我們前面是通過(guò)調(diào)用API來(lái)實(shí)現(xiàn)對(duì)事物的控制,這非常的繁瑣,與直接操作JDBC事物并沒(méi)有太多的改善,所以Spring提出了聲明示事物,使我們對(duì)事物的操作變得非常簡(jiǎn)單,甚至不需要關(guān)心它。
編寫(xiě)服務(wù)類(lèi)
@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);// 人為報(bào)錯(cuò)int i = 1 / 0;</code>}演示添加 @Transactional 注解和不添加注解的情況。
3、事物傳播機(jī)制
| 支持當(dāng)前事物 | PROPAGATION_REQUIRED | |
| (必須的) | 如果當(dāng)前沒(méi)有事物,就新建一個(gè)事物,如果已經(jīng)存在一個(gè)事物中,加入到這個(gè)事物中。這是最常見(jiàn)的選擇。 | |
| 支持當(dāng)前事物 | PROPAGATION_SUPPORTS | |
| (支持) | 支持當(dāng)前事物,如果當(dāng)前沒(méi)有事物,就以非事物方式執(zhí)行。 | |
| 支持當(dāng)前事物 | PROPAGATION_MANDATORY | |
| (強(qiáng)制) | 使用當(dāng)前的事物,如果當(dāng)前沒(méi)有事物,就拋出異常。 | |
| 不支持當(dāng)前事物 | PROPAGATION_REQUIRES_NEW | |
| (隔離) | 新建事物,如果當(dāng)前存在事物,把當(dāng)前事物掛起。 | |
| 不支持當(dāng)前事物 | PROPAGATION_NOT_SUPPORTED | |
| (不支持) | 以非事物方式執(zhí)行操作,如果當(dāng)前存在事物,就把當(dāng)前事物掛起。 | |
| 不支持當(dāng)前事物 | PROPAGATION_NEVER | |
| (強(qiáng)制非事物) | 以非事物方式執(zhí)行,如果當(dāng)前存在事物,則拋出異常。 | |
| 套事物 | PROPAGATION_NESTED | |
| (嵌套事物) | 如果當(dāng)前存在事物,則在嵌套事物內(nèi)執(zhí)行。如果當(dāng)前沒(méi)有事物,則執(zhí)行與PROPAGATION_REQUIRED類(lèi)似的操作。 |
常用事物傳播機(jī)制:
PROPAGATION_REQUIRED, 這個(gè)也是默認(rèn)的傳播機(jī)制;
PROPAGATION_NOT_SUPPORTED 可以用于發(fā)送提示消息,站內(nèi)信、短信、郵件提示等。不屬于并且不應(yīng)當(dāng)影響主體業(yè)務(wù)邏輯,即使發(fā)送失敗也不應(yīng)該對(duì)主體業(yè)務(wù)邏輯回滾。
PROPAGATION_REQUIRES_NEW 總是新啟一個(gè)事物,這個(gè)傳播機(jī)制適用于不受父方法事物影響的操作,比如某些業(yè)務(wù)場(chǎng)景下需要記錄業(yè)務(wù)日志,用于異步反查,那么不管主體業(yè)務(wù)邏輯是否完成,日志都需要記錄下來(lái),不能因?yàn)橹黧w業(yè)務(wù)邏輯報(bào)錯(cuò)而丟失日志;
l 演示常用事物的傳播機(jī)制
用例1:
創(chuàng)建用戶(hù)時(shí)初始化一個(gè)帳戶(hù),表結(jié)構(gòu)和服務(wù)類(lèi)如下。
| user | UserSerivce | 創(chuàng)建用戶(hù),并添加帳戶(hù) |
| account | AccountService | 添加帳戶(hù) |
UserSerivce.createUser(name) 實(shí)現(xiàn)代碼
@Transactional public void createUser(String name) {// 新增用戶(hù)基本信息jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);//調(diào)用accountService添加帳戶(hù)accountService.addAccount(name, 10000);}AccountService.addAccount(name,initMoney) 實(shí)現(xiàn)代碼(方法的最后有一個(gè)異常)
@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);// 出現(xiàn)分母為零的異常int i = 1 / 0;}實(shí)驗(yàn)預(yù)測(cè)一:
| 場(chǎng)景一 | 無(wú)事物 | required | createUser (成功) addAccount(不成功) |
| 場(chǎng)景二 | required | 無(wú)事物 | createUser (不成功) addAccount(不成功) |
| 場(chǎng)景三 | required | not_supported | createUser (不成功) addAccount(成功) |
| 場(chǎng)景四 | required | required_new | createUser (不成功) addAccount(不成功) |
| 場(chǎng)景五 | required(異常移至createUser方法未尾) | required_new | createUser(不成功) |
| addAccount(成功) | |||
| 場(chǎng)景六 | required(異常移至createUser方法未尾)(addAccount 方法移至createUser方法的同一個(gè)類(lèi)里) | required_new | createUser (不成功) addAccount(不成功) |
三、aop 事物底層實(shí)現(xiàn)原理
講事物原理之前我們先來(lái)做一個(gè)實(shí)驗(yàn),當(dāng)場(chǎng)景五的環(huán)境改變,把a(bǔ)ddAccount 方法移至UserService 類(lèi)下,其它配置和代碼不變:
@Override @Transactional public void createUser(String name) {jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);addAccount(name, 10000);// 人為報(bào)錯(cuò)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);}演示新場(chǎng)景
經(jīng)過(guò)演示我們發(fā)現(xiàn)得出的結(jié)果與場(chǎng)景五并不 一至,required_new 沒(méi)有起到其對(duì)應(yīng)的作用。原因在于spring 聲明示事物使用動(dòng)態(tài)代理實(shí)現(xiàn),而當(dāng)調(diào)用同一個(gè)類(lèi)的方法時(shí),是會(huì)不會(huì)走代理邏輯的,自然事物的配置也會(huì)失效。
通過(guò)一個(gè)動(dòng)態(tài)代理的實(shí)現(xiàn)來(lái)模擬這種場(chǎng)景
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("開(kāi)啟事物:"+method.getName());return method.invoke(userSerivce, args);} finally {System.out.println("關(guān)閉事物:"+method.getName());}}});proxyUserSerivce.createUser("kpioneer");當(dāng)我們調(diào)用createUser 方法時(shí) 僅打印了 createUser 的事物開(kāi)啟、關(guān)閉,并沒(méi)有打印addAccount 方法的事物開(kāi)啟、關(guān)閉,由此可見(jiàn)addAccount 的事物配置是失效的。
如果業(yè)務(wù)當(dāng)中上真有這種場(chǎng)景該如何實(shí)現(xiàn)呢?在spring xml中配置 暴露proxy 對(duì)象,然后在代碼中用AopContext.currentProxy() 就可以獲當(dāng)前代理對(duì)象
<!-- 配置暴露proxy --> <aop:aspectj-autoproxy expose-proxy="true"/>// 基于代理對(duì)象調(diào)用創(chuàng)建帳戶(hù),事物的配置又生效了
@Transactional public void createUser(String name) {// 新增用戶(hù)基本信息jdbcTemplate.update("INSERT INTO `user` (name) VALUES(?)", name);// 暴露proxy 對(duì)象 調(diào)用accountService添加帳戶(hù)((UserSerivce) AopContext.currentProxy()).addAccount(name, 10000);// 人為報(bào)錯(cuò)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); }但是并不推薦這樣寫(xiě)事務(wù),還是另寫(xiě)AccountService類(lèi) 調(diào)用addAccount最好
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專(zhuān)家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的Spring 事务底层原理,你会了吗?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Java 中的 BigDecimal 类
- 下一篇: Redis 过期键删除策略