環境
數據庫: oracle 11g
JAR:
- org.springframework:spring-jdbc:4.3.8.RELEASE
- org.mybatis:mybatis:3.4.2
概念
REQUIRED(默認): 表示當前方法必須運行在事務中。如果當前事務存在,方法將會在該事務中運行。否則,會啟動一個新的事務。
REQUIRED_NEW: 表示當前方法必須運行在它自己的事務中。一個新的事務將被啟動,如果存在當前事務,在該方法執行期間,當前事務會被掛起。
早前對NEW的理解只是停留在: 當前方法會新開啟一個事務,新事務的提交/回滾不會影響之前事務的提交/回滾。
但"當前事務會被掛起",那么NEW事務結束后,被掛起的當前事務應該是會恢復,但會恢復哪些東西了?
(摘自: 在Spring中使用PROPAGATION_REQUIRES_NEW帶來的緩存問題?)
源碼分析:
在TransactionTemplate的<T> T execute(TransactionCallback<T> action)中會通過TransactionManager的getTransaction方法來取得TransactionStatus
1、取得當前線程所關聯的SessionHolder
2、若存在SessionHolder並且開啟了事務(this.sessionHolder != null && this.sessionHolder.getTransaction() != null),而且當前的的傳播行為為PROPAGATION_REQUIRES_NEW
3、掛起當前線程綁定的事務及其事務同步器,取消當前線程和當前session和connection的綁定,並保存所有的掛起信息以供恢復
4、創建新的session,並開啟新的事務
5、執行TransactionTemplate的TransactionCallback回調
6、在新事務提交后,會恢復上個事務的所有信息和執行
測試問題描述
1、 method_A(T1事務)中查詢了某條記錄A(A.name = 1);然后調用method_B(T2事務,並且是REQUIRES_NEW )
2、在 method_B中修改了記錄A並提交保存(A.name = 2);(method_B方法結束,T2事務提交)
3、然后回到 method_A 中再次查詢這條記錄A,發現得到的A.name = 1;但是此時數據庫中的A.name = 2 。
用hibernate/mybatis得到的都是以上結果, 即在T1中查到的A.name = 1.
但是,我用spring的JdbcTemplate測試卻發現: 本來在(3)得到A.name=1,但實際A.name =2。
為什么mybatis&hibernate得到的結果與JdbcTemplate不一樣了?事務不是統一交給spring控制的嗎?
(見問題: Spring事務REQUIRES_NEW的問題?)
個人理解
最初, 我以為在(3)一定會得到是A.name = 2。但實際在hibernate中得到的是A.name =1。(當時遇到問題用的是hibernate。)
然后我就重新去理解了REQUIRES_NEW, 發現應該是要得到A.name = 1, 即mybatis&hibernate得到的就是正確的。
但無意用JdbcTemplate測試卻發現和mybatis&hibernate得到的不一樣, 所以就迷茫了。完全不知道怎么理解。
現在在想一想, 到底什么是"事務控制"? 其實現在我也不知道怎么理解, 渣狗一只, 汪汪~~
可以參考:
spring jdbcTemplate 事務,各種詭異,包你醍醐灌頂!
spring事務源碼解析 (這個可以細看)
但為什么覺得mybatis&hibernate的結果更正確呢?
1、更能體現出事務的一致性。
因為對t1事務來說,並沒有改變過A.name。所以,從始至終都應該得到A.name =1。
一、spring boot公共部分

@SpringBootApplication public class TransactionApplication { public final static String ID = "1"; public static void main(String[] args) { SpringApplication app = new SpringApplication(TransactionApplication.class); app.run(args); } }

#### 28.1.2. 連接到一個生產環境數據庫 ## 注:其他的連接池可以手動配置。如果你定義自己的DataSource bean,自動配置不會發生。 ## 詳細屬性配置參考:https://segmentfault.com/a/1190000004316491 #### ## Spring Boot能夠從大多數數據庫的url上推斷出driver-class-name,那么你就不需要再指定它了。 spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver ## 默認會從url推斷driver class spring.datasource.url=jdbc:oracle:thin:@127.0.0.1:1521:orcl spring.datasource.username=vergilyn spring.datasource.password=409839163 ## JDNI # spring.datasource.jndi-name=java:jboss/datasources/customers

public class Parent implements Serializable{ private int parentId; private String parentName; public Parent() { } public Parent(int parentId, String parentName) { this.parentId = parentId; this.parentName = parentName; } public int getParentId() { return parentId; } public void setParentId(int parentId) { this.parentId = parentId; } public String getParentName() { return parentName; } public void setParentName(String parentName) { this.parentName = parentName; } @Override public String toString() { return "Parent{" +"parentId=" + parentId +", parentName='" + parentName + '\'' +'}'; } }
數據庫:
二、JdbcTemplate測試

@Service public class JdbcMainService { @Autowired private JdbcParentDao parentDao; @Autowired private JdbcNewService newService; @Transactional(propagation = Propagation.REQUIRED) public void updateCur() { Parent init = parentDao.getEntity(TransactionApplication.ID); System.out.println("init select: " + init); Parent curu = this.updateCur(init); System.out.println("curu update: " + curu); Parent curs = parentDao.getEntity(TransactionApplication.ID); System.out.println("curs select: " + curs); Parent news = newService.getEntity(TransactionApplication.ID); System.out.println("news select: " + news); } @Transactional(propagation = Propagation.REQUIRED) public void updateNew() { Parent init = parentDao.getEntity(TransactionApplication.ID); System.out.println("init select: " + init); Parent newu = newService.updateNew(init); System.out.println("newu update: " + newu); Parent curs = parentDao.getEntity(TransactionApplication.ID); System.out.println("curs select: " + curs); Parent news = newService.getEntity(TransactionApplication.ID); System.out.println("news select: " + news); } @Transactional(propagation = Propagation.REQUIRED) public Parent updateCur(Parent parent) { Parent p2 = new Parent(); p2.setParentId(parent.getParentId()); p2.setParentName(parent.getParentName() + "@cur"); parentDao.update(p2); return p2; } }

@Service public class JdbcNewService { @Autowired private JdbcParentDao jdbcParentDao; @Transactional(propagation = Propagation.REQUIRES_NEW) public Parent updateNew(Parent parent){ Parent p2 = new Parent(); p2.setParentId(parent.getParentId()); p2.setParentName(parent.getParentName() + "@new"); jdbcParentDao.update(p2); return p2; } @Transactional(propagation = Propagation.REQUIRES_NEW) public Parent getEntity(String id) { return jdbcParentDao.getEntity(id); } }

@Repository public class JdbcParentDao { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private NamedParameterJdbcTemplate namedJdbc; public Parent getEntity(String id){ String sql = "select * from parent where parent_id = ?"; return this.jdbcTemplate.queryForObject(sql,new Object[]{id},new BeanPropertyRowMapper<Parent>(Parent.class)); } public void update(Parent parent){ String sql = "update parent set parent_name = ? where parent_id = ?"; this.jdbcTemplate.update(sql,new Object[]{parent.getParentName(),parent.getParentId()}); } }
2.1 說明
JdbcMainService: 其中的方法全是REQUIRED,且此class是主要的測試class。
JdbcNewService: 其中的方法全是REQUIRES_NEW。
JdbcParentDao: 共用的dao層方法。
2.2 測試JdbcMainService.updateCur()
@Transactional(propagation = Propagation.REQUIRED) public void updateCur() { Parent init = parentDao.getEntity(TransactionApplication.ID); System.out.println("init select: " + init); // init select: Parent{parentId=1, parentName='周父'} Parent curu = this.updateCur(init); System.out.println("curu update: " + curu); // curu update: Parent{parentId=1, parentName='周父@cur'} Parent curs = parentDao.getEntity(TransactionApplication.ID); System.out.println("curs select: " + curs); // curs select: Parent{parentId=1, parentName='周父@cur'} // 此時數據庫的實際值是: parentName='周父', 因為cur方法還未結束, 即cur事務還未提交。
Parent news = newService.getEntity(TransactionApplication.ID); System.out.println("news select: " + news); // news select: Parent{parentId=1, parentName='周父'} }
分析: 以上結果都在預料之內。因為update是在cur事務中, 所以cur事務中再次select可以得到cur事務的更新但未提交的數據, 即"curs select"得到的是CUR事務update后的值, 而非當前數據庫中的值。
這也證明了CUR事務、NEW事務是兩個完全獨立的事務。
回去再看NEW描述中的"當前事務會被掛起", 是否是在NEW事務執行完返回CUR事務, 會恢復CUR事務的所有信息和執行。
2.2 測試JdbcMainService.updateNew()
@Transactional(propagation = Propagation.REQUIRED) public void updateNew() { Parent init = parentDao.getEntity(TransactionApplication.ID); System.out.println("init select: " + init); // init select: Parent{parentId=1, parentName='周父'} Parent newu = newService.updateNew(init); System.out.println("newu update: " + newu); // newu update: Parent{parentId=1, parentName='周父@new'} // 用REQUIRES_NEW更新, 此時數據庫已是最新值 parentName='周父@new' Parent curs = parentDao.getEntity(TransactionApplication.ID); System.out.println("curs select: " + curs); // curs select: Parent{parentId=1, parentName='周父@new'} Parent news = newService.getEntity(TransactionApplication.ID); System.out.println("news select: " + news); // news select: Parent{parentId=1, parentName='周父@new'} }
問題: 為什么"curs select"得到的會是parentName='周父@new'?
針對之前對REQUIRED與REQUIRES_NEW的理解, 返回到CUR事務中查詢到的結果應該是與"init select"一樣才對。但現在明顯得到的是數據庫最新值。
三、mybatis測試

@Service public class MybatisMainService { @Autowired private MybatisParentMapper parentMapper; @Autowired private MybatisNewService newService; @Transactional(propagation = Propagation.REQUIRED) public void updateCur(){ Parent init = parentMapper.getEntity(TransactionApplication.ID); System.out.println("init select: " + init); Parent curu = this.updateCur(init); System.out.println("curu update: " + curu); Parent curs = parentMapper.getEntity(TransactionApplication.ID); System.out.println("curs select: " + curs); Parent news = newService.getEntity(TransactionApplication.ID); System.out.println("news select: " + news); Parent nocs = parentMapper.getNoCacheEntity(TransactionApplication.ID); System.out.println("nocs select: " + nocs); } @Transactional(propagation = Propagation.REQUIRED) public void updateNew(){ Parent init = parentMapper.getEntity(TransactionApplication.ID); System.out.println("init select: " + init); Parent newu = newService.updateNew(init); System.out.println("newu update: " + newu); Parent curs = parentMapper.getEntity(TransactionApplication.ID); System.out.println("curs select: " + curs); Parent news = newService.getEntity(TransactionApplication.ID); System.out.println("news select: " + news); Parent nocs = parentMapper.getNoCacheEntity(TransactionApplication.ID); System.out.println("nocs select: " + nocs); } @Transactional(propagation = Propagation.REQUIRED) public Parent updateCur(Parent parent){ Parent p2 = new Parent(); p2.setParentId(parent.getParentId()); p2.setParentName(parent.getParentName() + "@cur"); parentMapper.update(p2); return p2; } }

@Service public class MybatisNewService { @Autowired private MybatisParentMapper parentMapper; @Transactional(propagation = Propagation.REQUIRES_NEW) public Parent updateNew(Parent parent){ Parent p2 = new Parent(); p2.setParentId(parent.getParentId()); p2.setParentName(parent.getParentName() + "@new"); parentMapper.update(p2); return p2; } @Transactional(propagation = Propagation.REQUIRES_NEW) public Parent getEntity(String id) { return parentMapper.getEntity(id); } }

@Mapper public interface MybatisParentMapper { @Select("select * from parent where PARENT_ID = #{id}") @Results({ @Result(id = true, column = "PARENT_ID", property = "parentId"), @Result(column = "PARENT_NAME", property = "parentName") }) Parent getEntity(@Param("id") String id); @Select("select * from parent where PARENT_ID = #{id}") @Results({ @Result(id = true, column = "PARENT_ID", property = "parentId"), @Result(column = "PARENT_NAME", property = "parentName") }) @Options(flushCache = Options.FlushCachePolicy.TRUE,useCache = false) Parent getNoCacheEntity(@Param("id") String id); @Update("update parent set PARENT_NAME=#{parentName} where PARENT_ID = #{parentId}") @Results({ @Result(id = true, column = "PARENT_ID", property = "parentId"), @Result(column = "PARENT_NAME", property = "parentName") }) void update(Parent parent); }
3.1 說明
MybatisMainService: 其中的方法全是REQUIRED,且此class是主要的測試class。
MybatisNewService: 其中的方法全是REQUIRES_NEW。
MybatisParentMapper: mybatis純注解的共用dao。
3.2 測試MybatisMainService.updateCur()
@Transactional(propagation = Propagation.REQUIRED) public void updateCur(){ Parent init = parentMapper.getEntity(TransactionApplication.ID); System.out.println("init select: " + init); // init select: Parent{parentId=1, parentName='周父'} Parent curu = this.updateCur(init); System.out.println("curu update: " + curu); // curu update: Parent{parentId=1, parentName='周父@cur'} Parent curs = parentMapper.getEntity(TransactionApplication.ID); System.out.println("curs select: " + curs); // curs select: Parent{parentId=1, parentName='周父@cur'} Parent news = newService.getEntity(TransactionApplication.ID); System.out.println("news select: " + news); // news select: Parent{parentId=1, parentName='周父'} Parent nocs = parentMapper.getNoCacheEntity(TransactionApplication.ID); System.out.println("nocs select: " + nocs); // nocs select: Parent{parentId=1, parentName='周父@cur'} }
1) "curs select"確實是得到當前事務的結果(即使是未提交的)。
2) "news select"得到的是現在數據庫中的結果。
3) "nocs select"指定了@Options(flushCache = Options.FlushCachePolicy.TRUE,useCache = false),為什么得到的會是parentName='周父@cur',而不是parentName='周父'。
雖然現在數據庫中的值是parentName='周父',但是當前事務下當前數據庫中的值是parentName='周父@cur'(未提交)。
所以,當前事務中即使是useCache=flase,重新從數據庫去查詢,當前事務下的最新值就是parentName='周父'。
3.3 測試MybatisMainService.updateNew()
@Transactional(propagation = Propagation.REQUIRED) public void updateNew(){ Parent init = parentMapper.getEntity(TransactionApplication.ID); System.out.println("init select: " + init); // init select: Parent{parentId=1, parentName='周父'} Parent newu = newService.updateNew(init); System.out.println("newu update: " + newu); // newu update: Parent{parentId=1, parentName='周父@new'} Parent curs = parentMapper.getEntity(TransactionApplication.ID); System.out.println("curs select: " + curs); // curs select: Parent{parentId=1, parentName='周父'} Parent news = newService.getEntity(TransactionApplication.ID); System.out.println("news select: " + news); // news select: Parent{parentId=1, parentName='周父@new'} Parent nocs = parentMapper.getNoCacheEntity(TransactionApplication.ID); System.out.println("nocs select: " + nocs); // nocs select: Parent{parentId=1, parentName='周父@new'}
Parent curs2 = parentMapper.getEntity(TransactionApplication.ID); System.out.println("curs2 select: " + curs2); // curs2 select: Parent{parentId=1, parentName='周父@new'} }
1) "curs select"得到當前事務的結果,所以parentName='周父'。
2) "news select"&"nocs select"為什么得到parentName='周父@new'?
按3.2的測試結論,如果nocs得到的是當前事務下的最新結果,那么當前事務的最新結果是什么? 從結果看,是parentName='周父@new'。
個人又覺得為了事務的一致性,覺得應該得到parentName='周父'。即應該和"curs select"得到一樣的結果。
這種對事務的理解是對的嗎(不管是交由spring控制的,還是oracle等數據庫實際的事務控制)?
先說個人在此對"nocs select"得到結果的解釋: 當前事務中的最新值就是parentName='周父@new',即new事務提交后的結果已經對cur可見。
現在用PL/SQL做這么2個測試:
這是否可以證明:
只要更新被提交后,更新后的值對別的事務即是可見的。所以,別的事務得到的結果就是數據庫當前的值=別的事務更新后的值。
未提交的,對別的事務是不可見的,即別的事務得到的還是數據庫目前的值。
3) "curs select"為什么得到parentName='周父'?
按2)中的說法,new更新已經提交了那么得到的不應該是parentName='周父@new'嗎? (回過去看JdbcTemplate此處得到的是什么, parentName='周父@new')
現在是不是覺得JdbcTemplate才是最純粹的原始事務呢,那為什么mybatis&hibernate中會得到此結果呢?
這主要是mybatis&hibernate中都存在緩存(主要是一級緩存造成的),所以JdbcTemplate與mybatis&hibernate的差異是因為緩存造成的。(JdbcTemplate無緩存一說)
4) "curs2 select"為什么得到parentName='周父@new'?
因為在"curs2 select"執行過"nocs select","nocs select"清空了緩存,並把查詢到的結果重新放入緩存。這之后"curs2 select"又是從緩存中取的結果。所以得到的是parentName='周父@new'。
四、總結JdbcTemplate、mybatis的查詢
// 以下是 JdbcTemplate
@Transactional(propagation = Propagation.REQUIRED) public void getCur(){ Parent s1 = parentDao.getEntity(TransactionApplication.ID); System.out.println("s1 select: " + s1); // s1 select: Parent{parentId=1, parentName='周父'}
// 利用斷點: 手動修改數據庫的值 '周父' -> '周父s2' Parent s2 = parentDao.getEntity(TransactionApplication.ID); System.out.println("s2 select: " + s2); // s2 select: Parent{parentId=1, parentName='周父s2'} // 利用斷點: 手動修改數據庫的值 '周父' -> '周父s3' Parent s3 = parentDao.getEntity(TransactionApplication.ID); System.out.println("s3 select: " + s3); // s3 select: Parent{parentId=1, parentName='周父s3'} }
// 以下是 mybatis
@Transactional(propagation = Propagation.REQUIRED) public void getCur(){ Parent s1 = parentMapper.getEntity(TransactionApplication.ID); System.out.println("s1 select: " + s1); // s1 select: Parent{parentId=1, parentName='周父'} // 利用斷點: 手動修改數據庫的值 '周父' -> '周父s2' Parent s2 = parentMapper.getEntity(TransactionApplication.ID); System.out.println("s2 select: " + s2); // s2 select: Parent{parentId=1, parentName='周父'} // 利用斷點: 手動修改數據庫的值 '周父' -> '周父s3' Parent s3 = parentMapper.getEntity(TransactionApplication.ID); System.out.println("s3 select: " + s3); // s3 select: Parent{parentId=1, parentName='周父'} }
// 以下是mybatis noCache混用測試 @Transactional(propagation = Propagation.REQUIRED) public void getNoCache(){ Parent s1 = parentMapper.getEntity(TransactionApplication.ID); System.out.println("s1 select: " + s1); // s1 select: Parent{parentId=1, parentName='周父'} // 利用斷點: 手動修改數據庫的值 '周父' -> '周父s2' Parent s2 = parentMapper.getEntity(TransactionApplication.ID); System.out.println("s2 select: " + s2); // s2 select: Parent{parentId=1, parentName='周父'} 從緩存取值,所以s2=s1 Parent s3 = parentMapper.getNoCacheEntity(TransactionApplication.ID); System.out.println("s3 select: " + s3); // s3 select: Parent{parentId=1, parentName='周父s2'} 清空緩存重新查詢 Parent s4 = parentMapper.getEntity(TransactionApplication.ID); System.out.println("s4 select: " + s4); // s4 select: Parent{parentId=1, parentName='周父s2'} 從緩存取值,所以s4=s3 // 利用斷點: 手動修改數據庫的值 '周父' -> '周父s3' Parent s5 = parentMapper.getNoCacheEntity(TransactionApplication.ID); System.out.println("s5 select: " + s5); // s5 select: Parent{parentId=1, parentName='周父s3'} }
現在在回來看這兩種select得到的兩種結果。(個人從以上測試得到結論,並不一定正確,希望高人指出正確的理解)
1) mybatis的觀念: (默認情況下)從始至終當前事務都不應該看到別的事務的結果,即使是別的事務已經提交。
2) jdbcTemplate(或oracle)的觀念: 只要事務被提交,另一個事務中就可以看到它提交的結果。
所以,現在看來也不能說哪種更好,只是個人偏向於mybatis。最主要的知道了有這差異(感覺主要是對mybatis&hibernate的緩存最用不夠理解,只是知道存在緩存這一說)
五、題外話
在測試JdbcTemplate的時候,可能有一種猜測是: 並沒有在一個事務中,或者並沒有在同一個連接中,所以JdbcTemplate得到的查詢結果永遠是最新的。
這個可以很容易證明是同一個連接。見源碼: org.springframework.jdbc.datasource.DataSourceUtils
package org.springframework.jdbc.datasource; public abstract class DataSourceUtils { public static final int CONNECTION_SYNCHRONIZATION_ORDER = 1000; private static final Log logger = LogFactory.getLog(DataSourceUtils.class); public DataSourceUtils() { } public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { try { return doGetConnection(dataSource); } catch (SQLException var2) { throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", var2); } } public static Connection doGetConnection(DataSource dataSource) throws SQLException { Assert.notNull(dataSource, "No DataSource specified"); ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource); if(conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) { logger.debug("Fetching JDBC Connection from DataSource"); Connection con = dataSource.getConnection(); if(TransactionSynchronizationManager.isSynchronizationActive()) { logger.debug("Registering transaction synchronization for JDBC Connection"); ConnectionHolder holderToUse = conHolder; if(conHolder == null) { holderToUse = new ConnectionHolder(con); } else { conHolder.setConnection(con); } holderToUse.requested(); TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource)); holderToUse.setSynchronizedWithTransaction(true); if(holderToUse != conHolder) { TransactionSynchronizationManager.bindResource(dataSource, holderToUse); } } System.out.println("datasource cur: " + con); return con; } else { conHolder.requested(); if(!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(dataSource.getConnection()); } System.out.println("datasource new: " + conHolder.getConnection()); return conHolder.getConnection(); } } }