系列4】注解@Transactional)
主要講解注解Transactional的基礎(chǔ)知識(shí)、使用姿勢(shì)以及事務(wù)不生效的幾種Case。前言前面已經(jīng)講解了IOC的基礎(chǔ)知識(shí)以及Spring常用的注解這篇文章是對(duì)上一篇文章《【Spring基礎(chǔ)系列3】Spring常用的注解》的補(bǔ)充由于這個(gè)注解需要講述的內(nèi)容比較多一方面該注解非常重要另一方面非常容易入坑所以這個(gè)注解的內(nèi)容就單獨(dú)放到這篇文章來(lái)講。項(xiàng)目準(zhǔn)備為了更好通過(guò)示例講解注解Transactional的特性本文會(huì)有大量的示例這些示例是依賴如下配置和數(shù)據(jù)如果只關(guān)注基礎(chǔ)知識(shí)可以跳過(guò)這一部分。使用的是Mysql Innodb存儲(chǔ)引擎事務(wù)隔離級(jí)別設(shè)置為可重復(fù)讀RR。pom.xml需要添加的依賴包dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.4.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.0/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version6.0.6/version /dependency使用Spring MyBatis的方式對(duì)DB進(jìn)行操作下面是XML映射文件?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.mybatis.dao.UserDao !-- 根據(jù)uid查詢一個(gè)用戶信息 -- select idselectUserById parameterTypeInteger resultTypecom.mybatis.entity.MyUser select * from user_test where uid #{uid} /select !--修改一個(gè)用戶 -- update idupdateUser parameterTypecom.mybatis.entity.MyUser update user_test set uname #{uname},usex #{usex} where uid #{uid} /update /mapper提供的接口Repository(userDao) Mapper /* * 使用Spring自動(dòng)掃描MyBatis的接口并裝配 Spring將指定包中所有被Mapper注解標(biāo)注的接口自動(dòng)裝配為MyBatis的映射接口 */ public interface UserDao { /** * 接口方法對(duì)應(yīng)的SQL映射文件中的id */ public MyUser selectUserById(Integer uid); public int updateUser(MyUser user); }DB結(jié)構(gòu)CREATE TABLE user_test ( uid tinyint(2) NOT NULL, uname varchar(20) DEFAULT NULL, usex varchar(10) DEFAULT NULL, PRIMARY KEY (uid) ) ENGINEInnoDB DEFAULT CHARSETutf8DB初始數(shù)據(jù)uidunameusex1張三女2陳恒男3樓仔男Transactional基礎(chǔ)知識(shí)概述Transactional是聲明式事務(wù)管理編程中使用的注解可以添加在接口實(shí)現(xiàn)類(lèi)或接口實(shí)現(xiàn)方法上而不是接口類(lèi)中訪問(wèn)權(quán)限只有public的方法才起作用當(dāng)接口與接口中方法上同時(shí)帶有Transactional注解時(shí)方法上注解屬性會(huì)覆蓋類(lèi)注解上的相同屬性系統(tǒng)設(shè)計(jì)時(shí)將標(biāo)簽放置在需要進(jìn)行事務(wù)管理的方法上而不是不假思索的放置在接口實(shí)現(xiàn)類(lèi)上錯(cuò)誤使用的方式會(huì)在“事務(wù)不生效的幾種case”中講述多線程下事務(wù)管理因?yàn)榫€程不屬于spring托管故線程不能夠默認(rèn)使用spring的事務(wù)也不能獲取spring注入的bean在被spring聲明式事務(wù)管理的方法內(nèi)開(kāi)啟多線程多線程內(nèi)的方法不被事務(wù)控制具體可參考“事務(wù)不生效的幾種case”中多線程事務(wù)不生效的場(chǎng)景。使用姿勢(shì)講解基礎(chǔ)知識(shí)前我們先看Transactional怎么使用下面是DB數(shù)據(jù)正常更新的情況Controller(userController) public class UserController { Autowired private UserDao userDao; public void update(Integer id) { MyUser user new MyUser(); user.setUid(id); user.setUname(張三-testing); // 變更數(shù)據(jù) user.setUsex(女); userDao.updateUser(user); } public MyUser query(Integer id) { MyUser user userDao.selectUserById(id); return user; } Transactional(rollbackFor Exception.class) public void testSuccess() throws Exception { Integer id 1; MyUser user query(id); System.out.println(原記錄: user); update(id); //throw new Exception(測(cè)試事務(wù)回滾生效); } }再看一下測(cè)試用例public static void main(String[] args) throws Exception { ApplicationContext applicationContext new ClassPathXmlApplicationContext(applicationContext.xml); UserController uc (UserController) applicationContext.getBean(userController); try { uc.testSuccess(); } finally { MyUser user uc.query(1); System.out.println(修改后的記錄: user); } } // 輸出 // 原記錄:User[uid1,uname張三,usex女] // 修改后的記錄:User[uid1,uname張三-test,usex女]我們發(fā)現(xiàn)DB更新成功現(xiàn)在我們修改一下代碼將DB數(shù)據(jù)回退到原始狀態(tài)然后再把拋出異常放開(kāi)看事務(wù)是否生效public void update(Integer id) { MyUser user new MyUser(); user.setUid(id); user.setUname(張三-testing); // 變更數(shù)據(jù) user.setUsex(女); userDao.updateUser(user); } Transactional(rollbackFor Exception.class) public void testSuccess() throws Exception { Integer id 1; MyUser user query(id); System.out.println(原記錄: user); update(id); throw new Exception(測(cè)試事務(wù)回滾生效); }再看看執(zhí)行結(jié)果原記錄:User[uid1,uname張三,usex女] 修改后的記錄:User[uid1,uname張三,usex女]我們發(fā)現(xiàn)因?yàn)槌绦驋伋霎惓B數(shù)據(jù)正?;貪L符合預(yù)期。實(shí)現(xiàn)原理Transactional實(shí)質(zhì)是使用了JDBC的事務(wù)來(lái)進(jìn)行事務(wù)控制的Transactional基于Spring的動(dòng)態(tài)代理的機(jī)制 Transactional實(shí)現(xiàn)原理事務(wù)開(kāi)始時(shí)通過(guò)AOP機(jī)制生成一個(gè)代理connection對(duì)象并將其放入DataSource實(shí)例的某個(gè)與DataSourceTransactionManager相關(guān)的某處容器中。在接下來(lái)的整個(gè)事務(wù)中客戶代碼都應(yīng)該使用該connection連接數(shù)據(jù)庫(kù)執(zhí)行所有數(shù)據(jù)庫(kù)命令[不使用該connection連接數(shù)據(jù)庫(kù)執(zhí)行的數(shù)據(jù)庫(kù)命令在本事務(wù)回滾的時(shí)候得不到回滾]物理連接connection邏輯上新建一個(gè)會(huì)話sessionDataSource與TransactionManager配置相同的數(shù)據(jù)源事務(wù)結(jié)束時(shí)回滾在第1步驟中得到的代理connection對(duì)象上執(zhí)行的數(shù)據(jù)庫(kù)命令然后關(guān)閉該代理connection對(duì)象事務(wù)結(jié)束后回滾操作不會(huì)對(duì)已執(zhí)行完畢的SQL操作命令起作用事務(wù)特性pring所有的事務(wù)管理策略類(lèi)都繼承自org.springframework.transaction.PlatformTransactionManager接口public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }事務(wù)的隔離級(jí)別Transactional(isolation Isolation.READ_UNCOMMITTED)讀取未提交數(shù)據(jù)(會(huì)出現(xiàn)臟讀, 不可重復(fù)讀) 基本不使用Transactional(isolation Isolation.READ_COMMITTED)讀取已提交數(shù)據(jù)(會(huì)出現(xiàn)不可重復(fù)讀和幻讀)Transactional(isolation Isolation.REPEATABLE_READ)可重復(fù)讀(會(huì)出現(xiàn)幻讀)Transactional(isolation Isolation.SERIALIZABLE)串行化事務(wù)傳播行為如果在開(kāi)始當(dāng)前事務(wù)之前一個(gè)事務(wù)上下文已經(jīng)存在此時(shí)有若干選項(xiàng)可以指定一個(gè)事務(wù)性方法的執(zhí)行行為ransactionDefinition.PROPAGATION_REQUIRED如果當(dāng)前存在事務(wù)則加入該事務(wù)如果當(dāng)前沒(méi)有事務(wù)則創(chuàng)建一個(gè)新的事務(wù)。這是默認(rèn)值。TransactionDefinition.PROPAGATION_REQUIRES_NEW創(chuàng)建一個(gè)新的事務(wù)如果當(dāng)前存在事務(wù)則把當(dāng)前事務(wù)掛起。TransactionDefinition.PROPAGATION_SUPPORTS如果當(dāng)前存在事務(wù)則加入該事務(wù)如果當(dāng)前沒(méi)有事務(wù)則以非事務(wù)的方式繼續(xù)運(yùn)行。TransactionDefinition.PROPAGATION_NOT_SUPPORTED以非事務(wù)方式運(yùn)行如果當(dāng)前存在事務(wù)則把當(dāng)前事務(wù)掛起。TransactionDefinition.PROPAGATION_NEVER以非事務(wù)方式運(yùn)行如果當(dāng)前存在事務(wù)則拋出異常。TransactionDefinition.PROPAGATION_MANDATORY如果當(dāng)前存在事務(wù)則加入該事務(wù)如果當(dāng)前沒(méi)有事務(wù)則拋出異常。TransactionDefinition.PROPAGATION_NESTED如果當(dāng)前存在事務(wù)則創(chuàng)建一個(gè)事務(wù)作為當(dāng)前事務(wù)的嵌套事務(wù)來(lái)運(yùn)行如果當(dāng)前沒(méi)有事務(wù)則該取值等價(jià)于TransactionDefinition.PROPAGATION_REQUIRED。屬性配置字段說(shuō)明value 主要用來(lái)指定不同的事務(wù)管理器主要用來(lái)滿足在同一個(gè)系統(tǒng)中存在不同的事務(wù)管理器。比如在Spring中聲明了兩種事務(wù)管理器txManager1, txManager2。然后用戶可以根據(jù)這個(gè)參數(shù)來(lái)根據(jù)需要指定特定的txManager。value 適用場(chǎng)景即在一個(gè)系統(tǒng)中需要訪問(wèn)多個(gè)數(shù)據(jù)源或者多個(gè)數(shù)據(jù)庫(kù)則必然會(huì)配置多個(gè)事務(wù)管理器的isolation事務(wù)的隔離度默認(rèn)值采用 DEFAULT。propagation事務(wù)的傳播行為默認(rèn)值為 REQUIRED具體取值可參考“事務(wù)特性”例如Transactional(propagationPropagation.NOT_SUPPORTED,readOnlytrue)readOnly該屬性用于設(shè)置當(dāng)前事務(wù)是否為只讀事務(wù)設(shè)置為true表示只讀false則表示可讀寫(xiě)默認(rèn)值為false。例如Transactional(readOnlytrue)。timeout該屬性用于設(shè)置事務(wù)的超時(shí)秒數(shù)如果超過(guò)這個(gè)時(shí)間就強(qiáng)制回滾默認(rèn)值為-1表示永不超時(shí)。rollbackFor 該屬性用于設(shè)置需要進(jìn)行回滾的異常類(lèi)數(shù)組當(dāng)方法中拋出指定異常數(shù)組中的異常時(shí)則進(jìn)行事務(wù)回滾。例如指定單一異常類(lèi)Transactional(rollbackForRuntimeException.class)指定多個(gè)異常類(lèi)Transactional(rollbackFor{RuntimeException.class, Exception.class})rollbackForClassName該屬性用于設(shè)置需要進(jìn)行回滾的異常類(lèi)名稱數(shù)組當(dāng)方法中拋出指定異常名稱數(shù)組中的異常時(shí)則進(jìn)行事務(wù)回滾。例如指定單一異常類(lèi)名稱Transactional(rollbackForClassNameRuntimeException)指定多個(gè)異常類(lèi)名稱Transactional(rollbackForClassName{RuntimeException,Exception})noRollbackFor 該屬性用于設(shè)置不需要進(jìn)行回滾的異常類(lèi)數(shù)組當(dāng)方法中拋出指定異常數(shù)組中的異常時(shí)不進(jìn)行事務(wù)回滾。例如指定單一異常類(lèi)Transactional(noRollbackForRuntimeException.class)指定多個(gè)異常類(lèi)Transactional(noRollbackFor{RuntimeException.class, Exception.class})noRollbackForClassName該屬性用于設(shè)置不需要進(jìn)行回滾的異常類(lèi)名稱數(shù)組當(dāng)方法中拋出指定異常名稱數(shù)組中的異常時(shí)不進(jìn)行事務(wù)回滾。例如指定單一異常類(lèi)名稱Transactional(noRollbackForClassNameRuntimeException)指定多個(gè)異常類(lèi)名稱Transactional(noRollbackForClassName{RuntimeException,Exception})事務(wù)不生效的幾種case所有的測(cè)試Case測(cè)試完畢后DB數(shù)據(jù)需要手動(dòng)更新成原始數(shù)據(jù)保證測(cè)試Case互不影響。Case1: 類(lèi)內(nèi)部訪問(wèn)簡(jiǎn)單來(lái)講就是指非直接訪問(wèn)帶注解標(biāo)記的方法 B而是通過(guò)類(lèi)普通方法 A然后由 A 訪問(wèn) B下面是一個(gè)簡(jiǎn)單的 case我們?cè)陬?lèi)UserController中新增一個(gè)方法testFail()public void testFail() throws Exception { testSuccess(); throw new Exception(測(cè)試事務(wù)回滾不生效); }這里我們是通過(guò)testFail()調(diào)用testSuccess()再看一下測(cè)試用例public static void main(String[] args) throws Exception { ApplicationContext applicationContext new ClassPathXmlApplicationContext(applicationContext.xml); UserController uc (UserController) applicationContext.getBean(userController); try { uc.testFail(); } finally { MyUser user uc.query(1); System.out.println(修改后的記錄: user); } } // 輸出 // 原記錄:User[uid1,uname張三,usex女] // 修改后的記錄:User[uid1,uname張三,usex女]從上面的輸出可以看到事務(wù)并沒(méi)有回滾主要是因?yàn)轭?lèi)內(nèi)部調(diào)用不會(huì)通過(guò)代理方式訪問(wèn)。Case2: 私有方法在私有方法上添加Transactional注解也不會(huì)生效私有方法外部不能訪問(wèn)所以只能內(nèi)部訪問(wèn)上面的 case 不生效這個(gè)當(dāng)然也不生效了Transactional(rollbackFor Exception.class) private void testSuccess() throws Exception { Integer id 1; MyUser user query(id); System.out.println(原記錄: user); update(id); throw new Exception(測(cè)試事務(wù)生效); }直接使用時(shí)下面這種場(chǎng)景不太容易出現(xiàn)因?yàn)?IDEA 會(huì)有提醒文案為: Methods annotated with Transactional must be overridableCase3: 異常不匹配Transactional注解默認(rèn)處理運(yùn)行時(shí)異常即只有拋出運(yùn)行時(shí)異常時(shí)才會(huì)觸發(fā)事務(wù)回滾否則并不會(huì)回滾Transactional public void testSuccess() throws Exception { Integer id 1; MyUser user query(id); System.out.println(原記錄: user); update(id); throw new Exception(測(cè)試事務(wù)生效); }測(cè)試 case 如下public static void main(String[] args) throws Exception { ApplicationContext applicationContext new ClassPathXmlApplicationContext(applicationContext.xml); UserController uc (UserController) applicationContext.getBean(userController); try { uc.testSuccess(); } finally { MyUser user uc.query(1); System.out.println(修改后的記錄: user); } } // 輸出 // 原記錄:User[uid1,uname張三,usex女] // 修改后的記錄:User[uid1,uname張三-test,usex女]輸出結(jié)果如下事務(wù)并未回滾如果需要解決這個(gè)問(wèn)題通過(guò)設(shè)置Transactional的 rollbackFor 屬性即可Case4: 多線程這個(gè)場(chǎng)景可能并不多見(jiàn)在標(biāo)記事務(wù)的方法內(nèi)部另起子線程執(zhí)行 db 操作此時(shí)事務(wù)同樣不會(huì)生效下面給出兩個(gè)不同的姿勢(shì)一個(gè)是子線程拋異常主線程 ok一個(gè)是子線程 ok主線程拋異常。父線程拋出異常我們?cè)陬?lèi)UserController中新增一個(gè)方法testMultThread()該方法在主線程會(huì)拋出異常public void testSuccess() throws Exception { Integer id 1; MyUser user query(id); System.out.println(原記錄: user); update(id); } Transactional(rollbackFor Exception.class) public void testMultThread() throws Exception { new Thread(new Runnable() { SneakyThrows Override public void run() { testSuccess(); } }).start(); throw new Exception(測(cè)試事務(wù)不生效); }上面這種場(chǎng)景不生效很好理解子線程的異常不會(huì)被外部的線程捕獲testMultThread這個(gè)方法的調(diào)用不拋異常因此不會(huì)觸發(fā)事務(wù)回滾調(diào)用方式和前面一樣就換一個(gè)方法uc.testMultThread();這里提醒一下輸出的數(shù)據(jù)因?yàn)槎际窃谥骶€程中輸出所以輸出結(jié)果都是“張三”后來(lái)我看了庫(kù)表發(fā)現(xiàn)DB數(shù)據(jù)已經(jīng)更新為“張三-testing”所以事務(wù)回滾沒(méi)有生效。子線程拋出異常我們修改代碼如下讓子任務(wù)拋出異常public void testSuccess() throws Exception { Integer id 1; MyUser user query(id); System.out.println(原記錄: user); update(id); throw new Exception(測(cè)試事務(wù)生效); } Transactional(rollbackFor Exception.class) public void testMultThread() throws Exception { new Thread(new Runnable() { SneakyThrows Override public void run() { testSuccess(); } }).start(); }同上DB數(shù)據(jù)沒(méi)有回滾發(fā)現(xiàn)DB數(shù)據(jù)已經(jīng)更新為“張三-testing”所以事務(wù)回滾沒(méi)有生效。Case5: 傳播屬性這個(gè)內(nèi)容我后面再單獨(dú)出一篇文章來(lái)講要不然這篇文章篇幅又太長(zhǎng)了。